Example #1
0
    def list_cluster_templates(self, detail=False):
        """List cluster templates.

        :param bool detail. Ignored. Included for backwards compat.
            ClusterTemplates are always returned with full details.

        :returns: a list of dicts containing the cluster template details.

        :raises: ``OpenStackCloudException``: if something goes wrong during
            the OpenStack API call.
        """
        with _utils.shade_exceptions("Error fetching cluster template list"):
            try:
                data = self._container_infra_client.get('/clustertemplates')
                # NOTE(flwang): Magnum adds /clustertemplates and /cluster
                # to deprecate /baymodels and /bay since Newton release. So
                # we're using a small tag to indicate if current
                # cloud has those two new API endpoints.
                self._container_infra_client._has_magnum_after_newton = True
                return self._normalize_cluster_templates(
                    self._get_and_munchify('clustertemplates', data))
            except exc.OpenStackCloudURINotFound:
                data = self._container_infra_client.get('/baymodels/detail')
                return self._normalize_cluster_templates(
                    self._get_and_munchify('baymodels', data))
Example #2
0
    def delete_cluster_template(self, name_or_id):
        """Delete a cluster template.

        :param name_or_id: Name or unique ID of the cluster template.
        :returns: True if the delete succeeded, False if the
            cluster template was not found.

        :raises: OpenStackCloudException on operation error.
        """

        cluster_template = self.get_cluster_template(name_or_id)

        if not cluster_template:
            self.log.debug("Cluster template %(name_or_id)s does not exist",
                           {'name_or_id': name_or_id},
                           exc_info=True)
            return False

        with _utils.shade_exceptions("Error in deleting cluster template"):
            if getattr(self._container_infra_client,
                       '_has_magnum_after_newton', False):
                self._container_infra_client.delete(
                    '/clustertemplates/{id}'.format(id=cluster_template['id']))
            else:
                self._container_infra_client.delete(
                    '/baymodels/{id}'.format(id=cluster_template['id']))
            self.list_cluster_templates.invalidate(self)

        return True
Example #3
0
    def update_coe_cluster(self, name_or_id, operation, **kwargs):
        """Update a COE cluster.

        :param name_or_id: Name or ID of the COE cluster being updated.
        :param operation: Operation to perform - add, remove, replace.

        Other arguments will be passed with kwargs.

        :returns: a dict representing the updated cluster.

        :raises: OpenStackCloudException on operation error.
        """
        self.list_coe_clusters.invalidate(self)
        cluster = self.get_coe_cluster(name_or_id)
        if not cluster:
            raise exc.OpenStackCloudException("COE cluster %s not found." %
                                              name_or_id)

        if operation not in ['add', 'replace', 'remove']:
            raise TypeError("%s operation not in 'add', 'replace', 'remove'" %
                            operation)

        patches = _utils.generate_patches_from_kwargs(operation, **kwargs)
        # No need to fire an API call if there is an empty patch
        if not patches:
            return cluster

        with _utils.shade_exceptions(
                "Error updating COE cluster {0}".format(name_or_id)):
            self._container_infra_client.patch(
                '/clusters/{id}'.format(id=cluster['id']), json=patches)

        new_cluster = self.get_coe_cluster(name_or_id)
        return new_cluster
Example #4
0
    def create_coe_cluster(
            self, name, cluster_template_id, **kwargs):
        """Create a COE cluster based on given cluster template.

        :param string name: Name of the cluster.
        :param string image_id: ID of the cluster template to use.

        Other arguments will be passed in kwargs.

        :returns: a dict containing the cluster description

        :raises: ``OpenStackCloudException`` if something goes wrong during
            the OpenStack API call
        """
        error_message = ("Error creating cluster of name"
                         " {cluster_name}".format(cluster_name=name))
        with _utils.shade_exceptions(error_message):
            body = kwargs.copy()
            body['name'] = name
            body['cluster_template_id'] = cluster_template_id

            cluster = self._container_infra_client.post(
                '/clusters', json=body)

        self.list_coe_clusters.invalidate(self)
        return self._normalize_coe_cluster(cluster)
Example #5
0
 def make_swift_service(self, suppress_warning=False):
     # NOTE(mordred): Not using helper functions because the
     #                error message needs to be different
     if not suppress_warning:
         warnings.warn(
             'Using shade to get a SwiftService object is deprecated. shade'
             ' will automatically do the things SwiftServices does as part'
             ' of the normal object resource calls. If you are having'
             ' trouble using those such that you still need to use'
             ' SwiftService, please file a bug with shade.'
             ' If you understand the issues and want to make this warning'
             ' go away, use cloud.make_swift_service(True) instead of'
             ' cloud.swift_service')
         # Abuse self._legacy_clients so that we only give the warning
         # once. We don't cache SwiftService objects.
         self._legacy_clients['swift-service'] = True
     try:
         import swiftclient.service
     except ImportError:
         self.log.error(
             'swiftclient is no longer a dependency of shade. You need to'
             ' install python-swiftclient directly.')
     with _utils.shade_exceptions("Error constructing SwiftService"):
         endpoint = self.get_session_endpoint(
             service_key='object-store')
         options = dict(os_auth_token=self.auth_token,
                        os_storage_url=endpoint,
                        os_region_name=self.region_name)
         options.update(self._get_swift_kwargs())
         return swiftclient.service.SwiftService(options=options)
Example #6
0
    def delete_coe_cluster(self, name_or_id):
        """Delete a COE cluster.

        :param name_or_id: Name or unique ID of the cluster.
        :returns: True if the delete succeeded, False if the
            cluster was not found.

        :raises: OpenStackCloudException on operation error.
        """

        cluster = self.get_coe_cluster(name_or_id)

        if not cluster:
            self.log.debug(
                "COE Cluster %(name_or_id)s does not exist",
                {'name_or_id': name_or_id},
                exc_info=True)
            return False

        with _utils.shade_exceptions("Error in deleting COE cluster"):
            self._container_infra_client.delete(
                '/clusters/{id}'.format(id=cluster['id']))
            self.list_coe_clusters.invalidate(self)

        return True
Example #7
0
    def list_magnum_services(self):
        """List all Magnum services.
        :returns: a list of dicts containing the service details.

        :raises: OpenStackCloudException on operation error.
        """
        with _utils.shade_exceptions("Error fetching Magnum services list"):
            data = self._container_infra_client.get('/mservices')
            return self._normalize_magnum_services(
                self._get_and_munchify('mservices', data))
Example #8
0
    def list_coe_clusters(self):
        """List COE(Ccontainer Orchestration Engine) cluster.

        :returns: a list of dicts containing the cluster.

        :raises: ``OpenStackCloudException``: if something goes wrong during
            the OpenStack API call.
        """
        with _utils.shade_exceptions("Error fetching cluster list"):
            data = self._container_infra_client.get('/clusters')
        return self._normalize_coe_clusters(
            self._get_and_munchify('clusters', data))
    def delete_volume(self,
                      name_or_id=None,
                      wait=True,
                      timeout=None,
                      force=False):
        """Delete a volume.

        :param name_or_id: Name or unique ID of the volume.
        :param wait: If true, waits for volume to be deleted.
        :param timeout: Seconds to wait for volume deletion. None is forever.
        :param force: Force delete volume even if the volume is in deleting
            or error_deleting state.

        :raises: OpenStackCloudTimeout if wait time exceeded.
        :raises: OpenStackCloudException on operation error.
        """

        self.list_volumes.invalidate(self)
        volume = self.get_volume(name_or_id)

        if not volume:
            self.log.debug("Volume %(name_or_id)s does not exist",
                           {'name_or_id': name_or_id},
                           exc_info=True)
            return False

        with _utils.shade_exceptions("Error in deleting volume"):
            try:
                if force:
                    proxy._json_response(
                        self.block_storage.post(
                            'volumes/{id}/action'.format(id=volume['id']),
                            json={'os-force_delete': None}))
                else:
                    proxy._json_response(
                        self.block_storage.delete(
                            'volumes/{id}'.format(id=volume['id'])))
            except exc.OpenStackCloudURINotFound:
                self.log.debug(
                    "Volume {id} not found when deleting. Ignoring.".format(
                        id=volume['id']))
                return False

        self.list_volumes.invalidate(self)
        if wait:
            for count in utils.iterate_timeout(
                    timeout, "Timeout waiting for the volume to be deleted."):

                if not self.get_volume(volume['id']):
                    break

        return True
Example #10
0
    def delete_volume(self, name_or_id=None, wait=True, timeout=None,
                      force=False):
        """Delete a volume.

        :param name_or_id: Name or unique ID of the volume.
        :param wait: If true, waits for volume to be deleted.
        :param timeout: Seconds to wait for volume deletion. None is forever.
        :param force: Force delete volume even if the volume is in deleting
            or error_deleting state.

        :raises: OpenStackCloudTimeout if wait time exceeded.
        :raises: OpenStackCloudException on operation error.
        """

        self.list_volumes.invalidate(self)
        volume = self.get_volume(name_or_id)

        if not volume:
            self.log.debug(
                "Volume %(name_or_id)s does not exist",
                {'name_or_id': name_or_id},
                exc_info=True)
            return False

        with _utils.shade_exceptions("Error in deleting volume"):
            try:
                if force:
                    self._volume_client.post(
                        'volumes/{id}/action'.format(id=volume['id']),
                        json={'os-force_delete': None})
                else:
                    self._volume_client.delete(
                        'volumes/{id}'.format(id=volume['id']))
            except exc.OpenStackCloudURINotFound:
                self.log.debug(
                    "Volume {id} not found when deleting. Ignoring.".format(
                        id=volume['id']))
                return False

        self.list_volumes.invalidate(self)
        if wait:
            for count in utils.iterate_timeout(
                    timeout,
                    "Timeout waiting for the volume to be deleted."):

                if not self.get_volume(volume['id']):
                    break

        return True
Example #11
0
    def remove_volume_type_access(self, name_or_id, project_id):
        """Revoke access on a volume_type to a project.

        :param name_or_id: ID or name of a volume_type
        :param project_id: A project id

        :raises: OpenStackCloudException on operation error.
        """
        volume_type = self.get_volume_type(name_or_id)
        if not volume_type:
            raise exc.OpenStackCloudException("VolumeType not found: %s" %
                                              name_or_id)
        with _utils.shade_exceptions():
            payload = {'project': project_id}
            self._volume_client.post(
                '/types/{id}/action'.format(id=volume_type.id),
                json=dict(removeProjectAccess=payload),
                error_message="Unable to revoke {project} "
                "to use volume type {name}".format(name=name_or_id,
                                                   project=project_id))
Example #12
0
    def remove_volume_type_access(self, name_or_id, project_id):
        """Revoke access on a volume_type to a project.

        :param name_or_id: ID or name of a volume_type
        :param project_id: A project id

        :raises: OpenStackCloudException on operation error.
        """
        volume_type = self.get_volume_type(name_or_id)
        if not volume_type:
            raise exc.OpenStackCloudException(
                "VolumeType not found: %s" % name_or_id)
        with _utils.shade_exceptions():
            payload = {'project': project_id}
            self._volume_client.post(
                '/types/{id}/action'.format(id=volume_type.id),
                json=dict(removeProjectAccess=payload),
                error_message="Unable to revoke {project} "
                              "to use volume type {name}".format(
                    name=name_or_id, project=project_id))
Example #13
0
    def update_cluster_template(self, name_or_id, operation, **kwargs):
        """Update a cluster template.

        :param name_or_id: Name or ID of the cluster template being updated.
        :param operation: Operation to perform - add, remove, replace.

        Other arguments will be passed with kwargs.

        :returns: a dict representing the updated cluster template.

        :raises: OpenStackCloudException on operation error.
        """
        self.list_cluster_templates.invalidate(self)
        cluster_template = self.get_cluster_template(name_or_id)
        if not cluster_template:
            raise exc.OpenStackCloudException(
                "Cluster template %s not found." % name_or_id)

        if operation not in ['add', 'replace', 'remove']:
            raise TypeError("%s operation not in 'add', 'replace', 'remove'" %
                            operation)

        patches = _utils.generate_patches_from_kwargs(operation, **kwargs)
        # No need to fire an API call if there is an empty patch
        if not patches:
            return cluster_template

        with _utils.shade_exceptions(
                "Error updating cluster template {0}".format(name_or_id)):
            if getattr(self._container_infra_client,
                       '_has_magnum_after_newton', False):
                self._container_infra_client.patch(
                    '/clustertemplates/{id}'.format(id=cluster_template['id']),
                    json=patches)
            else:
                self._container_infra_client.patch(
                    '/baymodels/{id}'.format(id=cluster_template['id']),
                    json=patches)

        new_cluster_template = self.get_cluster_template(name_or_id)
        return new_cluster_template
Example #14
0
    def create_cluster_template(self,
                                name,
                                image_id=None,
                                keypair_id=None,
                                coe=None,
                                **kwargs):
        """Create a cluster template.

        :param string name: Name of the cluster template.
        :param string image_id: Name or ID of the image to use.
        :param string keypair_id: Name or ID of the keypair to use.
        :param string coe: Name of the coe for the cluster template.

        Other arguments will be passed in kwargs.

        :returns: a dict containing the cluster template description

        :raises: ``OpenStackCloudException`` if something goes wrong during
            the OpenStack API call
        """
        error_message = ("Error creating cluster template of name"
                         " {cluster_template_name}".format(
                             cluster_template_name=name))
        with _utils.shade_exceptions(error_message):
            body = kwargs.copy()
            body['name'] = name
            body['image_id'] = image_id
            body['keypair_id'] = keypair_id
            body['coe'] = coe

            try:
                cluster_template = self._container_infra_client.post(
                    '/clustertemplates', json=body)
                self._container_infra_client._has_magnum_after_newton = True
            except exc.OpenStackCloudURINotFound:
                cluster_template = self._container_infra_client.post(
                    '/baymodels', json=body)

        self.list_cluster_templates.invalidate(self)
        return self._normalize_cluster_template(cluster_template)
Example #15
0
    def add_volume_type_access(self, name_or_id, project_id):
        """Grant access on a volume_type to a project.

        :param name_or_id: ID or name of a volume_type
        :param project_id: A project id

        NOTE: the call works even if the project does not exist.

        :raises: OpenStackCloudException on operation error.
        """
        volume_type = self.get_volume_type(name_or_id)
        if not volume_type:
            raise exc.OpenStackCloudException("VolumeType not found: %s" %
                                              name_or_id)
        with _utils.shade_exceptions():
            payload = {'project': project_id}
            self._volume_client.post(
                '/types/{id}/action'.format(id=volume_type.id),
                json=dict(addProjectAccess=payload),
                error_message="Unable to authorize {project} "
                "to use volume type {name}".format(name=name_or_id,
                                                   project=project_id))
Example #16
0
    def add_volume_type_access(self, name_or_id, project_id):
        """Grant access on a volume_type to a project.

        :param name_or_id: ID or name of a volume_type
        :param project_id: A project id

        NOTE: the call works even if the project does not exist.

        :raises: OpenStackCloudException on operation error.
        """
        volume_type = self.get_volume_type(name_or_id)
        if not volume_type:
            raise exc.OpenStackCloudException(
                "VolumeType not found: %s" % name_or_id)
        with _utils.shade_exceptions():
            payload = {'project': project_id}
            self._volume_client.post(
                '/types/{id}/action'.format(id=volume_type.id),
                json=dict(addProjectAccess=payload),
                error_message="Unable to authorize {project} "
                              "to use volume type {name}".format(
                    name=name_or_id, project=project_id))
Example #17
0
    def sign_coe_cluster_certificate(self, cluster_id, csr):
        """Sign client key and generate the CA certificate for a cluster

        :param cluster_id: UUID of the cluster.
        :param csr: Certificate Signing Request (CSR) for authenticating
                    client key.The CSR will be used by Magnum to generate
                    a signed certificate that client will use to communicate
                    with the cluster.

        :returns: a dict representing the signed certs.

        :raises: OpenStackCloudException on operation error.
        """
        error_message = ("Error signing certs for cluster"
                         " {cluster_id}".format(cluster_id=cluster_id))
        with _utils.shade_exceptions(error_message):
            body = {}
            body['cluster_uuid'] = cluster_id
            body['csr'] = csr

            certs = self._container_infra_client.post('/certificates',
                                                      json=body)

        return self._get_and_munchify(key=None, data=certs)
Example #18
0
    def unregister_machine(self, nics, uuid, wait=False, timeout=600):
        """Unregister Baremetal from Ironic

        Removes entries for Network Interfaces and baremetal nodes
        from an Ironic API

        :param nics: An array of strings that consist of MAC addresses
                          to be removed.
        :param string uuid: The UUID of the node to be deleted.

        :param wait: Boolean value, defaults to false, if to block the method
                     upon the final step of unregistering the machine.

        :param timeout: Integer value, representing seconds with a default
                        value of 600, which controls the maximum amount of
                        time to block the method's completion on.

        :raises: OpenStackCloudException on operation failure.
        """

        machine = self.get_machine(uuid)
        invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed']
        if machine['provision_state'] in invalid_states:
            raise exc.OpenStackCloudException(
                "Error unregistering node '%s' due to current provision "
                "state '%s'" % (uuid, machine['provision_state']))

        # NOTE(TheJulia) There is a high possibility of a lock being present
        # if the machine was just moved through the state machine. This was
        # previously concealed by exception retry logic that detected the
        # failure, and resubitted the request in python-ironicclient.
        try:
            self.wait_for_baremetal_node_lock(machine, timeout=timeout)
        except exc.OpenStackCloudException as e:
            raise exc.OpenStackCloudException(
                "Error unregistering node '%s': Exception occured while"
                " waiting to be able to proceed: %s" % (machine['uuid'], e))

        for nic in nics:
            port_msg = ("Error removing NIC {nic} from baremetal API for "
                        "node {uuid}").format(nic=nic, uuid=uuid)
            port_url = '/ports/detail?address={mac}'.format(mac=nic['mac'])
            port = self._baremetal_client.get(port_url, microversion=1.6,
                                              error_message=port_msg)
            port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid'])
            _utils._call_client_and_retry(self._baremetal_client.delete,
                                          port_url, retry_on=[409, 503],
                                          error_message=port_msg)

        with _utils.shade_exceptions(
                "Error unregistering machine {node_id} from the baremetal "
                "API".format(node_id=uuid)):

            # NOTE(TheJulia): While this should not matter microversion wise,
            # ironic assumes all calls without an explicit microversion to be
            # version 1.0. Ironic expects to deprecate support for older
            # microversions in future releases, as such, we explicitly set
            # the version to what we have been using with the client library..
            version = "1.6"
            msg = "Baremetal machine failed to be deleted"
            url = '/nodes/{node_id}'.format(
                node_id=uuid)
            _utils._call_client_and_retry(self._baremetal_client.delete,
                                          url, retry_on=[409, 503],
                                          error_message=msg,
                                          microversion=version)

            if wait:
                for count in utils.iterate_timeout(
                        timeout,
                        "Timeout waiting for machine to be deleted"):
                    if not self.get_machine(uuid):
                        break
Example #19
0
    def register_machine(self,
                         nics,
                         wait=False,
                         timeout=3600,
                         lock_timeout=600,
                         **kwargs):
        """Register Baremetal with Ironic

        Allows for the registration of Baremetal nodes with Ironic
        and population of pertinant node information or configuration
        to be passed to the Ironic API for the node.

        This method also creates ports for a list of MAC addresses passed
        in to be utilized for boot and potentially network configuration.

        If a failure is detected creating the network ports, any ports
        created are deleted, and the node is removed from Ironic.

        :param nics:
           An array of MAC addresses that represent the
           network interfaces for the node to be created.

           Example::

              [
                  {'mac': 'aa:bb:cc:dd:ee:01'},
                  {'mac': 'aa:bb:cc:dd:ee:02'}
              ]

        :param wait: Boolean value, defaulting to false, to wait for the
                     node to reach the available state where the node can be
                     provisioned. It must be noted, when set to false, the
                     method will still wait for locks to clear before sending
                     the next required command.

        :param timeout: Integer value, defautling to 3600 seconds, for the
                        wait state to reach completion.

        :param lock_timeout: Integer value, defaulting to 600 seconds, for
                             locks to clear.

        :param kwargs: Key value pairs to be passed to the Ironic API,
                       including uuid, name, chassis_uuid, driver_info,
                       parameters.

        :raises: OpenStackCloudException on operation error.

        :returns: Returns a ``munch.Munch`` representing the new
                  baremetal node.
        """

        msg = ("Baremetal machine node failed to be created.")
        port_msg = ("Baremetal machine port failed to be created.")

        url = '/nodes'
        # TODO(TheJulia): At some point we need to figure out how to
        # handle data across when the requestor is defining newer items
        # with the older api.
        machine = self._baremetal_client.post(url,
                                              json=kwargs,
                                              error_message=msg,
                                              microversion="1.6")

        created_nics = []
        try:
            for row in nics:
                payload = {'address': row['mac'], 'node_uuid': machine['uuid']}
                nic = self._baremetal_client.post('/ports',
                                                  json=payload,
                                                  error_message=port_msg)
                created_nics.append(nic['uuid'])

        except Exception as e:
            self.log.debug("ironic NIC registration failed", exc_info=True)
            # TODO(mordred) Handle failures here
            try:
                for uuid in created_nics:
                    try:
                        port_url = '/ports/{uuid}'.format(uuid=uuid)
                        # NOTE(TheJulia): Added in hope that it is logged.
                        port_msg = ('Failed to delete port {port} for node '
                                    '{node}').format(port=uuid,
                                                     node=machine['uuid'])
                        self._baremetal_client.delete(port_url,
                                                      error_message=port_msg)
                    except Exception:
                        pass
            finally:
                version = "1.6"
                msg = "Baremetal machine failed to be deleted."
                url = '/nodes/{node_id}'.format(node_id=machine['uuid'])
                self._baremetal_client.delete(url,
                                              error_message=msg,
                                              microversion=version)
            raise exc.OpenStackCloudException(
                "Error registering NICs with the baremetal service: %s" %
                str(e))

        with _utils.shade_exceptions(
                "Error transitioning node to available state"):
            if wait:
                for count in utils.iterate_timeout(
                        timeout, "Timeout waiting for node transition to "
                        "available state"):

                    machine = self.get_machine(machine['uuid'])

                    # Note(TheJulia): Per the Ironic state code, a node
                    # that fails returns to enroll state, which means a failed
                    # node cannot be determined at this point in time.
                    if machine['provision_state'] in ['enroll']:
                        self.node_set_provision_state(machine['uuid'],
                                                      'manage')
                    elif machine['provision_state'] in ['manageable']:
                        self.node_set_provision_state(machine['uuid'],
                                                      'provide')
                    elif machine['last_error'] is not None:
                        raise exc.OpenStackCloudException(
                            "Machine encountered a failure: %s" %
                            machine['last_error'])

                    # Note(TheJulia): Earlier versions of Ironic default to
                    # None and later versions default to available up until
                    # the introduction of enroll state.
                    # Note(TheJulia): The node will transition through
                    # cleaning if it is enabled, and we will wait for
                    # completion.
                    elif machine['provision_state'] in ['available', None]:
                        break

            else:
                if machine['provision_state'] in ['enroll']:
                    self.node_set_provision_state(machine['uuid'], 'manage')
                    # Note(TheJulia): We need to wait for the lock to clear
                    # before we attempt to set the machine into provide state
                    # which allows for the transition to available.
                    for count in utils.iterate_timeout(
                            lock_timeout,
                            "Timeout waiting for reservation to clear "
                            "before setting provide state"):
                        machine = self.get_machine(machine['uuid'])
                        if (machine['reservation'] is None
                                and machine['provision_state'] != 'enroll'):
                            # NOTE(TheJulia): In this case, the node has
                            # has moved on from the previous state and is
                            # likely not being verified, as no lock is
                            # present on the node.
                            self.node_set_provision_state(
                                machine['uuid'], 'provide')
                            machine = self.get_machine(machine['uuid'])
                            break

                        elif machine['provision_state'] in [
                                'cleaning', 'available'
                        ]:
                            break

                        elif machine['last_error'] is not None:
                            raise exc.OpenStackCloudException(
                                "Machine encountered a failure: %s" %
                                machine['last_error'])
        if not isinstance(machine, str):
            return self._normalize_machine(machine)
        else:
            return machine
Example #20
0
    def register_machine(self, nics, wait=False, timeout=3600,
                         lock_timeout=600, **kwargs):
        """Register Baremetal with Ironic

        Allows for the registration of Baremetal nodes with Ironic
        and population of pertinant node information or configuration
        to be passed to the Ironic API for the node.

        This method also creates ports for a list of MAC addresses passed
        in to be utilized for boot and potentially network configuration.

        If a failure is detected creating the network ports, any ports
        created are deleted, and the node is removed from Ironic.

        :param nics:
           An array of MAC addresses that represent the
           network interfaces for the node to be created.

           Example::

              [
                  {'mac': 'aa:bb:cc:dd:ee:01'},
                  {'mac': 'aa:bb:cc:dd:ee:02'}
              ]

        :param wait: Boolean value, defaulting to false, to wait for the
                     node to reach the available state where the node can be
                     provisioned. It must be noted, when set to false, the
                     method will still wait for locks to clear before sending
                     the next required command.

        :param timeout: Integer value, defautling to 3600 seconds, for the
                        wait state to reach completion.

        :param lock_timeout: Integer value, defaulting to 600 seconds, for
                             locks to clear.

        :param kwargs: Key value pairs to be passed to the Ironic API,
                       including uuid, name, chassis_uuid, driver_info,
                       parameters.

        :raises: OpenStackCloudException on operation error.

        :returns: Returns a ``munch.Munch`` representing the new
                  baremetal node.
        """

        msg = ("Baremetal machine node failed to be created.")
        port_msg = ("Baremetal machine port failed to be created.")

        url = '/nodes'
        # TODO(TheJulia): At some point we need to figure out how to
        # handle data across when the requestor is defining newer items
        # with the older api.
        machine = self._baremetal_client.post(url,
                                              json=kwargs,
                                              error_message=msg,
                                              microversion="1.6")

        created_nics = []
        try:
            for row in nics:
                payload = {'address': row['mac'],
                           'node_uuid': machine['uuid']}
                nic = self._baremetal_client.post('/ports',
                                                  json=payload,
                                                  error_message=port_msg)
                created_nics.append(nic['uuid'])

        except Exception as e:
            self.log.debug("ironic NIC registration failed", exc_info=True)
            # TODO(mordred) Handle failures here
            try:
                for uuid in created_nics:
                    try:
                        port_url = '/ports/{uuid}'.format(uuid=uuid)
                        # NOTE(TheJulia): Added in hope that it is logged.
                        port_msg = ('Failed to delete port {port} for node '
                                    '{node}').format(port=uuid,
                                                     node=machine['uuid'])
                        self._baremetal_client.delete(port_url,
                                                      error_message=port_msg)
                    except Exception:
                        pass
            finally:
                version = "1.6"
                msg = "Baremetal machine failed to be deleted."
                url = '/nodes/{node_id}'.format(
                    node_id=machine['uuid'])
                self._baremetal_client.delete(url,
                                              error_message=msg,
                                              microversion=version)
            raise exc.OpenStackCloudException(
                "Error registering NICs with the baremetal service: %s"
                % str(e))

        with _utils.shade_exceptions(
                "Error transitioning node to available state"):
            if wait:
                for count in utils.iterate_timeout(
                        timeout,
                        "Timeout waiting for node transition to "
                        "available state"):

                    machine = self.get_machine(machine['uuid'])

                    # Note(TheJulia): Per the Ironic state code, a node
                    # that fails returns to enroll state, which means a failed
                    # node cannot be determined at this point in time.
                    if machine['provision_state'] in ['enroll']:
                        self.node_set_provision_state(
                            machine['uuid'], 'manage')
                    elif machine['provision_state'] in ['manageable']:
                        self.node_set_provision_state(
                            machine['uuid'], 'provide')
                    elif machine['last_error'] is not None:
                        raise exc.OpenStackCloudException(
                            "Machine encountered a failure: %s"
                            % machine['last_error'])

                    # Note(TheJulia): Earlier versions of Ironic default to
                    # None and later versions default to available up until
                    # the introduction of enroll state.
                    # Note(TheJulia): The node will transition through
                    # cleaning if it is enabled, and we will wait for
                    # completion.
                    elif machine['provision_state'] in ['available', None]:
                        break

            else:
                if machine['provision_state'] in ['enroll']:
                    self.node_set_provision_state(machine['uuid'], 'manage')
                    # Note(TheJulia): We need to wait for the lock to clear
                    # before we attempt to set the machine into provide state
                    # which allows for the transition to available.
                    for count in utils.iterate_timeout(
                            lock_timeout,
                            "Timeout waiting for reservation to clear "
                            "before setting provide state"):
                        machine = self.get_machine(machine['uuid'])
                        if (machine['reservation'] is None
                                and machine['provision_state'] != 'enroll'):
                            # NOTE(TheJulia): In this case, the node has
                            # has moved on from the previous state and is
                            # likely not being verified, as no lock is
                            # present on the node.
                            self.node_set_provision_state(
                                machine['uuid'], 'provide')
                            machine = self.get_machine(machine['uuid'])
                            break

                        elif machine['provision_state'] in [
                                'cleaning',
                                'available']:
                            break

                        elif machine['last_error'] is not None:
                            raise exc.OpenStackCloudException(
                                "Machine encountered a failure: %s"
                                % machine['last_error'])
        if not isinstance(machine, str):
            return self._normalize_machine(machine)
        else:
            return machine
Example #21
0
    def unregister_machine(self, nics, uuid, wait=False, timeout=600):
        """Unregister Baremetal from Ironic

        Removes entries for Network Interfaces and baremetal nodes
        from an Ironic API

        :param nics: An array of strings that consist of MAC addresses
                          to be removed.
        :param string uuid: The UUID of the node to be deleted.

        :param wait: Boolean value, defaults to false, if to block the method
                     upon the final step of unregistering the machine.

        :param timeout: Integer value, representing seconds with a default
                        value of 600, which controls the maximum amount of
                        time to block the method's completion on.

        :raises: OpenStackCloudException on operation failure.
        """

        machine = self.get_machine(uuid)
        invalid_states = ['active', 'cleaning', 'clean wait', 'clean failed']
        if machine['provision_state'] in invalid_states:
            raise exc.OpenStackCloudException(
                "Error unregistering node '%s' due to current provision "
                "state '%s'" % (uuid, machine['provision_state']))

        # NOTE(TheJulia) There is a high possibility of a lock being present
        # if the machine was just moved through the state machine. This was
        # previously concealed by exception retry logic that detected the
        # failure, and resubitted the request in python-ironicclient.
        try:
            self.wait_for_baremetal_node_lock(machine, timeout=timeout)
        except exc.OpenStackCloudException as e:
            raise exc.OpenStackCloudException(
                "Error unregistering node '%s': Exception occured while"
                " waiting to be able to proceed: %s" % (machine['uuid'], e))

        for nic in nics:
            port_msg = ("Error removing NIC {nic} from baremetal API for "
                        "node {uuid}").format(nic=nic, uuid=uuid)
            port_url = '/ports/detail?address={mac}'.format(mac=nic['mac'])
            port = self._baremetal_client.get(port_url,
                                              microversion=1.6,
                                              error_message=port_msg)
            port_url = '/ports/{uuid}'.format(uuid=port['ports'][0]['uuid'])
            _utils._call_client_and_retry(self._baremetal_client.delete,
                                          port_url,
                                          retry_on=[409, 503],
                                          error_message=port_msg)

        with _utils.shade_exceptions(
                "Error unregistering machine {node_id} from the baremetal "
                "API".format(node_id=uuid)):

            # NOTE(TheJulia): While this should not matter microversion wise,
            # ironic assumes all calls without an explicit microversion to be
            # version 1.0. Ironic expects to deprecate support for older
            # microversions in future releases, as such, we explicitly set
            # the version to what we have been using with the client library..
            version = "1.6"
            msg = "Baremetal machine failed to be deleted"
            url = '/nodes/{node_id}'.format(node_id=uuid)
            _utils._call_client_and_retry(self._baremetal_client.delete,
                                          url,
                                          retry_on=[409, 503],
                                          error_message=msg,
                                          microversion=version)

            if wait:
                for count in utils.iterate_timeout(
                        timeout, "Timeout waiting for machine to be deleted"):
                    if not self.get_machine(uuid):
                        break