# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging import os import tempfile import urlparse from django.conf import settings from django.utils.translation import ugettext_lazy as _ from heatclient.common import template_utils from heatclient.exc import HTTPNotFound from horizon.utils import memoized from openstack_dashboard.api import base from openstack_dashboard.api import heat from openstack_dashboard.api import keystone from tuskar_ui.api import node from tuskar_ui.api import tuskar from tuskar_ui.cached_property import cached_property # noqa from tuskar_ui.handle_errors import handle_errors # noqa from tuskar_ui.utils import utils LOG = logging.getLogger(__name__) @memoized.memoized def overcloud_keystoneclient(request, endpoint, password): """Returns a client connected to the Keystone backend. Several forms of authentication are supported: * Username + password -> Unscoped authentication * Username + password + tenant id -> Scoped authentication * Unscoped token -> Unscoped authentication * Unscoped token + tenant id -> Scoped authentication * Scoped token -> Scoped authentication Available services and data from the backend will vary depending on whether the authentication was scoped or unscoped. Lazy authentication if an ``endpoint`` parameter is provided. Calls requiring the admin endpoint should have ``admin=True`` passed in as a keyword argument. The client is cached so that subsequent API calls during the same request/response cycle don't have to be re-authenticated. """ api_version = keystone.VERSIONS.get_active_version() # TODO(lsmola) add support of certificates and secured http and rest of # parameters according to horizon and add configuration to local settings # (somehow plugin based, we should not maintain a copy of settings) LOG.debug("Creating a new keystoneclient connection to %s." % endpoint) # TODO(lsmola) we should create tripleo-admin user for this purpose # this needs to be done first on tripleo side conn = api_version['client'].Client(username="admin", password=password, tenant_name="admin", auth_url=endpoint) return conn def _save_templates(templates): """Saves templates into tmpdir on server This should go away and get replaced by libutils.save_templates from tripleo-common https://github.com/openstack/tripleo-common/ """ output_dir = tempfile.mkdtemp() for template_name, template_content in templates.items(): # It's possible to organize the role templates and their dependent # files into directories, in which case the template_name will carry # the directory information. If that's the case, first create the # directory structure (if it hasn't already been created by another # file in the templates list). template_dir = os.path.dirname(template_name) output_template_dir = os.path.join(output_dir, template_dir) if template_dir and not os.path.exists(output_template_dir): os.makedirs(output_template_dir) filename = os.path.join(output_dir, template_name) with open(filename, 'w+') as template_file: template_file.write(template_content) return output_dir def _process_templates(templates): """Process templates Due to bug in heat api https://bugzilla.redhat.com/show_bug.cgi?id=1212740, we need to save the templates in tmpdir, reprocess them with template_utils from heatclient and then we can use them in creating/updating stack. This should be replaced by the same code that is in tripleo-common and eventually it will not be needed at all. """ tpl_dir = _save_templates(templates) tpl_files, template = template_utils.get_template_contents( template_file=os.path.join(tpl_dir, tuskar.MASTER_TEMPLATE_NAME)) env_files, env = ( template_utils.process_multiple_environments_and_files( env_paths=[os.path.join(tpl_dir, tuskar.ENVIRONMENT_NAME)])) files = dict(list(tpl_files.items()) + list(env_files.items())) return template, env, files class Stack(base.APIResourceWrapper): _attrs = ('id', 'stack_name', 'outputs', 'stack_status', 'parameters') def __init__(self, apiresource, request=None): super(Stack, self).__init__(apiresource) self._request = request @classmethod def create(cls, request, stack_name, templates): template, environment, files = _process_templates(templates) fields = { 'stack_name': stack_name, 'template': template, 'environment': environment, 'files': files, } password = getattr(settings, 'UNDERCLOUD_ADMIN_PASSWORD', None) stack = heat.stack_create(request, password, **fields) return cls(stack, request=request) def update(self, request, stack_name, templates): template, environment, files = _process_templates(templates) fields = { 'stack_name': stack_name, 'template': template, 'environment': environment, 'files': files, } password = getattr(settings, 'UNDERCLOUD_ADMIN_PASSWORD', None) heat.stack_update(request, self.id, password, **fields) @classmethod @handle_errors(_("Unable to retrieve heat stacks"), []) def list(cls, request): """Return a list of stacks in Heat :param request: request object :type request: django.http.HttpRequest :return: list of Heat stacks, or an empty list if there are none :rtype: list of tuskar_ui.api.heat.Stack """ stacks, has_more_data, has_prev_data = heat.stacks_list(request) return [cls(stack, request=request) for stack in stacks] @classmethod @handle_errors(_("Unable to retrieve stack")) def get(cls, request, stack_id): """Return the Heat Stack associated with this Overcloud :return: Heat Stack associated with the stack_id; or None if no Stack is associated, or no Stack can be found :rtype: tuskar_ui.api.heat.Stack or None """ return cls(heat.stack_get(request, stack_id), request=request) @classmethod @handle_errors(_("Unable to retrieve stack")) def get_by_plan(cls, request, plan): """Return the Heat Stack associated with a Plan :return: Heat Stack associated with the plan; or None if no Stack is associated, or no Stack can be found :rtype: tuskar_ui.api.heat.Stack or None """ # TODO(lsmola) until we have working deployment through Tuskar-API, # this will not work # for stack in Stack.list(request): # if stack.plan and (stack.plan.id == plan.id): # return stack try: stack = Stack.list(request)[0] except IndexError: return None # TODO(lsmola) stack list actually does not contain all the detail # info, there should be call for that, investigate return Stack.get(request, stack.id) @classmethod @handle_errors(_("Unable to delete Heat stack"), []) def delete(cls, request, stack_id): heat.stack_delete(request, stack_id) @memoized.memoized def resources(self, with_joins=True, role=None): """Return list of OS::Nova::Server Resources Return list of OS::Nova::Server Resources associated with the Stack and which are associated with a Role :param with_joins: should we also retrieve objects associated with each retrieved Resource? :type with_joins: bool :return: list of all Resources or an empty list if there are none :rtype: list of tuskar_ui.api.heat.Resource """ if role: roles = [role] else: roles = self.plan.role_list resource_dicts = [] # A provider resource is deployed as a nested stack, so we have to # drill down and retrieve those that match a tuskar role for role in roles: resource_group_name = role.name try: resource_group = heat.resource_get(self._request, self.id, resource_group_name) group_resources = heat.resources_list( self._request, resource_group.physical_resource_id) for group_resource in group_resources: if not group_resource.physical_resource_id: # Skip groups who has no physical resource. continue nova_resources = heat.resources_list( self._request, group_resource.physical_resource_id) resource_dicts.extend([{"resource": resource, "role": role} for resource in nova_resources]) except HTTPNotFound: pass if not with_joins: return [Resource(rd['resource'], request=self._request, stack=self, role=rd['role']) for rd in resource_dicts] nodes_dict = utils.list_to_dict(node.Node.list(self._request, associated=True), key_attribute='instance_uuid') joined_resources = [] for rd in resource_dicts: resource = rd['resource'] joined_resources.append( Resource(resource, node=nodes_dict.get(resource.physical_resource_id, None), request=self._request, stack=self, role=rd['role'])) # TODO(lsmola) I want just resources with nova instance # this could be probably filtered a better way, investigate return [r for r in joined_resources if r.node is not None] @memoized.memoized def resources_count(self, overcloud_role=None): """Return count of associated Resources :param overcloud_role: role of resources to be counted; None means all :type overcloud_role: tuskar_ui.api.tuskar.Role :return: Number of matching resources :rtype: int """ # TODO(dtantsur): there should be better way to do it, rather than # fetching and calling len() # FIXME(dtantsur): should also be able to use with_joins=False # but unable due to bug #1289505 if overcloud_role is None: resources = self.resources() else: resources = self.resources(role=overcloud_role) return len(resources) @cached_property def plan(self): """return associated Plan if a plan_id exists within stack parameters. :return: associated Plan if plan_id exists and a matching plan exists as well; None otherwise :rtype: tuskar_ui.api.tuskar.Plan """ # TODO(lsmola) replace this by actual reference, I am pretty sure # the relation won't be stored in parameters, that would mean putting # that into template, which doesn't make sense # if 'plan_id' in self.parameters: # return tuskar.Plan.get(self._request, # self.parameters['plan_id']) try: plan = tuskar.Plan.list(self._request)[0] except IndexError: return None return plan @cached_property def is_initialized(self): """Check if this Stack is successfully initialized. :return: True if this Stack is successfully initialized, False otherwise :rtype: bool """ return len(self.dashboard_urls) > 0 @cached_property def is_deployed(self): """Check if this Stack is successfully deployed. :return: True if this Stack is successfully deployed, False otherwise :rtype: bool """ return self.stack_status in ('CREATE_COMPLETE', 'UPDATE_COMPLETE') @cached_property def is_deploying(self): """Check if this Stack is currently deploying. :return: True if deployment is in progress, False otherwise. :rtype: bool """ return self.stack_status in ('CREATE_IN_PROGRESS',) @cached_property def is_updating(self): """Check if this Stack is currently updating. :return: True if updating is in progress, False otherwise. :rtype: bool """ return self.stack_status in ('UPDATE_IN_PROGRESS',) @cached_property def is_failed(self): """Check if this Stack failed to update or deploy. :return: True if deployment there was an error, False otherwise. :rtype: bool """ return self.stack_status in ('CREATE_FAILED', 'UPDATE_FAILED',) @cached_property def is_deleting(self): """Check if this Stack is deleting. :return: True if Stack is deleting, False otherwise. :rtype: bool """ return self.stack_status in ('DELETE_IN_PROGRESS', ) @cached_property def is_delete_failed(self): """Check if Stack deleting has failed. :return: True if Stack deleting has failed, False otherwise. :rtype: bool """ return self.stack_status in ('DELETE_FAILED', ) @cached_property def events(self): """Return the Heat Events associated with this Stack :return: list of Heat Events associated with this Stack; or an empty list if there is no Stack associated with this Stack, or there are no Events :rtype: list of heatclient.v1.events.Event """ return heat.events_list(self._request, self.stack_name) @property def stack_outputs(self): return getattr(self, 'outputs', []) @cached_property def keystone_auth_url(self): for output in self.stack_outputs: if output['output_key'] == 'KeystoneURL': return output['output_value'] @cached_property def keystone_ip(self): if self.keystone_auth_url: return urlparse.urlparse(self.keystone_auth_url).hostname @cached_property def overcloud_keystone(self): try: return overcloud_keystoneclient( self._request, self.keystone_auth_url, self.plan.parameter_value('Controller-1::AdminPassword')) except Exception: LOG.debug('Unable to connect to overcloud keystone.') return None @cached_property def dashboard_urls(self): client = self.overcloud_keystone if not client: return [] services = client.services.list() for service in services: if service.name == 'horizon': break else: return [] admin_urls = [endpoint.adminurl for endpoint in client.endpoints.list() if endpoint.service_id == service.id] return admin_urls class Resource(base.APIResourceWrapper): _attrs = ('resource_name', 'resource_type', 'resource_status', 'physical_resource_id') def __init__(self, apiresource, request=None, **kwargs): """Initialize a resource :param apiresource: apiresource we want to wrap :type apiresource: heatclient.v1.resources.Resource :param request: request :type request: django.core.handlers.wsgi.WSGIRequest :param node: node relation we want to cache :type node: tuskar_ui.api.node.Node :return: Resource object :rtype: Resource """ super(Resource, self).__init__(apiresource) self._request = request if 'node' in kwargs: self._node = kwargs['node'] if 'stack' in kwargs: self._stack = kwargs['stack'] if 'role' in kwargs: self._role = kwargs['role'] @classmethod @memoized.memoized def _resources_by_nodes(cls, request): return {resource.physical_resource_id: resource for resource in cls.list_all_resources(request)} @classmethod def get_by_node(cls, request, node): """Return the specified Heat Resource given a Node :param request: request object :type request: django.http.HttpRequest :param node: node to match :type node: tuskar_ui.api.node.Node :return: matching Resource, or raises LookupError if no resource matches the node :rtype: tuskar_ui.api.heat.Resource """ return cls._resources_by_nodes(request)[node.instance_uuid] @classmethod def list_all_resources(cls, request): """Iterate through all the stacks and return all relevant resources :param request: request object :type request: django.http.HttpRequest :return: list of resources :rtype: list of tuskar_ui.api.heat.Resource """ all_resources = [] for stack in Stack.list(request): all_resources.extend(stack.resources(with_joins=False)) return all_resources @cached_property def role(self): """Return the Role associated with this Resource :return: Role associated with this Resource, or None if no Role is associated :rtype: tuskar_ui.api.tuskar.Role """ if hasattr(self, '_role'): return self._role @cached_property def node(self): """Return the Ironic Node associated with this Resource :return: Ironic Node associated with this Resource, or None if no Node is associated :rtype: tuskar_ui.api.node.Node :raises: ironicclient.exc.HTTPNotFound if there is no Node with the matching instance UUID """ if hasattr(self, '_node'): return self._node if self.physical_resource_id: return node.Node.get_by_instance_uuid(self._request, self.physical_resource_id) return None @cached_property def stack(self): """Return the Stack associated with this Resource :return: Stack associated with this Resource, or None if no Stack is associated :rtype: tuskar_ui.api.heat.Stack """ if hasattr(self, '_stack'): return self._stack