diff options
Diffstat (limited to 'tuskar_ui/api')
-rw-r--r-- | tuskar_ui/api/__init__.py | 0 | ||||
-rw-r--r-- | tuskar_ui/api/flavor.py | 121 | ||||
-rw-r--r-- | tuskar_ui/api/heat.py | 553 | ||||
-rw-r--r-- | tuskar_ui/api/node.py | 445 | ||||
-rw-r--r-- | tuskar_ui/api/tuskar.py | 558 |
5 files changed, 0 insertions, 1677 deletions
diff --git a/tuskar_ui/api/__init__.py b/tuskar_ui/api/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/tuskar_ui/api/__init__.py +++ /dev/null diff --git a/tuskar_ui/api/flavor.py b/tuskar_ui/api/flavor.py deleted file mode 100644 index 518400cc..00000000 --- a/tuskar_ui/api/flavor.py +++ /dev/null @@ -1,121 +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 - -from django.utils.translation import ugettext_lazy as _ -from horizon.utils import memoized -from openstack_dashboard.api import nova - -import tuskar_ui -from tuskar_ui.cached_property import cached_property # noqa -from tuskar_ui.handle_errors import handle_errors # noqa - - -LOG = logging.getLogger(__name__) - - -class Flavor(object): - - def __init__(self, flavor): - """Construct by wrapping Nova flavor - - :param flavor: Nova flavor - :type flavor: novaclient.v2.flavors.Flavor - """ - self._flavor = flavor - - def __getattr__(self, name): - return getattr(self._flavor, name) - - @property - def ram_bytes(self): - """Get RAM size in bytes - - Default RAM size is in MB. - """ - return self.ram * 1024 * 1024 - - @property - def disk_bytes(self): - """Get disk size in bytes - - Default disk size is in GB. - """ - return self.disk * 1024 * 1024 * 1024 - - @cached_property - def extras_dict(self): - """Return extra flavor parameters - - :return: Nova flavor keys - :rtype: dict - """ - return self._flavor.get_keys() - - @property - def cpu_arch(self): - return self.extras_dict.get('cpu_arch', '') - - @property - def kernel_image_id(self): - return self.extras_dict.get('baremetal:deploy_kernel_id', '') - - @property - def ramdisk_image_id(self): - return self.extras_dict.get('baremetal:deploy_ramdisk_id', '') - - @classmethod - def create(cls, request, name, memory, vcpus, disk, cpu_arch, - kernel_image_id=None, ramdisk_image_id=None): - extras_dict = { - 'cpu_arch': cpu_arch, - 'capabilities:boot_option': 'local', - } - if kernel_image_id is not None: - extras_dict['baremetal:deploy_kernel_id'] = kernel_image_id - if ramdisk_image_id is not None: - extras_dict['baremetal:deploy_ramdisk_id'] = ramdisk_image_id - return cls(nova.flavor_create(request, name, memory, vcpus, disk, - metadata=extras_dict)) - - @classmethod - @handle_errors(_("Unable to load flavor.")) - def get(cls, request, flavor_id): - return cls(nova.flavor_get(request, flavor_id)) - - @classmethod - @handle_errors(_("Unable to load flavor.")) - def get_by_name(cls, request, name): - for flavor in cls.list(request): - if flavor.name == name: - return flavor - - @classmethod - @handle_errors(_("Unable to retrieve flavor list."), []) - def list(cls, request): - return [cls(item) for item in nova.flavor_list(request)] - - @classmethod - @memoized.memoized - @handle_errors(_("Unable to retrieve existing servers list."), []) - def list_deployed_ids(cls, request): - """Get and memoize ID's of deployed flavors.""" - servers = nova.server_list(request)[0] - deployed_ids = set(server.flavor['id'] for server in servers) - deployed_names = [] - for plan in tuskar_ui.api.tuskar.Plan.list(request): - deployed_names.extend( - [plan.parameter_value(role.flavor_parameter_name) - for role in plan.role_list]) - return [flavor.id for flavor in cls.list(request) - if flavor.id in deployed_ids or flavor.name in deployed_names] 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 diff --git a/tuskar_ui/api/node.py b/tuskar_ui/api/node.py deleted file mode 100644 index e9fef71b..00000000 --- a/tuskar_ui/api/node.py +++ /dev/null @@ -1,445 +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 time - -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from horizon.utils import memoized -from ironic_inspector_client import client as inspector_client -from ironicclient import client as ironic_client -from openstack_dashboard.api import base -from openstack_dashboard.api import glance -from openstack_dashboard.api import nova - -from tuskar_ui.cached_property import cached_property # noqa -from tuskar_ui.handle_errors import handle_errors # noqa -from tuskar_ui.utils import utils - - -# power states -ERROR_STATES = set(['deploy failed', 'error']) -POWER_ON_STATES = set(['on', 'power on']) - -# provision_states of ironic aggregated to reasonable groups -PROVISION_STATE_FREE = ['available', 'deleted', None] -PROVISION_STATE_PROVISIONED = ['active'] -PROVISION_STATE_PROVISIONING = [ - 'deploying', 'wait call-back', 'rebuild', 'deploy complete'] -PROVISION_STATE_DELETING = ['deleting'] -PROVISION_STATE_ERROR = ['error', 'deploy failed'] - -# names for states of ironic used in UI, -# provison_states + discovery states -DISCOVERING_STATE = 'discovering' -DISCOVERED_STATE = 'discovered' -DISCOVERY_FAILED_STATE = 'discovery failed' -MAINTENANCE_STATE = 'manageable' -PROVISIONED_STATE = 'provisioned' -PROVISIONING_FAILED_STATE = 'provisioning failed' -PROVISIONING_STATE = 'provisioning' -DELETING_STATE = 'deleting' -FREE_STATE = 'free' - - -IRONIC_DISCOVERD_URL = getattr(settings, 'IRONIC_DISCOVERD_URL', None) -LOG = logging.getLogger(__name__) - - -@memoized.memoized -def ironicclient(request): - api_version = 1 - kwargs = {'os_auth_token': request.user.token.id, - 'ironic_url': base.url_for(request, 'baremetal')} - return ironic_client.get_client(api_version, **kwargs) - - -# FIXME(lsmola) This should be done in Horizon, they don't have caching -@memoized.memoized -@handle_errors(_("Unable to retrieve image.")) -def image_get(request, image_id): - """Returns an Image object with metadata - - Returns an Image object populated with metadata for image - with supplied identifier. - - :param image_id: list of objects to be put into a dict - :type image_id: list - - :return: object - :rtype: glanceclient.v1.images.Image - """ - image = glance.image_get(request, image_id) - return image - - -class Node(base.APIResourceWrapper): - _attrs = ('id', 'uuid', 'instance_uuid', 'driver', 'driver_info', - 'properties', 'power_state', 'target_power_state', - 'provision_state', 'maintenance', 'extra') - - def __init__(self, apiresource, request=None, instance=None): - """Initialize a Node - - :param apiresource: apiresource we want to wrap - :type apiresource: IronicNode - - :param request: request - :type request: django.core.handlers.wsgi.WSGIRequest - - :param instance: instance relation we want to cache - :type instance: openstack_dashboard.api.nova.Server - - :return: Node object - :rtype: tusar_ui.api.node.Node - """ - super(Node, self).__init__(apiresource) - self._request = request - self._instance = instance - - @classmethod - def create(cls, request, ipmi_address=None, cpu_arch=None, cpus=None, - memory_mb=None, local_gb=None, mac_addresses=[], - ipmi_username=None, ipmi_password=None, ssh_address=None, - ssh_username=None, ssh_key_contents=None, - deployment_kernel=None, deployment_ramdisk=None, - driver=None): - """Create a Node in Ironic.""" - if driver == 'pxe_ssh': - driver_info = { - 'ssh_address': ssh_address, - 'ssh_username': ssh_username, - 'ssh_key_contents': ssh_key_contents, - 'ssh_virt_type': 'virsh', - } - else: - driver_info = { - 'ipmi_address': ipmi_address, - 'ipmi_username': ipmi_username, - 'ipmi_password': ipmi_password - } - driver_info.update( - deploy_kernel=deployment_kernel, - deploy_ramdisk=deployment_ramdisk - ) - - properties = {'capabilities': 'boot_option:local', } - if cpus: - properties.update(cpus=cpus) - if memory_mb: - properties.update(memory_mb=memory_mb) - if local_gb: - properties.update(local_gb=local_gb) - if cpu_arch: - properties.update(cpu_arch=cpu_arch) - - node = ironicclient(request).node.create( - driver=driver, - driver_info=driver_info, - properties=properties, - ) - for mac_address in mac_addresses: - ironicclient(request).port.create( - node_uuid=node.uuid, - address=mac_address - ) - - return cls(node, request) - - @classmethod - @memoized.memoized - @handle_errors(_("Unable to retrieve node")) - def get(cls, request, uuid): - """Return the Node that matches the ID - - :param request: request object - :type request: django.http.HttpRequest - - :param uuid: ID of Node to be retrieved - :type uuid: str - - :return: matching Node, or None if no IronicNode matches the ID - :rtype: tuskar_ui.api.node.Node - """ - node = ironicclient(request).node.get(uuid) - if node.instance_uuid is not None: - server = nova.server_get(request, node.instance_uuid) - else: - server = None - return cls(node, request, server) - - @classmethod - @handle_errors(_("Unable to retrieve node")) - def get_by_instance_uuid(cls, request, instance_uuid): - """Return the Node associated with the instance ID - - :param request: request object - :type request: django.http.HttpRequest - - :param instance_uuid: ID of Instance that is deployed on the Node - to be retrieved - :type instance_uuid: str - - :return: matching Node - :rtype: tuskar_ui.api.node.Node - - :raises: ironicclient.exc.HTTPNotFound if there is no Node with - the matching instance UUID - """ - node = ironicclient(request).node.get_by_instance_uuid(instance_uuid) - server = nova.server_get(request, instance_uuid) - return cls(node, request, server) - - @classmethod - @memoized.memoized - @handle_errors(_("Unable to retrieve nodes"), []) - def list(cls, request, associated=None, maintenance=None): - """Return a list of Nodes - - :param request: request object - :type request: django.http.HttpRequest - - :param associated: should we also retrieve all Nodes, only those - associated with an Instance, or only those not - associated with an Instance? - :type associated: bool - - :param maintenance: should we also retrieve all Nodes, only those - in maintenance mode, or those which are not in - maintenance mode? - :type maintenance: bool - - :return: list of Nodes, or an empty list if there are none - :rtype: list of tuskar_ui.api.node.Node - """ - nodes = ironicclient(request).node.list(associated=associated, - maintenance=maintenance) - if associated is None or associated: - servers = nova.server_list(request)[0] - servers_dict = utils.list_to_dict(servers) - nodes_with_instance = [] - for n in nodes: - server = servers_dict.get(n.instance_uuid, None) - nodes_with_instance.append(cls(n, instance=server, - request=request)) - return [cls.get(request, node.uuid) - for node in nodes_with_instance] - return [cls.get(request, node.uuid) for node in nodes] - - @classmethod - def delete(cls, request, uuid): - """Delete an Node - - Remove the IronicNode matching the ID if it - exists; otherwise, does nothing. - - :param request: request object - :type request: django.http.HttpRequest - - :param uuid: ID of IronicNode to be removed - :type uuid: str - """ - return ironicclient(request).node.delete(uuid) - - @classmethod - def discover(cls, request, uuids): - """Set the maintenance status of node - - :param request: request object - :type request: django.http.HttpRequest - - :param uuids: IDs of IronicNodes - :type uuids: list of str - """ - if not IRONIC_DISCOVERD_URL: - return - for uuid in uuids: - - inspector_client.introspect( - uuid, - base_url=IRONIC_DISCOVERD_URL, - auth_token=request.user.token.id) - - # NOTE(dtantsur): PXE firmware on virtual machines misbehaves when - # a lot of nodes start DHCPing simultaneously: it ignores NACK from - # DHCP server, tries to get the same address, then times out. Work - # around it by using sleep, anyway introspection takes much longer. - time.sleep(5) - - @classmethod - def set_maintenance(cls, request, uuid, maintenance): - """Set the maintenance status of node - - :param request: request object - :type request: django.http.HttpRequest - - :param uuid: ID of Node to be removed - :type uuid: str - - :param maintenance: desired maintenance state - :type maintenance: bool - """ - patch = { - 'op': 'replace', - 'value': 'True' if maintenance else 'False', - 'path': '/maintenance' - } - node = ironicclient(request).node.update(uuid, [patch]) - return cls(node, request) - - @classmethod - def set_power_state(cls, request, uuid, power_state): - """Set the power_state of node - - :param request: request object - :type request: django.http.HttpRequest - - :param uuid: ID of Node - :type uuid: str - - :param power_state: desired power_state - :type power_state: str - """ - node = ironicclient(request).node.set_power_state(uuid, power_state) - return cls(node, request) - - @classmethod - @memoized.memoized - def list_ports(cls, request, uuid): - """Return a list of ports associated with this Node - - :param request: request object - :type request: django.http.HttpRequest - - :param uuid: ID of IronicNode - :type uuid: str - """ - return ironicclient(request).node.list_ports(uuid) - - @cached_property - def addresses(self): - """Return a list of port addresses associated with this IronicNode - - :return: list of port addresses associated with this IronicNode, or - an empty list if no addresses are associated with - this IronicNode - :rtype: list of str - """ - ports = self.list_ports(self._request, self.uuid) - return [port.address for port in ports] - - @cached_property - def cpus(self): - return self.properties.get('cpus', None) - - @cached_property - def memory_mb(self): - return self.properties.get('memory_mb', None) - - @cached_property - def local_gb(self): - return self.properties.get('local_gb', None) - - @cached_property - def cpu_arch(self): - return self.properties.get('cpu_arch', None) - - @cached_property - def state(self): - if self.maintenance: - if not IRONIC_DISCOVERD_URL: - return MAINTENANCE_STATE - try: - status = inspector_client.get_status( - uuid=self.uuid, - base_url=IRONIC_DISCOVERD_URL, - auth_token=self._request.user.token.id, - ) - except inspector_client.ClientError as e: - if getattr(e.response, 'status_code', None) == 404: - return MAINTENANCE_STATE - raise - if status['error']: - return DISCOVERY_FAILED_STATE - elif status['finished']: - return DISCOVERED_STATE - else: - return DISCOVERING_STATE - else: - if self.provision_state in PROVISION_STATE_FREE: - return FREE_STATE - if self.provision_state in PROVISION_STATE_PROVISIONING: - return PROVISIONING_STATE - if self.provision_state in PROVISION_STATE_PROVISIONED: - return PROVISIONED_STATE - if self.provision_state in PROVISION_STATE_DELETING: - return DELETING_STATE - if self.provision_state in PROVISION_STATE_ERROR: - return PROVISIONING_FAILED_STATE - # Unknown state - return None - - @cached_property - def instance(self): - """Return the Nova Instance associated with this Node - - :return: Nova Instance associated with this Node; or - None if there is no Instance associated with this - Node, or no matching Instance is found - :rtype: Instance - """ - if self._instance is not None: - return self._instance - if self.instance_uuid: - servers, _has_more_data = nova.server_list(self._request) - for server in servers: - if server.id == self.instance_uuid: - return server - - @cached_property - def ip_address(self): - try: - apiresource = self.instace._apiresource - except AttributeError: - LOG.error("Couldn't obtain IP address") - return None - return apiresource.addresses['ctlplane'][0]['addr'] - - @cached_property - def image_name(self): - """Return image name of associated instance - - Returns image name of instance associated with node - - :return: Image name of instance - :rtype: string - """ - if self.instance is None: - return - image = image_get(self._request, self.instance.image['id']) - return image.name - - @cached_property - def instance_status(self): - return getattr(getattr(self, 'instance', None), 'status', None) - - @cached_property - def provisioning_status(self): - if self.instance_uuid: - return _("Provisioned") - return _("Free") - - @classmethod - def get_all_mac_addresses(cls, request): - macs = [node.addresses for node in cls.list(request)] - return set([mac.upper() for sublist in macs for mac in sublist]) diff --git a/tuskar_ui/api/tuskar.py b/tuskar_ui/api/tuskar.py deleted file mode 100644 index 89244591..00000000 --- a/tuskar_ui/api/tuskar.py +++ /dev/null @@ -1,558 +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 random -import string - -from django.conf import settings -from django.utils.translation import ugettext_lazy as _ -from glanceclient import exc as glance_exceptions -from horizon.utils import memoized -from openstack_dashboard.api import base -from openstack_dashboard.api import glance -from openstack_dashboard.api import neutron -from os_cloud_config import keystone_pki -from tuskarclient import client as tuskar_client - -from tuskar_ui.api import flavor -from tuskar_ui.cached_property import cached_property # noqa -from tuskar_ui.handle_errors import handle_errors # noqa - -LOG = logging.getLogger(__name__) -MASTER_TEMPLATE_NAME = 'plan.yaml' -ENVIRONMENT_NAME = 'environment.yaml' -TUSKAR_SERVICE = 'management' - -SSL_HIDDEN_PARAMS = ('SSLCertificate', 'SSLKey') -KEYSTONE_CERTIFICATE_PARAMS = ( - 'KeystoneSigningCertificate', 'KeystoneCACertificate', - 'KeystoneSigningKey') - - -@memoized.memoized -def tuskarclient(request, password=None): - api_version = "2" - insecure = getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False) - ca_file = getattr(settings, 'OPENSTACK_SSL_CACERT', None) - endpoint = base.url_for(request, TUSKAR_SERVICE) - - LOG.debug('tuskarclient connection created using token "%s" and url "%s"' % - (request.user.token.id, endpoint)) - - client = tuskar_client.get_client(api_version, - tuskar_url=endpoint, - insecure=insecure, - ca_file=ca_file, - username=request.user.username, - password=password, - os_auth_token=request.user.token.id) - return client - - -def password_generator(size=40, chars=(string.ascii_uppercase + - string.ascii_lowercase + - string.digits)): - return ''.join(random.choice(chars) for _ in range(size)) - - -def strip_prefix(parameter_name): - return parameter_name.split('::', 1)[-1] - - -def _is_blank(parameter): - return not parameter['value'] or parameter['value'] == 'unset' - - -def _should_generate_password(parameter): - # TODO(lsmola) Filter out SSL params for now. Once it will be generated - # in TripleO add it here too. Note: this will also affect how endpoints are - # created - key = parameter['name'] - return all([ - parameter['hidden'], - _is_blank(parameter), - strip_prefix(key) not in SSL_HIDDEN_PARAMS, - strip_prefix(key) not in KEYSTONE_CERTIFICATE_PARAMS, - key != 'SnmpdReadonlyUserPassword', - ]) - - -def _should_generate_keystone_cert(parameter): - return all([ - strip_prefix(parameter['name']) in KEYSTONE_CERTIFICATE_PARAMS, - _is_blank(parameter), - ]) - - -def _should_generate_neutron_control_plane(parameter): - return all([ - strip_prefix(parameter['name']) == 'NeutronControlPlaneID', - _is_blank(parameter), - ]) - - -class Plan(base.APIResourceWrapper): - _attrs = ('uuid', 'name', 'description', 'created_at', 'modified_at', - 'roles', 'parameters') - - def __init__(self, apiresource, request=None): - super(Plan, self).__init__(apiresource) - self._request = request - - @classmethod - def create(cls, request, name, description): - """Create a Plan in Tuskar - - :param request: request object - :type request: django.http.HttpRequest - - :param name: plan name - :type name: string - - :param description: plan description - :type description: string - - :return: the created Plan object - :rtype: tuskar_ui.api.tuskar.Plan - """ - plan = tuskarclient(request).plans.create(name=name, - description=description) - return cls(plan, request=request) - - @classmethod - def patch(cls, request, plan_id, parameters): - """Update a Plan in Tuskar - - :param request: request object - :type request: django.http.HttpRequest - - :param plan_id: id of the plan we want to update - :type plan_id: string - - :param parameters: new values for the plan's parameters - :type parameters: dict - - :return: the updated Plan object - :rtype: tuskar_ui.api.tuskar.Plan - """ - parameter_list = [{ - 'name': unicode(name), - 'value': unicode(value), - } for (name, value) in parameters.items()] - plan = tuskarclient(request).plans.patch(plan_id, parameter_list) - return cls(plan, request=request) - - @classmethod - @memoized.memoized - def list(cls, request): - """Return a list of Plans in Tuskar - - :param request: request object - :type request: django.http.HttpRequest - - :return: list of Plans, or an empty list if there are none - :rtype: list of tuskar_ui.api.tuskar.Plan - """ - plans = tuskarclient(request).plans.list() - return [cls(plan, request=request) for plan in plans] - - @classmethod - @handle_errors(_("Unable to retrieve plan")) - def get(cls, request, plan_id): - """Return the Plan that matches the ID - - :param request: request object - :type request: django.http.HttpRequest - - :param plan_id: id of Plan to be retrieved - :type plan_id: int - - :return: matching Plan, or None if no Plan matches - the ID - :rtype: tuskar_ui.api.tuskar.Plan - """ - plan = tuskarclient(request).plans.get(plan_uuid=plan_id) - return cls(plan, request=request) - - # TODO(lsmola) before will will support multiple overclouds, we - # can work only with overcloud that is named overcloud. Delete - # this once we have more overclouds. Till then, this is the overcloud - # that rules them all. - # This is how API supports it now, so we have to have it this way. - # Also till Overcloud workflow is done properly, we have to work - # with situations that overcloud is deleted, but stack is still - # there. So overcloud will pretend to exist when stack exist. - @classmethod - def get_the_plan(cls, request): - plan_list = cls.list(request) - for plan in plan_list: - return plan - # if plan doesn't exist, create it - plan = cls.create(request, 'overcloud', 'overcloud') - return plan - - @classmethod - def delete(cls, request, plan_id): - """Delete a Plan - - :param request: request object - :type request: django.http.HttpRequest - - :param plan_id: plan id - :type plan_id: int - """ - tuskarclient(request).plans.delete(plan_uuid=plan_id) - - @cached_property - def role_list(self): - return [Role.get(self._request, role.uuid) - for role in self.roles] - - @cached_property - def _roles_by_name(self): - return dict((role.name, role) for role in self.role_list) - - def get_role_by_name(self, role_name): - """Get the role with the given name.""" - return self._roles_by_name[role_name] - - def get_role_node_count(self, role): - """Get the node count for the given role.""" - return int(self.parameter_value(role.node_count_parameter_name, - 0) or 0) - - @cached_property - def templates(self): - return tuskarclient(self._request).plans.templates(self.uuid) - - @cached_property - def master_template(self): - return self.templates.get(MASTER_TEMPLATE_NAME, '') - - @cached_property - def environment(self): - return self.templates.get(ENVIRONMENT_NAME, '') - - @cached_property - def provider_resource_templates(self): - template_dict = dict(self.templates) - del template_dict[MASTER_TEMPLATE_NAME] - del template_dict[ENVIRONMENT_NAME] - return template_dict - - def parameter_list(self, include_key_parameters=True): - params = self.parameters - if not include_key_parameters: - key_params = [] - for role in self.role_list: - key_params.extend([role.node_count_parameter_name, - role.image_parameter_name, - role.flavor_parameter_name]) - params = [p for p in params if p['name'] not in key_params] - return [Parameter(p, plan=self) for p in params] - - def parameter(self, param_name): - for parameter in self.parameters: - if parameter['name'] == param_name: - return Parameter(parameter, plan=self) - - def parameter_value(self, param_name, default=None): - parameter = self.parameter(param_name) - if parameter is not None: - return parameter.value - return default - - def list_generated_parameters(self, with_prefix=True): - if with_prefix: - key_format = lambda key: key - else: - key_format = strip_prefix - - # Get all password like parameters - return dict( - (key_format(parameter['name']), parameter) - for parameter in self.parameter_list() - if any([ - _should_generate_password(parameter), - _should_generate_keystone_cert(parameter), - _should_generate_neutron_control_plane(parameter), - ]) - ) - - def _make_keystone_certificates(self, wanted_generated_params): - generated_params = {} - for cert_param in KEYSTONE_CERTIFICATE_PARAMS: - if cert_param in wanted_generated_params.keys(): - # If one of the keystone certificates is not set, we have - # to generate all of them. - generate_certificates = True - break - else: - generate_certificates = False - - # Generate keystone certificates - if generate_certificates: - ca_key_pem, ca_cert_pem = keystone_pki.create_ca_pair() - signing_key_pem, signing_cert_pem = ( - keystone_pki.create_signing_pair(ca_key_pem, ca_cert_pem)) - generated_params['KeystoneSigningCertificate'] = ( - signing_cert_pem) - generated_params['KeystoneCACertificate'] = ca_cert_pem - generated_params['KeystoneSigningKey'] = signing_key_pem - return generated_params - - def make_generated_parameters(self): - wanted_generated_params = self.list_generated_parameters( - with_prefix=False) - - # Generate keystone certificates - generated_params = self._make_keystone_certificates( - wanted_generated_params) - - # Generate passwords and control plane id - for (key, param) in wanted_generated_params.items(): - if _should_generate_password(param): - generated_params[key] = password_generator() - elif _should_generate_neutron_control_plane(param): - generated_params[key] = neutron.network_list( - self._request, name='ctlplane')[0].id - - # Fill all the Tuskar parameters with generated content. There are - # parameters that has just different prefix, such parameters should - # have the same values. - wanted_prefixed_params = self.list_generated_parameters( - with_prefix=True) - tuskar_params = {} - - for (key, param) in wanted_prefixed_params.items(): - tuskar_params[key] = generated_params[strip_prefix(key)] - - return tuskar_params - - @property - def id(self): - return self.uuid - - -class Role(base.APIResourceWrapper): - _attrs = ('uuid', 'name', 'version', 'description', 'created') - - def __init__(self, apiresource, request=None): - super(Role, self).__init__(apiresource) - self._request = request - - @classmethod - @memoized.memoized - @handle_errors(_("Unable to retrieve overcloud roles"), []) - def list(cls, request): - """Return a list of Overcloud Roles in Tuskar - - :param request: request object - :type request: django.http.HttpRequest - - :return: list of Overcloud Roles, or an empty list if there - are none - :rtype: list of tuskar_ui.api.tuskar.Role - """ - roles = tuskarclient(request).roles.list() - return [cls(role, request=request) for role in roles] - - @classmethod - @memoized.memoized - @handle_errors(_("Unable to retrieve overcloud role")) - def get(cls, request, role_id): - """Return the Tuskar Role that matches the ID - - :param request: request object - :type request: django.http.HttpRequest - - :param role_id: ID of Role to be retrieved - :type role_id: int - - :return: matching Role, or None if no matching - Role can be found - :rtype: tuskar_ui.api.tuskar.Role - """ - for role in Role.list(request): - if role.uuid == role_id: - return role - - @classmethod - @memoized.memoized - def _roles_by_image(cls, request, plan): - roles_by_image = {} - - for role in Role.list(request): - image = plan.parameter_value(role.image_parameter_name) - if image in roles_by_image: - roles_by_image[image].append(role) - else: - roles_by_image[image] = [role] - - return roles_by_image - - @classmethod - @handle_errors(_("Unable to retrieve overcloud role")) - def get_by_image(cls, request, plan, image): - """Return the Role whose ImageID parameter matches the image. - - :param request: request object - :type request: django.http.HttpRequest - - :param plan: associated plan to check against - :type plan: Plan - - :param image: image to be matched - :type image: Image - - :return: matching Role, or None if no matching - Role can be found - :rtype: tuskar_ui.api.tuskar.Role - """ - roles = cls._roles_by_image(request, plan) - try: - return roles[image.name] - except KeyError: - return [] - - @classmethod - @memoized.memoized - def _roles_by_resource_type(cls, request): - return {role.provider_resource_type: role - for role in Role.list(request)} - - @classmethod - @handle_errors(_("Unable to retrieve overcloud role")) - def get_by_resource_type(cls, request, resource_type): - roles = cls._roles_by_resource_type(request) - try: - return roles[resource_type] - except KeyError: - return None - - @property - def provider_resource_type(self): - return "Tuskar::{0}-{1}".format(self.name, self.version) - - @property - def parameter_prefix(self): - return "{0}-{1}::".format(self.name, self.version) - - @property - def node_count_parameter_name(self): - return self.parameter_prefix + 'count' - - @property - def image_parameter_name(self): - return self.parameter_prefix + 'Image' - - @property - def flavor_parameter_name(self): - return self.parameter_prefix + 'Flavor' - - def image(self, plan): - image_name = plan.parameter_value(self.image_parameter_name) - if image_name: - try: - return glance.image_list_detailed( - self._request, filters={'name': image_name})[0][0] - except (glance_exceptions.HTTPNotFound, IndexError): - LOG.error("Couldn't obtain image with name %s" % image_name) - return None - - def flavor(self, plan): - flavor_name = plan.parameter_value( - self.flavor_parameter_name) - if flavor_name: - return flavor.Flavor.get_by_name(self._request, flavor_name) - - def parameter_list(self, plan): - return [p for p in plan.parameter_list() if self == p.role] - - def is_valid_for_deployment(self, plan): - node_count = plan.get_role_node_count(self) - pending_required_params = list(Parameter.pending_parameters( - Parameter.required_parameters(self.parameter_list(plan)))) - return not ( - self.image(plan) is None or - (node_count and self.flavor(plan) is None) or - pending_required_params - ) - - @property - def id(self): - return self.uuid - - -class Parameter(base.APIDictWrapper): - - _attrs = ['name', 'value', 'default', 'description', 'hidden', 'label', - 'parameter_type', 'constraints'] - - def __init__(self, apidict, plan=None): - super(Parameter, self).__init__(apidict) - self._plan = plan - - @property - def stripped_name(self): - return strip_prefix(self.name) - - @property - def plan(self): - return self._plan - - @property - def role(self): - if self.plan: - for role in self.plan.role_list: - if self.name.startswith(role.parameter_prefix): - return role - - def is_required(self): - """Boolean: True if parameter is required, False otherwise.""" - return self.default is None - - def get_constraint_by_type(self, constraint_type): - """Returns parameter constraint by it's type. - - For available constraint types see HOT Spec: - http://docs.openstack.org/developer/heat/template_guide/hot_spec.html - """ - - constraints_of_type = [c for c in self.constraints - if c['constraint_type'] == constraint_type] - if constraints_of_type: - return constraints_of_type[0] - else: - return None - - @staticmethod - def required_parameters(parameters): - """Yields parameters which are required.""" - for parameter in parameters: - if parameter.is_required(): - yield parameter - - @staticmethod - def pending_parameters(parameters): - """Yields parameters which don't have value set.""" - for parameter in parameters: - if not parameter.value: - yield parameter - - @staticmethod - def global_parameters(parameters): - """Yields parameters with name without role prefix.""" - for parameter in parameters: - if '::' not in parameter.name: - yield parameter |