diff options
Diffstat (limited to 'tuskar_ui/api/heat.py')
-rw-r--r-- | tuskar_ui/api/heat.py | 553 |
1 files changed, 0 insertions, 553 deletions
diff --git a/tuskar_ui/api/heat.py b/tuskar_ui/api/heat.py deleted file mode 100644 index 18056de5..00000000 --- a/tuskar_ui/api/heat.py +++ /dev/null @@ -1,553 +0,0 @@ -# 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, - 'timeout_mins': 240, - } - 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 [] - - try: - services = client.services.list() - for service in services: - if service.name == 'horizon': - break - else: - return [] - except Exception: - 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 |