Exemplo n.º 1
0
 def test_initialized(self):
     sot = schema.Operation('des', schema={'foo': schema.StringParam()})
     self.assertEqual('des', sot['description'])
     self.assertEqual({'foo': {
         'required': False,
         'type': 'String'
     }}, sot['parameters'])
Exemplo n.º 2
0
class DummyProfile(pb.Profile):

    VERSION = '1.0'
    CONTEXT = 'context'

    properties_schema = {
        CONTEXT: schema.Map('context data'),
        'key1': schema.String(
            'first key',
            default='value1',
            updatable=True,
        ),
        'key2': schema.Integer(
            'second key',
            required=True,
            updatable=True,
        ),
        'key3': schema.String('third key', ),
    }
    OPERATIONS = {
        'op1':
        schema.Operation(
            'Operation 1',
            schema={'param1': schema.StringParam('description of param1', )})
    }

    def __init__(self, name, spec, **kwargs):
        super(DummyProfile, self).__init__(name, spec, **kwargs)
Exemplo n.º 3
0
    def test_validate_unrecognizable_param(self):
        sot = schema.Operation('des', schema={'foo': schema.StringParam()})

        ex = self.assertRaises(exc.ESchema, sot.validate,
                               {'baar': 'baar'})

        self.assertEqual("Unrecognizable parameter 'baar'", str(ex))
Exemplo n.º 4
0
    def test_validate_failed_type(self):
        sot = schema.Operation('des', schema={'foo': schema.StringParam()})

        ex = self.assertRaises(exc.ESchema, sot.validate,
                               {'foo': ['baaar']})

        self.assertEqual("value is not a string",
                         str(ex))
Exemplo n.º 5
0
    def test_validate_failed_required(self):
        sot = schema.Operation('des',
                               schema={
                                   'foo': schema.StringParam(),
                                   'bar': schema.StringParam(required=True)
                               })

        ex = self.assertRaises(exc.ESchema, sot.validate, {'foo': 'baaar'})

        self.assertEqual("Required parameter 'bar' not provided",
                         six.text_type(ex))
Exemplo n.º 6
0
    def test_validate_failed_version(self):
        sot = schema.Operation('des',
                               schema={
                                   'foo':
                                   schema.StringParam(min_version='2.0'),
                               })

        ex = self.assertRaises(exc.ESchema, sot.validate, {'foo': 'baaar'},
                               '1.0')

        self.assertEqual(
            "foo (min_version=2.0) is not supported by spec "
            "version 1.0.", six.text_type(ex))
Exemplo n.º 7
0
    def test_validate_failed_constraint(self):
        sot = schema.Operation(
            'des',
            schema={
                'foo':
                schema.StringParam(
                    constraints=[constraints.AllowedValues(['bar'])])
            })

        ex = self.assertRaises(exc.ESchema, sot.validate, {'foo': 'baaar'})

        self.assertEqual("'baaar' must be one of the allowed values: bar",
                         six.text_type(ex))
Exemplo n.º 8
0
class ServerProfile(base.Profile):
    """Profile for an OpenStack Nova server."""

    KEYS = (
        CONTEXT,
        ADMIN_PASS,
        AUTO_DISK_CONFIG,
        AVAILABILITY_ZONE,
        BLOCK_DEVICE_MAPPING_V2,
        CONFIG_DRIVE,
        FLAVOR,
        IMAGE,
        KEY_NAME,
        METADATA,
        NAME,
        NETWORKS,
        PERSONALITY,
        SECURITY_GROUPS,
        USER_DATA,
        SCHEDULER_HINTS,
    ) = (
        'context',
        'admin_pass',
        'auto_disk_config',
        'availability_zone',
        'block_device_mapping_v2',
        'config_drive',
        'flavor',
        'image',
        'key_name',
        'metadata',
        'name',
        'networks',
        'personality',
        'security_groups',
        'user_data',
        'scheduler_hints',
    )

    BDM2_KEYS = (
        BDM2_UUID,
        BDM2_SOURCE_TYPE,
        BDM2_DESTINATION_TYPE,
        BDM2_DISK_BUS,
        BDM2_DEVICE_NAME,
        BDM2_VOLUME_SIZE,
        BDM2_GUEST_FORMAT,
        BDM2_BOOT_INDEX,
        BDM2_DEVICE_TYPE,
        BDM2_DELETE_ON_TERMINATION,
    ) = (
        'uuid',
        'source_type',
        'destination_type',
        'disk_bus',
        'device_name',
        'volume_size',
        'guest_format',
        'boot_index',
        'device_type',
        'delete_on_termination',
    )

    NETWORK_KEYS = (
        PORT,
        FIXED_IP,
        NETWORK,
    ) = (
        'port',
        'fixed_ip',
        'network',
    )

    PERSONALITY_KEYS = (
        PATH,
        CONTENTS,
    ) = (
        'path',
        'contents',
    )

    SCHEDULER_HINTS_KEYS = (GROUP, ) = ('group', )

    properties_schema = {
        CONTEXT:
        schema.Map(_('Customized security context for operating servers.'), ),
        ADMIN_PASS:
        schema.String(_('Password for the administrator account.'), ),
        AUTO_DISK_CONFIG:
        schema.Boolean(
            _('Whether the disk partition is done automatically.'),
            default=True,
        ),
        AVAILABILITY_ZONE:
        schema.String(
            _('Name of availability zone for running the server.'), ),
        BLOCK_DEVICE_MAPPING_V2:
        schema.List(
            _('A list specifying the properties of block devices to be used '
              'for this server.'),
            schema=schema.Map(
                _('A map specifying the properties of a block device to be '
                  'used by the server.'),
                schema={
                    BDM2_UUID:
                    schema.String(
                        _('ID of the source image, snapshot or volume'), ),
                    BDM2_SOURCE_TYPE:
                    schema.String(
                        _('Volume source type, should be image, snapshot, '
                          'volume or blank'),
                        required=True,
                    ),
                    BDM2_DESTINATION_TYPE:
                    schema.String(
                        _('Volume destination type, should be volume or '
                          'local'),
                        required=True,
                    ),
                    BDM2_DISK_BUS:
                    schema.String(_('Bus of the device.'), ),
                    BDM2_DEVICE_NAME:
                    schema.String(
                        _('Name of the device(e.g. vda, xda, ....).'), ),
                    BDM2_VOLUME_SIZE:
                    schema.Integer(
                        _('Size of the block device in MB(for swap) and '
                          'in GB(for other formats)'),
                        required=True,
                    ),
                    BDM2_GUEST_FORMAT:
                    schema.String(
                        _('Specifies the disk file system format(e.g. swap, '
                          'ephemeral, ...).'), ),
                    BDM2_BOOT_INDEX:
                    schema.Integer(_('Define the boot order of the device'), ),
                    BDM2_DEVICE_TYPE:
                    schema.String(
                        _('Type of the device(e.g. disk, cdrom, ...).'), ),
                    BDM2_DELETE_ON_TERMINATION:
                    schema.Boolean(
                        _('Whether to delete the volume when the server '
                          'stops.'), ),
                }),
        ),
        CONFIG_DRIVE:
        schema.Boolean(
            _('Whether config drive should be enabled for the server.'), ),
        FLAVOR:
        schema.String(
            _('ID of flavor used for the server.'),
            required=True,
            updatable=True,
        ),
        IMAGE:
        schema.String(
            # IMAGE is not required, because there could be BDM or BDMv2
            # support and the corresponding settings effective
            _('ID of image to be used for the new server.'),
            updatable=True,
        ),
        KEY_NAME:
        schema.String(_('Name of Nova keypair to be injected to server.'), ),
        METADATA:
        schema.Map(
            _('A collection of key/value pairs to be associated with the '
              'server created. Both key and value should be <=255 chars.'),
            updatable=True,
        ),
        NAME:
        schema.String(
            _('Name of the server. When omitted, the node name will be used.'),
            updatable=True,
        ),
        NETWORKS:
        schema.List(
            _('List of networks for the server.'),
            schema=schema.Map(
                _('A map specifying the properties of a network for uses.'),
                schema={
                    NETWORK:
                    schema.String(
                        _('Name or ID of network to create a port on.'), ),
                    PORT:
                    schema.String(_('Port ID to be used by the network.'), ),
                    FIXED_IP:
                    schema.String(_('Fixed IP to be used by the network.'), ),
                },
            ),
            updatable=True,
        ),
        PERSONALITY:
        schema.List(
            _('List of files to be injected into the server, where each.'),
            schema=schema.Map(
                _('A map specifying the path & contents for an injected '
                  'file.'),
                schema={
                    PATH:
                    schema.String(
                        _('In-instance path for the file to be injected.'),
                        required=True,
                    ),
                    CONTENTS:
                    schema.String(
                        _('Contents of the file to be injected.'),
                        required=True,
                    ),
                },
            ),
        ),
        SCHEDULER_HINTS:
        schema.Map(
            _('A collection of key/value pairs to be associated with the '
              'Scheduler hints. Both key and value should be <=255 chars.'), ),
        SECURITY_GROUPS:
        schema.List(
            _('List of security groups.'),
            schema=schema.String(
                _('Name of a security group'),
                required=True,
            ),
        ),
        USER_DATA:
        schema.String(_('User data to be exposed by the metadata server.'), ),
    }

    OP_NAMES = (
        OP_REBOOT,
        OP_CHANGE_PASSWORD,
    ) = (
        'reboot',
        'change_password',
    )

    REBOOT_TYPE = 'type'
    REBOOT_TYPES = (REBOOT_SOFT, REBOOT_HARD) = ('SOFT', 'HARD')
    ADMIN_PASSWORD = '******'

    OPERATIONS = {
        OP_REBOOT:
        schema.Operation(
            _("Reboot the nova server."),
            schema={
                REBOOT_TYPE:
                schema.StringParam(
                    _("Type of reboot which can be 'SOFT' or 'HARD'."),
                    default=REBOOT_SOFT,
                    constraints=[
                        constraints.AllowedValues(REBOOT_TYPES),
                    ])
            }),
        OP_CHANGE_PASSWORD:
        schema.Operation(_("Change the administrator password."),
                         schema={
                             ADMIN_PASSWORD:
                             schema.StringParam(
                                 _("New password for the administrator."))
                         }),
    }

    def __init__(self, type_name, name, **kwargs):
        super(ServerProfile, self).__init__(type_name, name, **kwargs)
        self.server_id = None

    def _validate_az(self, obj, az_name, reason=None):
        try:
            res = self.compute(obj).validate_azs([az_name])
        except exc.InternalError as ex:
            if reason == 'create':
                raise exc.EResourceCreation(type='server',
                                            message=six.text_type(ex))
            else:
                raise

        if not res:
            msg = _("The specified %(key)s '%(value)s' could not be found") % {
                'key': self.AVAILABILITY_ZONE,
                'value': az_name
            }
            if reason == 'create':
                raise exc.EResourceCreation(type='server', message=msg)
            else:
                raise exc.InvalidSpec(message=msg)

        return az_name

    def _validate_flavor(self, obj, name_or_id, reason=None):
        flavor = None
        msg = ''
        try:
            flavor = self.compute(obj).flavor_find(name_or_id, False)
        except exc.InternalError as ex:
            msg = six.text_type(ex)
            if reason is None:  # reaons is 'validate'
                if ex.code == 404:
                    msg = _(
                        "The specified %(k)s '%(v)s' could not be found.") % {
                            'k': self.FLAVOR,
                            'v': name_or_id
                        }
                    raise exc.InvalidSpec(message=msg)
                else:
                    raise

        if flavor is not None:
            if not flavor.is_disabled:
                return flavor
            msg = _("The specified %(k)s '%(v)s' is disabled") % {
                'k': self.FLAVOR,
                'v': name_or_id
            }

        if reason == 'create':
            raise exc.EResourceCreation(type='server', message=msg)
        elif reason == 'update':
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=msg)
        else:
            raise exc.InvalidSpec(message=msg)

    def _validate_image(self, obj, name_or_id, reason=None):
        try:
            return self.compute(obj).image_find(name_or_id, False)
        except exc.InternalError as ex:
            if reason == 'create':
                raise exc.EResourceCreation(type='server',
                                            message=six.text_type(ex))
            elif reason == 'update':
                raise exc.EResourceUpdate(type='server',
                                          id=obj.physical_id,
                                          message=six.text_type(ex))
            elif ex.code == 404:
                msg = _("The specified %(k)s '%(v)s' could not be found.") % {
                    'k': self.IMAGE,
                    'v': name_or_id
                }
                raise exc.InvalidSpec(message=msg)
            else:
                raise

    def _validate_keypair(self, obj, name_or_id, reason=None):
        try:
            return self.compute(obj).keypair_find(name_or_id, False)
        except exc.InternalError as ex:
            if reason == 'create':
                raise exc.EResourceCreation(type='server',
                                            message=six.text_type(ex))
            elif reason == 'update':
                raise exc.EResourceUpdate(type='server',
                                          id=obj.physical_id,
                                          message=six.text_type(ex))
            elif ex.code == 404:
                msg = _("The specified %(k)s '%(v)s' could not be found.") % {
                    'k': self.KEY_NAME,
                    'v': name_or_id
                }
                raise exc.InvalidSpec(message=msg)
            else:
                raise

    def do_validate(self, obj):
        """Validate if the spec has provided valid info for server creation.

        :param obj: The node object.
        """
        # validate availability_zone
        az_name = self.properties[self.AVAILABILITY_ZONE]
        if az_name is not None:
            self._validate_az(obj, az_name)

        # validate flavor
        flavor = self.properties[self.FLAVOR]
        self._validate_flavor(obj, flavor)

        # validate image
        image = self.properties[self.IMAGE]
        if image is not None:
            self._validate_image(obj, image)

        # validate key_name
        keypair = self.properties[self.KEY_NAME]
        if keypair is not None:
            self._validate_keypair(obj, keypair)

        # validate networks
        networks = self.properties[self.NETWORKS]
        for net in networks:
            self._validate_network(obj, net)

        return True

    def _resolve_bdm(self, bdm):
        for bd in bdm:
            for key in self.BDM2_KEYS:
                if bd[key] is None:
                    del bd[key]
        return bdm

    def _validate_network(self, obj, network, reason=None):
        result = {}
        error = None
        # check network
        net_ident = network.get(self.NETWORK)
        if net_ident:
            try:
                net = self.network(obj).network_get(net_ident)
                if reason == 'update':
                    result['net_id'] = net.id
                else:
                    result['uuid'] = net.id
            except exc.InternalError as ex:
                error = six.text_type(ex)

        # check port
        port_ident = network.get(self.PORT)
        if not error and port_ident:
            try:
                port = self.network(obj).port_find(port_ident)
                if port.status != 'DOWN':
                    error = _(
                        "The status of the port %(port)s must be DOWN") % {
                            'port': port_ident
                        }

                if reason == 'update':
                    result['port_id'] = port.id
                else:
                    result['port'] = port.id
            except exc.InternalError as ex:
                error = six.text_type(ex)
        elif port_ident is None and net_ident is None:
            error = _("'%(port)s' is required if '%(net)s' is omitted") % {
                'port': self.PORT,
                'net': self.NETWORK
            }

        fixed_ip = network.get(self.FIXED_IP)
        if not error and fixed_ip:
            if port_ident is not None:
                error = _("The '%(port)s' property and the '%(fixed_ip)s' "
                          "property cannot be specified at the same time") % {
                              'port': self.PORT,
                              'fixed_ip': self.FIXED_IP
                          }
            else:
                if reason == 'update':
                    result['fixed_ips'] = [{'ip_address': fixed_ip}]
                else:
                    result['fixed_ip'] = fixed_ip

        if error:
            if reason == 'create':
                raise exc.EResourceCreation(type='server', message=error)
            elif reason == 'update':
                raise exc.EResourceUpdate(type='server',
                                          id=obj.physical_id,
                                          message=error)
            else:
                raise exc.InvalidSpec(message=error)

        return result

    def _build_metadata(self, obj, usermeta):
        """Build custom metadata for server.

        :param obj: The node object to operate on.
        :return: A dictionary containing the new metadata.
        """
        metadata = usermeta or {}
        metadata['cluster_node_id'] = obj.id
        if obj.cluster_id:
            metadata['cluster_id'] = obj.cluster_id
            metadata['cluster_node_index'] = six.text_type(obj.index)

        return metadata

    def do_create(self, obj):
        """Create a server for the node object.

        :param obj: The node object for which a server will be created.
        """
        kwargs = {}
        for key in self.KEYS:
            # context is treated as connection parameters
            if key == self.CONTEXT:
                continue

            if self.properties[key] is not None:
                kwargs[key] = self.properties[key]

        admin_pass = self.properties[self.ADMIN_PASS]
        if admin_pass:
            kwargs.pop(self.ADMIN_PASS)
            kwargs['adminPass'] = admin_pass

        auto_disk_config = self.properties[self.AUTO_DISK_CONFIG]
        kwargs.pop(self.AUTO_DISK_CONFIG)
        kwargs['OS-DCF:diskConfig'] = 'AUTO' if auto_disk_config else 'MANUAL'

        image_ident = self.properties[self.IMAGE]
        if image_ident is not None:
            image = self._validate_image(obj, image_ident, 'create')
            kwargs.pop(self.IMAGE)
            kwargs['imageRef'] = image.id

        flavor_ident = self.properties[self.FLAVOR]
        flavor = self._validate_flavor(obj, flavor_ident, 'create')
        kwargs.pop(self.FLAVOR)
        kwargs['flavorRef'] = flavor.id

        keypair_name = self.properties[self.KEY_NAME]
        if keypair_name:
            keypair = self._validate_keypair(obj, keypair_name, 'create')
            kwargs['key_name'] = keypair.name

        kwargs['name'] = self.properties[self.NAME] or obj.name

        metadata = self._build_metadata(obj, self.properties[self.METADATA])
        kwargs['metadata'] = metadata

        block_device_mapping_v2 = self.properties[self.BLOCK_DEVICE_MAPPING_V2]
        if block_device_mapping_v2 is not None:
            kwargs['block_device_mapping_v2'] = self._resolve_bdm(
                block_device_mapping_v2)

        user_data = self.properties[self.USER_DATA]
        if user_data is not None:
            ud = encodeutils.safe_encode(user_data)
            kwargs['user_data'] = encodeutils.safe_decode(base64.b64encode(ud))

        networks = self.properties[self.NETWORKS]
        if networks is not None:
            kwargs['networks'] = []
            for net_spec in networks:
                net = self._validate_network(obj, net_spec, 'create')
                kwargs['networks'].append(net)

        secgroups = self.properties[self.SECURITY_GROUPS]
        if secgroups:
            kwargs['security_groups'] = [{'name': sg} for sg in secgroups]

        if 'placement' in obj.data:
            if 'zone' in obj.data['placement']:
                kwargs['availability_zone'] = obj.data['placement']['zone']

            if 'servergroup' in obj.data['placement']:
                group_id = obj.data['placement']['servergroup']
                hints = self.properties.get(self.SCHEDULER_HINTS, {})
                hints.update({'group': group_id})
                kwargs['scheduler_hints'] = hints

        server = None
        resource_id = 'UNKNOWN'
        try:
            server = self.compute(obj).server_create(**kwargs)
            self.compute(obj).wait_for_server(server.id)
            return server.id
        except exc.InternalError as ex:
            if server and server.id:
                resource_id = server.id
            raise exc.EResourceCreation(type='server',
                                        message=ex.message,
                                        resource_id=resource_id)

    def do_delete(self, obj, **params):
        """Delete the physical resource associated with the specified node.

        :param obj: The node object to operate on.
        :param kwargs params: Optional keyword arguments for the delete
                              operation.
        :returns: This operation always return True unless exception is
                  caught.
        :raises: `EResourceDeletion` if interaction with compute service fails.
        """
        if not obj.physical_id:
            return True

        server_id = obj.physical_id
        ignore_missing = params.get('ignore_missing', True)
        force = params.get('force', False)

        try:
            driver = self.compute(obj)
            if force:
                driver.server_force_delete(server_id, ignore_missing)
            else:
                driver.server_delete(server_id, ignore_missing)
            driver.wait_for_server_delete(server_id)
            return True
        except exc.InternalError as ex:
            raise exc.EResourceDeletion(type='server',
                                        id=server_id,
                                        message=six.text_type(ex))

    def _check_server_name(self, obj, profile):
        """Check if there is a new name to be assigned to the server.

        :param obj: The node object to operate on.
        :param new_profile: The new profile which may contain a name for
                            the server instance.
        :return: A tuple consisting a boolean indicating whether the name
                 needs change and the server name determined.
        """
        old_name = self.properties[self.NAME] or obj.name
        new_name = profile.properties[self.NAME] or obj.name
        if old_name == new_name:
            return False, new_name
        return True, new_name

    def _update_name(self, obj, new_name):
        """Update the name of the server.

        :param obj: The node object to operate.
        :param new_name: The new name for the server instance.
        :return: ``None``.
        :raises: ``EResourceUpdate``.
        """
        try:
            self.compute(obj).server_update(obj.physical_id, name=new_name)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=six.text_type(ex))

    def _check_password(self, obj, new_profile):
        """Check if the admin password has been changed in the new profile.

        :param obj: The server node to operate, not used currently.
        :param new_profile: The new profile which may contain a new password
                            for the server instance.
        :return: A tuple consisting a boolean indicating whether the password
                 needs a change and the password determined which could be
                 '' if new password is not set.
        """
        old_passwd = self.properties.get(self.ADMIN_PASS) or ''
        new_passwd = new_profile.properties[self.ADMIN_PASS] or ''
        if old_passwd == new_passwd:
            return False, new_passwd
        return True, new_passwd

    def _update_password(self, obj, new_password):
        """Update the admin password for the server.

        :param obj: The node object to operate.
        :param new_password: The new password for the server instance.
        :return: ``None``.
        :raises: ``EResourceUpdate``.
        """
        try:
            self.compute(obj).server_change_password(obj.physical_id,
                                                     new_password)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=six.text_type(ex))

    def _update_metadata(self, obj, new_profile):
        """Update the server metadata.

        :param obj: The node object to operate on.
        :param new_profile: The new profile that may contain some changes to
                            the metadata.
        :returns: ``None``
        :raises: `EResourceUpdate`.
        """
        old_meta = self._build_metadata(obj, self.properties[self.METADATA])
        new_meta = self._build_metadata(obj,
                                        new_profile.properties[self.METADATA])
        if new_meta == old_meta:
            return

        try:
            self.compute(obj).server_metadata_update(obj.physical_id, new_meta)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=six.text_type(ex))

    def _update_flavor(self, obj, new_profile):
        """Update server flavor.

        :param obj: The node object to operate on.
        :param old_flavor: The identity of the current flavor.
        :param new_flavor: The identity of the new flavor.
        :returns: ``None``.
        :raises: `EResourceUpdate` when operation was a failure.
        """
        old_flavor = self.properties[self.FLAVOR]
        new_flavor = new_profile.properties[self.FLAVOR]
        cc = self.compute(obj)
        oldflavor = self._validate_flavor(obj, old_flavor, 'update')
        newflavor = self._validate_flavor(obj, new_flavor, 'update')
        if oldflavor.id == newflavor.id:
            return

        try:
            cc.server_resize(obj.physical_id, newflavor.id)
            cc.wait_for_server(obj.physical_id, 'VERIFY_RESIZE')
        except exc.InternalError as ex:
            msg = six.text_type(ex)
            try:
                cc.server_resize_revert(obj.physical_id)
                cc.wait_for_server(obj.physical_id, 'ACTIVE')
            except exc.InternalError as ex1:
                msg = six.text_type(ex1)
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=msg)

        try:
            cc.server_resize_confirm(obj.physical_id)
            cc.wait_for_server(obj.physical_id, 'ACTIVE')
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=six.text_type(ex))

    def _update_image(self, obj, new_profile, new_name, new_password):
        """Update image used by server node.

        :param obj: The node object to operate on.
        :param new_profile: The profile which may contain a new image name or
                            ID to use.
        :param new_name: The name for the server node.
        :param newn_password: The new password for the administrative account
                              if provided.
        :returns: A boolean indicating whether the image needs an update.
        :raises: ``InternalError`` if operation was a failure.
        """
        old_image = self.properties[self.IMAGE]
        new_image = new_profile.properties[self.IMAGE]
        if not new_image:
            msg = _("Updating Nova server with image set to None is not "
                    "supported by Nova")
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=msg)
        # check the new image first
        img_new = self._validate_image(obj, new_image, reason='update')
        new_image_id = img_new.id

        driver = self.compute(obj)
        if old_image:
            img_old = self._validate_image(obj, old_image, reason='update')
            old_image_id = img_old.id
        else:
            try:
                server = driver.server_get(obj.physical_id)
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server',
                                          id=obj.physical_id,
                                          message=six.text_type(ex))
            # Still, this 'old_image_id' could be empty, but it doesn't matter
            # because the comparison below would fail if that is the case
            old_image_id = server.image.get('id', None)

        if new_image_id == old_image_id:
            return False

        try:
            driver.server_rebuild(obj.physical_id, new_image_id, new_name,
                                  new_password)
            driver.wait_for_server(obj.physical_id, 'ACTIVE')
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=six.text_type(ex))
        return True

    def _create_interfaces(self, obj, networks):
        """Create new interfaces for the server node.

        :param obj: The node object to operate.
        :param networks: A list containing information about new network
                         interfaces to be created.
        :returns: ``None``.
        :raises: ``EResourceUpdate`` if interaction with drivers failed.
        """
        cc = self.compute(obj)
        try:
            server = cc.server_get(obj.physical_id)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=six.text_type(ex))

        for net_spec in networks:
            net_attrs = self._validate_network(obj, net_spec, 'update')
            if net_attrs:
                try:
                    cc.server_interface_create(server, **net_attrs)
                except exc.InternalError as ex:
                    raise exc.EResourceUpdate(type='server',
                                              id=obj.physical_id,
                                              message=six.text_type(ex))

    def _delete_interfaces(self, obj, networks):
        """Delete existing interfaces from the node.

        :param obj: The node object to operate.
        :param networks: A list containing information about network
                         interfaces to be created.
        :returns: ``None``
        :raises: ``EResourceUpdate``
        """
        def _get_network(nc, net_id, server_id):
            try:
                net = nc.network_get(net_id)
                return net.id
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server',
                                          id=server_id,
                                          message=six.text_type(ex))

        def _do_delete(port_id, server_id):
            try:
                cc.server_interface_delete(port_id, server_id)
            except exc.InternalError as ex:
                raise exc.EResourceUpdate(type='server',
                                          id=server_id,
                                          message=six.text_type(ex))

        cc = self.compute(obj)
        nc = self.network(obj)
        try:
            existing = list(cc.server_interface_list(obj.physical_id))
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='server',
                                      id=obj.physical_id,
                                      message=six.text_type(ex))

        ports = []
        for intf in existing:
            fixed_ips = [addr['ip_address'] for addr in intf.fixed_ips]
            ports.append({
                'id': intf.port_id,
                'net': intf.net_id,
                'ips': fixed_ips
            })

        for n in networks:
            network = n.get('network', None)
            port = n.get('port', None)
            fixed_ip = n.get('fixed_ip', None)
            if port:
                for p in ports:
                    if p['id'] == port:
                        ports.remove(p)
                        _do_delete(port, obj.physical_id)
            elif fixed_ip:
                net_id = _get_network(nc, network, obj.physical_id)
                for p in ports:
                    if (fixed_ip in p['ips'] and net_id == p['net']):
                        ports.remove(p)
                        _do_delete(p['id'], obj.physical_id)
            elif port is None and fixed_ip is None:
                net_id = _get_network(nc, network, obj.physical_id)
                for p in ports:
                    if p['net'] == net_id:
                        ports.remove(p)
                        _do_delete(p['id'], obj.physical_id)

    def _update_network(self, obj, new_profile):
        """Updating server network interfaces.

        :param obj: The node object to operate.
        :param new_profile: The new profile which may contain new network
                            settings.
        :return: ``None``
        :raises: ``EResourceUpdate`` if there are driver failures.
        """
        networks_current = self.properties[self.NETWORKS]
        networks_create = new_profile.properties[self.NETWORKS]
        networks_delete = copy.deepcopy(networks_current)
        for network in networks_current:
            if network in networks_create:
                networks_create.remove(network)
                networks_delete.remove(network)

        # Detach some existing interfaces
        if networks_delete:
            self._delete_interfaces(obj, networks_delete)

        # Attach new interfaces
        if networks_create:
            self._create_interfaces(obj, networks_create)
        return

    def do_update(self, obj, new_profile=None, **params):
        """Perform update on the server.

        :param obj: the server to operate on
        :param new_profile: the new profile for the server.
        :param params: a dictionary of optional parameters.
        :returns: True if update was successful or False otherwise.
        :raises: `EResourceUpdate` if operation fails.
        """
        self.server_id = obj.physical_id
        if not self.server_id:
            return False

        if not new_profile:
            return False

        if not self.validate_for_update(new_profile):
            return False

        name_changed, new_name = self._check_server_name(obj, new_profile)
        passwd_changed, new_passwd = self._check_password(obj, new_profile)
        # Update server image: may have side effect of changing server name
        # and/or admin password
        image_changed = self._update_image(obj, new_profile, new_name,
                                           new_passwd)
        if not image_changed:
            # we do this separately only when rebuild wasn't performed
            if name_changed:
                self._update_name(obj, new_name)
            if passwd_changed:
                self._update_password(obj, new_passwd)

        # Update server flavor: note that flavor is a required property
        self._update_flavor(obj, new_profile)
        self._update_network(obj, new_profile)

        # TODO(Yanyan Hu): Update block_device properties
        # Update server metadata
        self._update_metadata(obj, new_profile)

        return True

    def do_get_details(self, obj):
        known_keys = {
            'OS-DCF:diskConfig', 'OS-EXT-AZ:availability_zone',
            'OS-EXT-STS:power_state', 'OS-EXT-STS:vm_state', 'accessIPv4',
            'accessIPv6', 'config_drive', 'created', 'hostId', 'id',
            'key_name', 'locked', 'metadata', 'name',
            'os-extended-volumes:volumes_attached', 'progress', 'status',
            'updated'
        }
        if obj.physical_id is None or obj.physical_id == '':
            return {}

        driver = self.compute(obj)
        try:
            server = driver.server_get(obj.physical_id)
        except exc.InternalError as ex:
            return {'Error': {'code': ex.code, 'message': six.text_type(ex)}}

        if server is None:
            return {}
        server_data = server.to_dict()
        details = {
            'image': server_data['image']['id'],
            'flavor': server_data['flavor']['id'],
        }
        for key in known_keys:
            if key in server_data:
                details[key] = server_data[key]

        # process special keys like 'OS-EXT-STS:task_state': these keys have
        # a default value '-' when not existing
        special_keys = [
            'OS-EXT-STS:task_state',
            'OS-SRV-USG:launched_at',
            'OS-SRV-USG:terminated_at',
        ]
        for key in special_keys:
            if key in server_data:
                val = server_data[key]
                details[key] = val if val else '-'

        # process network addresses
        details['addresses'] = copy.deepcopy(server_data['addresses'])

        # process security groups
        sgroups = []
        if 'security_groups' in server_data:
            for sg in server_data['security_groups']:
                sgroups.append(sg['name'])
        if len(sgroups) == 0:
            details['security_groups'] = ''
        elif len(sgroups) == 1:
            details['security_groups'] = sgroups[0]
        else:
            details['security_groups'] = sgroups

        return dict((k, details[k]) for k in sorted(details))

    def do_join(self, obj, cluster_id):
        if not obj.physical_id:
            return False

        driver = self.compute(obj)
        metadata = driver.server_metadata_get(obj.physical_id) or {}
        metadata['cluster_id'] = cluster_id
        metadata['cluster_node_index'] = six.text_type(obj.index)
        driver.server_metadata_update(obj.physical_id, metadata)
        return super(ServerProfile, self).do_join(obj, cluster_id)

    def do_leave(self, obj):
        if not obj.physical_id:
            return False

        keys = ['cluster_id', 'cluster_node_index']
        self.compute(obj).server_metadata_delete(obj.physical_id, keys)
        return super(ServerProfile, self).do_leave(obj)

    def do_rebuild(self, obj):
        if not obj.physical_id:
            return False

        self.server_id = obj.physical_id
        driver = self.compute(obj)
        try:
            server = driver.server_get(self.server_id)
        except exc.InternalError as ex:
            raise exc.EResourceOperation(op='rebuilding',
                                         type='server',
                                         id=self.server_id,
                                         message=six.text_type(ex))

        if server is None or server.image is None:
            return False

        image_id = server.image['id']
        admin_pass = self.properties.get(self.ADMIN_PASS)
        try:
            driver.server_rebuild(self.server_id, image_id,
                                  self.properties.get(self.NAME), admin_pass)
            driver.wait_for_server(self.server_id, 'ACTIVE')
        except exc.InternalError as ex:
            raise exc.EResourceOperation(op='rebuilding',
                                         type='server',
                                         id=self.server_id,
                                         message=six.text_type(ex))
        return True

    def do_check(self, obj):
        if not obj.physical_id:
            return False

        try:
            server = self.compute(obj).server_get(obj.physical_id)
        except exc.InternalError as ex:
            raise exc.EResourceOperation(op='checking',
                                         type='server',
                                         id=obj.physical_id,
                                         message=six.text_type(ex))

        if (server is None or server.status != 'ACTIVE'):
            return False

        return True

    def do_recover(self, obj, **options):
        # NOTE: We do a 'get' not a 'pop' here, because the operations may
        #       get fall back to the base class for handling
        operation = options.get('operation', None)

        if operation and not isinstance(operation, six.string_types):
            operation = operation[0]
        # TODO(Qiming): Handle the case that the operation contains other
        #               alternative recover operation
        # Depends-On: https://review.openstack.org/#/c/359676/
        if operation == 'REBUILD':
            return self.do_rebuild(obj)

        return super(ServerProfile, self).do_recover(obj, **options)

    def handle_reboot(self, obj, **options):
        """Handler for the reboot operation."""
        if not obj.physical_id:
            return False

        reboot_type = options.get(self.REBOOT_TYPE, self.REBOOT_SOFT)
        if (not isinstance(reboot_type, six.string_types)
                or reboot_type not in self.REBOOT_TYPES):
            return False

        self.compute(obj).server_reboot(obj.physical_id, reboot_type)
        self.compute(obj).wait_for_server(obj.physical_id, 'ACTIVE')
        return True

    def handle_change_password(self, obj, **options):
        """Handler for the change_password operation."""
        if not obj.physical_id:
            return False

        password = options.get(self.ADMIN_PASSWORD, None)
        if (password is None or not isinstance(password, six.string_types)):
            return False

        self.compute(obj).server_change_password(obj.physical_id, password)
        return True
Exemplo n.º 9
0
 def test_validate(self):
     sot = schema.Operation('des', schema={'foo': schema.StringParam()})
     res = sot.validate({'foo': 'bar'})
     self.assertIsNone(res)
Exemplo n.º 10
0
 def test_basic(self):
     sot = schema.Operation()
     self.assertEqual('Undocumented', sot['description'])
     self.assertEqual({}, sot['parameters'])
Exemplo n.º 11
0
class DockerProfile(base.Profile):
    """Profile for a docker container."""
    VERSIONS = {'1.0': [{'status': consts.EXPERIMENTAL, 'since': '2017.02'}]}

    _VALID_HOST_TYPES = [
        HOST_NOVA_SERVER,
        HOST_HEAT_STACK,
    ] = [
        "os.nova.server",
        "os.heat.stack",
    ]

    KEYS = (
        CONTEXT,
        IMAGE,
        NAME,
        COMMAND,
        HOST_NODE,
        HOST_CLUSTER,
        PORT,
    ) = (
        'context',
        'image',
        'name',
        'command',
        'host_node',
        'host_cluster',
        'port',
    )

    properties_schema = {
        CONTEXT:
        schema.Map(_('Customized security context for operating containers.')),
        IMAGE:
        schema.String(
            _('The image used to create a container'),
            required=True,
        ),
        NAME:
        schema.String(
            _('The name of the container.'),
            updatable=True,
        ),
        COMMAND:
        schema.String(_('The command to run when container is started.')),
        PORT:
        schema.Integer(_('The port number used to connect to docker daemon.'),
                       default=2375),
        HOST_NODE:
        schema.String(_('The node on which container will be launched.')),
        HOST_CLUSTER:
        schema.String(_('The cluster on which container will be launched.')),
    }

    OP_NAMES = (
        OP_RESTART,
        OP_PAUSE,
        OP_UNPAUSE,
    ) = (
        'restart',
        'pause',
        'unpause',
    )

    _RESTART_WAIT = (RESTART_WAIT) = ('wait_time')

    OPERATIONS = {
        OP_RESTART:
        schema.Operation(
            _("Restart a container."),
            schema={
                RESTART_WAIT:
                schema.IntegerParam(
                    _("Number of seconds to wait before killing the "
                      "container."))
            }),
        OP_PAUSE:
        schema.Operation(_("Pause a container.")),
        OP_UNPAUSE:
        schema.Operation(_("Unpause a container."))
    }

    def __init__(self, type_name, name, **kwargs):
        super(DockerProfile, self).__init__(type_name, name, **kwargs)

        self._dockerclient = None
        self.container_id = None
        self.host = None
        self.cluster = None

    @classmethod
    def create(cls, ctx, name, spec, metadata=None):
        profile = super(DockerProfile, cls).create(ctx, name, spec, metadata)

        host_cluster = profile.properties.get(profile.HOST_CLUSTER, None)
        if host_cluster:
            db_api.cluster_add_dependents(ctx, host_cluster, profile.id)

        host_node = profile.properties.get(profile.HOST_NODE, None)
        if host_node:
            db_api.node_add_dependents(ctx, host_node, profile.id, 'profile')

        return profile

    @classmethod
    def delete(cls, ctx, profile_id):
        obj = cls.load(ctx, profile_id=profile_id)
        cluster_id = obj.properties.get(obj.HOST_CLUSTER, None)
        if cluster_id:
            db_api.cluster_remove_dependents(ctx, cluster_id, profile_id)

        node_id = obj.properties.get(obj.HOST_NODE, None)
        if node_id:
            db_api.node_remove_dependents(ctx, node_id, profile_id, 'profile')

        super(DockerProfile, cls).delete(ctx, profile_id)

    def docker(self, obj):
        """Construct docker client based on object.

        :param obj: Object for which the client is created. It is expected to
                    be None when retrieving an existing client. When creating
                    a client, it contains the user and project to be used.
        """
        if self._dockerclient is not None:
            return self._dockerclient

        host_node = self.properties.get(self.HOST_NODE, None)
        host_cluster = self.properties.get(self.HOST_CLUSTER, None)
        ctx = context.get_admin_context()
        self.host = self._get_host(ctx, host_node, host_cluster)

        # TODO(Anyone): Check node.data for per-node host selection
        host_type = self.host.rt['profile'].type_name
        if host_type not in self._VALID_HOST_TYPES:
            msg = _("Type of host node (%s) is not supported") % host_type
            raise exc.InternalError(message=msg)

        host_ip = self._get_host_ip(obj, self.host.physical_id, host_type)
        if host_ip is None:
            msg = _("Unable to determine the IP address of host node")
            raise exc.InternalError(message=msg)

        url = 'tcp://%(ip)s:%(port)d' % {
            'ip': host_ip,
            'port': self.properties[self.PORT]
        }
        self._dockerclient = docker_driver.DockerClient(url)
        return self._dockerclient

    def _get_host(self, ctx, host_node, host_cluster):
        """Determine which node to launch container on.

        :param ctx: An instance of the request context.
        :param host_node: The uuid of the hosting node.
        :param host_cluster: The uuid of the hosting cluster.
        """
        host = None
        if host_node is not None:
            try:
                host = node_mod.Node.load(ctx, node_id=host_node)
            except exc.ResourceNotFound as ex:
                msg = ex.enhance_msg('host', ex)
                raise exc.InternalError(message=msg)
            return host

        if host_cluster is not None:
            host = self._get_random_node(ctx, host_cluster)

        return host

    def _get_random_node(self, ctx, host_cluster):
        """Get a node randomly from the host cluster.

        :param ctx: An instance of the request context.
        :param host_cluster: The uuid of the hosting cluster.
        """
        self.cluster = None
        try:
            self.cluster = cluster.Cluster.load(ctx, cluster_id=host_cluster)
        except exc.ResourceNotFound as ex:
            msg = ex.enhance_msg('host', ex)
            raise exc.InternalError(message=msg)

        filters = {consts.NODE_STATUS: consts.NS_ACTIVE}
        nodes = no.Node.get_all_by_cluster(ctx,
                                           cluster_id=host_cluster,
                                           filters=filters)
        if len(nodes) == 0:
            msg = _("The cluster (%s) contains no active nodes") % host_cluster
            raise exc.InternalError(message=msg)

        # TODO(anyone): Should pick a node by its load
        db_node = nodes[random.randrange(len(nodes))]
        return node_mod.Node.load(ctx, db_node=db_node)

    def _get_host_ip(self, obj, host_node, host_type):
        """Fetch the ip address of physical node.

        :param obj: The node object representing the container instance.
        :param host_node: The name or ID of the hosting node object.
        :param host_type: The type of the hosting node, which can be either a
                          nova server or a heat stack.
        :returns: The fixed IP address of the hosting node.
        """
        host_ip = None
        if host_type == self.HOST_NOVA_SERVER:
            server = self.compute(obj).server_get(host_node)
            private_addrs = server.addresses['private']
            for addr in private_addrs:
                if addr['version'] == 4 and addr['OS-EXT-IPS:type'] == 'fixed':
                    host_ip = addr['addr']
        elif host_type == self.HOST_HEAT_STACK:
            stack = self.orchestration(obj).stack_get(host_node)
            outputs = stack.outputs or {}
            if outputs:
                for output in outputs:
                    if output['output_key'] == 'fixed_ip':
                        host_ip = output['output_value']
                        break

            if not outputs or host_ip is None:
                msg = _("Output 'fixed_ip' is missing from the provided stack"
                        " node")
                raise exc.InternalError(message=msg)

        return host_ip

    def do_validate(self, obj):
        """Validate if the spec has provided valid configuration.

        :param obj: The node object.
        """
        cluster = self.properties[self.HOST_CLUSTER]
        node = self.properties[self.HOST_NODE]
        if all([cluster, node]):
            msg = _("Either '%(c)s' or '%(n)s' must be specified, but not "
                    "both.") % {
                        'c': self.HOST_CLUSTER,
                        'n': self.HOST_NODE
                    }
            raise exc.InvalidSpec(message=msg)

        if not any([cluster, node]):
            msg = _("Either '%(c)s' or '%(n)s' must be specified.") % {
                'c': self.HOST_CLUSTER,
                'n': self.HOST_NODE
            }
            raise exc.InvalidSpec(message=msg)

        if cluster:
            try:
                co.Cluster.find(self.context, cluster)
            except (exc.ResourceNotFound, exc.MultipleChoices):
                msg = _("The specified %(key)s '%(val)s' could not be found "
                        "or is not unique.") % {
                            'key': self.HOST_CLUSTER,
                            'val': cluster
                        }
                raise exc.InvalidSpec(message=msg)

        if node:
            try:
                no.Node.find(self.context, node)
            except (exc.ResourceNotFound, exc.MultipleChoices):
                msg = _("The specified %(key)s '%(val)s' could not be found "
                        "or is not unique.") % {
                            'key': self.HOST_NODE,
                            'val': node
                        }
                raise exc.InvalidSpec(message=msg)

    def do_create(self, obj):
        """Create a container instance using the given profile.

        :param obj: The node object for this container.
        :returns: ID of the container instance or ``None`` if driver fails.
        :raises: `EResourceCreation`
        """
        name = self.properties[self.NAME]
        if name is None:
            name = '-'.join([obj.name, utils.random_name()])

        params = {
            'image': self.properties[self.IMAGE],
            'name': name,
            'command': self.properties[self.COMMAND],
        }

        try:
            ctx = context.get_service_context(project=obj.project,
                                              user=obj.user)
            dockerclient = self.docker(obj)
            db_api.node_add_dependents(ctx, self.host.id, obj.id)
            container = dockerclient.container_create(**params)
            dockerclient.start(container['Id'])
        except exc.InternalError as ex:
            raise exc.EResourceCreation(type='container', message=str(ex))

        self.container_id = container['Id'][:36]
        return self.container_id

    def do_delete(self, obj):
        """Delete a container node.

        :param obj: The node object representing the container.
        :returns: `None`
        """
        if not obj.physical_id:
            return

        try:
            self.handle_stop(obj)
            self.docker(obj).container_delete(obj.physical_id)
        except exc.InternalError as ex:
            raise exc.EResourceDeletion(type='container',
                                        id=obj.physical_id,
                                        message=str(ex))
        ctx = context.get_admin_context()
        db_api.node_remove_dependents(ctx, self.host.id, obj.id)
        return

    def do_update(self, obj, new_profile=None, **params):
        """Perform update on the container.

        :param obj: the container to operate on
        :param new_profile: the new profile for the container.
        :param params: a dictionary of optional parameters.
        :returns: True if update was successful or False otherwise.
        :raises: `EResourceUpdate` if operation fails.
        """
        self.server_id = obj.physical_id
        if not self.server_id:
            return False

        if not new_profile:
            return False

        if not self.validate_for_update(new_profile):
            return False

        name_changed, new_name = self._check_container_name(obj, new_profile)
        if name_changed:
            self._update_name(obj, new_name)

        return True

    def _check_container_name(self, obj, profile):
        """Check if there is a new name to be assigned to the container.

        :param obj: The node object to operate on.
        :param new_profile: The new profile which may contain a name for
                            the container.
        :return: A tuple consisting a boolean indicating whether the name
                 needs change and the container name determined.
        """
        old_name = self.properties[self.NAME] or obj.name
        new_name = profile.properties[self.NAME] or obj.name
        if old_name == new_name:
            return False, new_name
        return True, new_name

    def _update_name(self, obj, new_name):
        try:
            self.docker(obj).rename(obj.physical_id, new_name)
        except exc.InternalError as ex:
            raise exc.EResourceUpdate(type='container',
                                      id=obj.physical_id,
                                      message=str(ex))

    def handle_reboot(self, obj, **options):
        """Handler for a reboot operation.

        :param obj: The node object representing the container.
        :returns: None
        """
        if not obj.physical_id:
            return

        if 'timeout' in options:
            params = {'timeout': options['timeout']}
        else:
            params = {}
        try:
            self.docker(obj).restart(obj.physical_id, **params)
        except exc.InternalError as ex:
            raise exc.EResourceOperation(type='container',
                                         id=obj.physical_id[:8],
                                         op='rebooting',
                                         message=str(ex))
        return

    def handle_pause(self, obj):
        """Handler for a pause operation.

        :param obj: The node object representing the container.
        :returns: None
        """
        if not obj.physical_id:
            return

        try:
            self.docker(obj).pause(obj.physical_id)
        except exc.InternalError as ex:
            raise exc.EResourceOperation(type='container',
                                         id=obj.physical_id[:8],
                                         op='pausing',
                                         message=str(ex))
        return

    def handle_unpause(self, obj):
        """Handler for an unpause operation.

        :param obj: The node object representing the container.
        :returns: None
        """
        if not obj.physical_id:
            return

        try:
            self.docker(obj).unpause(obj.physical_id)
        except exc.InternalError as ex:
            raise exc.EResourceOperation(type='container',
                                         id=obj.physical_id[:8],
                                         op='unpausing',
                                         message=str(ex))
        return

    def handle_stop(self, obj, **options):
        """Handler for the stop operation."""
        if not obj.physical_id:
            return
        timeout = options.get('timeout', None)
        if timeout:
            timeout = int(timeout)
        try:
            self.docker(obj).stop(obj.physical_id, timeout=timeout)
        except exc.InternalError as ex:
            raise exc.EResourceOperation(type='container',
                                         id=obj.physical_id[:8],
                                         op='stop',
                                         message=str(ex))
Exemplo n.º 12
0
class ServerProfile(base.Profile):
    '''Profile for an OpenStack Nova server.'''

    KEYS = (
        CONTEXT,
        ADMIN_PASS,
        AUTO_DISK_CONFIG,
        AVAILABILITY_ZONE,
        BLOCK_DEVICE_MAPPING,
        BLOCK_DEVICE_MAPPING_V2,
        CONFIG_DRIVE,
        FLAVOR,
        IMAGE,
        KEY_NAME,
        METADATA,
        NAME,
        NETWORKS,
        PERSONALITY,
        SECURITY_GROUPS,
        USER_DATA,
        SCHEDULER_HINTS,
    ) = (
        'context',
        'adminPass',
        'auto_disk_config',
        'availability_zone',
        'block_device_mapping',
        'block_device_mapping_v2',
        'config_drive',
        'flavor',
        'image',
        'key_name',
        'metadata',
        'name',
        'networks',
        'personality',
        'security_groups',
        'user_data',
        'scheduler_hints',
    )

    BDM_KEYS = (
        BDM_DEVICE_NAME,
        BDM_VOLUME_SIZE,
    ) = (
        'device_name',
        'volume_size',
    )

    BDM2_KEYS = (
        BDM2_UUID,
        BDM2_SOURCE_TYPE,
        BDM2_DESTINATION_TYPE,
        BDM2_DISK_BUS,
        BDM2_DEVICE_NAME,
        BDM2_VOLUME_SIZE,
        BDM2_GUEST_FORMAT,
        BDM2_BOOT_INDEX,
        BDM2_DEVICE_TYPE,
        BDM2_DELETE_ON_TERMINATION,
    ) = (
        'uuid',
        'source_type',
        'destination_type',
        'disk_bus',
        'device_name',
        'volume_size',
        'guest_format',
        'boot_index',
        'device_type',
        'delete_on_termination',
    )

    NETWORK_KEYS = (
        PORT,
        FIXED_IP,
        NETWORK,
    ) = (
        'port',
        'fixed-ip',
        'network',
    )

    PERSONALITY_KEYS = (
        PATH,
        CONTENTS,
    ) = (
        'path',
        'contents',
    )

    SCHEDULER_HINTS_KEYS = (GROUP, ) = ('group', )

    properties_schema = {
        CONTEXT:
        schema.Map(_('Customized security context for operating servers.'), ),
        ADMIN_PASS:
        schema.String(_('Password for the administrator account.'), ),
        AUTO_DISK_CONFIG:
        schema.Boolean(
            _('Whether the disk partition is done automatically.'),
            default=True,
        ),
        AVAILABILITY_ZONE:
        schema.String(
            _('Name of availability zone for running the server.'), ),
        BLOCK_DEVICE_MAPPING:
        schema.List(
            _('A list specifying the properties of block devices to be used '
              'for this server.'),
            schema=schema.Map(
                _('A map specifying the properties of a block device to be '
                  'used by the server.'),
                schema={
                    BDM_DEVICE_NAME:
                    schema.String(
                        _('Block device name, should be <=255 chars.'), ),
                    BDM_VOLUME_SIZE:
                    schema.Integer(_('Block device size in GB.'), ),
                }),
        ),
        BLOCK_DEVICE_MAPPING_V2:
        schema.List(
            _('A list specifying the properties of block devices to be used '
              'for this server.'),
            schema=schema.Map(
                _('A map specifying the properties of a block device to be '
                  'used by the server.'),
                schema={
                    BDM2_UUID:
                    schema.String(
                        _('ID of the source image, snapshot or volume'), ),
                    BDM2_SOURCE_TYPE:
                    schema.String(
                        _('Volume source type, should be image, snapshot, '
                          'volume or blank'),
                        required=True,
                    ),
                    BDM2_DESTINATION_TYPE:
                    schema.String(
                        _('Volume destination type, should be volume or '
                          'local'),
                        required=True,
                    ),
                    BDM2_DISK_BUS:
                    schema.String(_('Bus of the device.'), ),
                    BDM2_DEVICE_NAME:
                    schema.String(
                        _('Name of the device(e.g. vda, xda, ....).'), ),
                    BDM2_VOLUME_SIZE:
                    schema.Integer(
                        _('Size of the block device in MB(for swap) and '
                          'in GB(for other formats)'),
                        required=True,
                    ),
                    BDM2_GUEST_FORMAT:
                    schema.String(
                        _('Specifies the disk file system format(e.g. swap, '
                          'ephemeral, ...).'), ),
                    BDM2_BOOT_INDEX:
                    schema.Integer(_('Define the boot order of the device'), ),
                    BDM2_DEVICE_TYPE:
                    schema.String(
                        _('Type of the device(e.g. disk, cdrom, ...).'), ),
                    BDM2_DELETE_ON_TERMINATION:
                    schema.Boolean(
                        _('Whether to delete the volume when the server '
                          'stops.'), ),
                }),
        ),
        CONFIG_DRIVE:
        schema.Boolean(
            _('Whether config drive should be enabled for the server.'), ),
        FLAVOR:
        schema.String(
            _('ID of flavor used for the server.'),
            required=True,
            updatable=True,
        ),
        IMAGE:
        schema.String(
            # IMAGE is not required, because there could be BDM or BDMv2
            # support and the corresponding settings effective
            _('ID of image to be used for the new server.'),
            updatable=True,
        ),
        KEY_NAME:
        schema.String(_('Name of Nova keypair to be injected to server.'), ),
        METADATA:
        schema.Map(
            _('A collection of key/value pairs to be associated with the '
              'server created. Both key and value should be <=255 chars.'),
            updatable=True,
        ),
        NAME:
        schema.String(
            _('Name of the server.'),
            updatable=True,
        ),
        NETWORKS:
        schema.List(
            _('List of networks for the server.'),
            schema=schema.Map(
                _('A map specifying the properties of a network for uses.'),
                schema={
                    NETWORK:
                    schema.String(
                        _('Name or ID of network to create a port on.'), ),
                    PORT:
                    schema.String(_('Port ID to be used by the network.'), ),
                    FIXED_IP:
                    schema.String(_('Fixed IP to be used by the network.'), ),
                },
            ),
            updatable=True,
        ),
        PERSONALITY:
        schema.List(
            _('List of files to be injected into the server, where each.'),
            schema=schema.Map(
                _('A map specifying the path & contents for an injected '
                  'file.'),
                schema={
                    PATH:
                    schema.String(
                        _('In-instance path for the file to be injected.'),
                        required=True,
                    ),
                    CONTENTS:
                    schema.String(
                        _('Contents of the file to be injected.'),
                        required=True,
                    ),
                },
            ),
        ),
        SCHEDULER_HINTS:
        schema.Map(
            _('A collection of key/value pairs to be associated with the '
              'Scheduler hints. Both key and value should be <=255 chars.'), ),
        SECURITY_GROUPS:
        schema.List(
            _('List of security groups.'),
            schema=schema.String(
                _('Name of a security group'),
                required=True,
            ),
        ),
        USER_DATA:
        schema.String(_('User data to be exposed by the metadata server.'), ),
    }

    OP_NAMES = (OP_REBOOT, ) = ('reboot', )

    REBOOT_TYPE = 'type'
    REBOOT_TYPES = (REBOOT_SOFT, REBOOT_HARD) = ('SOFT', 'HARD')

    OPERATIONS = {
        OP_REBOOT:
        schema.Operation(
            _("Reboot the nova server."),
            schema={
                REBOOT_TYPE:
                schema.String(
                    _("Type of reboot which can be 'SOFT' or 'HARD'."),
                    default=REBOOT_SOFT,
                    constraints=[
                        constraints.AllowedValues(REBOOT_TYPES),
                    ])
            })
    }

    def __init__(self, type_name, name, **kwargs):
        super(ServerProfile, self).__init__(type_name, name, **kwargs)

        self._novaclient = None
        self._neutronclient = None
        self.server_id = None

    def nova(self, obj):
        '''Construct nova client based on object.

        :param obj: Object for which the client is created. It is expected to
                    be None when retrieving an existing client. When creating
                    a client, it contains the user and project to be used.
        '''

        if self._novaclient is not None:
            return self._novaclient
        params = self._build_conn_params(obj.user, obj.project)
        self._novaclient = driver_base.SenlinDriver().compute(params)
        return self._novaclient

    def neutron(self, obj):
        '''Construct neutron client based on object.

        :param obj: Object for which the client is created. It is expected to
                    be None when retrieving an existing client. When creating
                    a client, it contains the user and project to be used.
        '''

        if self._neutronclient is not None:
            return self._neutronclient
        params = self._build_conn_params(obj.user, obj.project)
        self._neutronclient = driver_base.SenlinDriver().network(params)
        return self._neutronclient

    def do_validate(self, obj):
        '''Validate if the spec has provided valid info for server creation.'''
        return True

    def _resolve_bdm(self, bdm):
        for bd in bdm:
            for key in self.BDM2_KEYS:
                if bd[key] is None:
                    del bd[key]
        return bdm

    def _resolve_network(self, networks, client):
        for network in networks:
            net_name_id = network.get(self.NETWORK)
            if net_name_id:
                res = client.network_get(net_name_id)
                network['uuid'] = res.id
                del network[self.NETWORK]
                if network['port'] is None:
                    del network['port']
                if network['fixed-ip'] is None:
                    del network['fixed-ip']
        return networks

    def do_create(self, obj):
        '''Create a server using the given profile.'''
        kwargs = {}
        for key in self.KEYS:
            # context is treated as connection parameters
            if key == self.CONTEXT:
                continue

            if self.properties[key] is not None:
                kwargs[key] = self.properties[key]

        name_or_id = self.properties[self.IMAGE]
        if name_or_id is not None:
            image = self.nova(obj).image_find(name_or_id)
            # wait for new version of openstacksdk to fix this
            kwargs.pop(self.IMAGE)
            kwargs['imageRef'] = image.id

        flavor_id = self.properties[self.FLAVOR]
        flavor = self.nova(obj).flavor_find(flavor_id, False)

        # wait for new verson of openstacksdk to fix this
        kwargs.pop(self.FLAVOR)
        kwargs['flavorRef'] = flavor.id

        name = self.properties[self.NAME]
        if name:
            kwargs['name'] = name
        else:
            kwargs['name'] = obj.name

        metadata = self.properties[self.METADATA] or {}
        if obj.cluster_id:
            metadata['cluster'] = obj.cluster_id
        kwargs['metadata'] = metadata

        block_device_mapping_v2 = self.properties[self.BLOCK_DEVICE_MAPPING_V2]
        if block_device_mapping_v2 is not None:
            kwargs['block_device_mapping_v2'] = self._resolve_bdm(
                block_device_mapping_v2)

        user_data = self.properties[self.USER_DATA]
        if user_data is not None:
            ud = encodeutils.safe_encode(user_data)
            kwargs['user_data'] = encodeutils.safe_decode(base64.b64encode(ud))

        networks = self.properties[self.NETWORKS]
        if networks is not None:
            kwargs['networks'] = self._resolve_network(networks,
                                                       self.neutron(obj))

        secgroups = self.properties[self.SECURITY_GROUPS]
        if secgroups:
            kwargs['security_groups'] = [{'name': sg} for sg in secgroups]

        if 'placement' in obj.data:
            if 'zone' in obj.data['placement']:
                kwargs['availability_zone'] = obj.data['placement']['zone']
            if 'servergroup' in obj.data['placement']:
                group_id = obj.data['placement']['servergroup']
                hints = self.properties.get(self.SCHEDULER_HINTS, {})
                hints.update({'group': group_id})
                kwargs['scheduler_hints'] = hints

        LOG.info('Creating server: %s' % kwargs)
        server = self.nova(obj).server_create(**kwargs)
        self.nova(obj).wait_for_server(server.id)
        self.server_id = server.id

        return server.id

    def do_delete(self, obj):
        self.server_id = obj.physical_id

        if not obj.physical_id:
            return True

        try:
            self.nova(obj).server_delete(self.server_id)
            self.nova(obj).wait_for_server_delete(self.server_id)
        except Exception as ex:
            LOG.error('Error: %s' % six.text_type(ex))
            return False

        return True

    def do_update(self, obj, new_profile=None, **params):
        '''Perform update on the server.

        :param obj: the server to operate on
        :param new_profile: the new profile for the server.
        :param params: a dictionary of optional parameters.
        '''
        self.server_id = obj.physical_id
        if not self.server_id:
            return True

        if not new_profile:
            return True

        if not self.validate_for_update(new_profile):
            return False

        # TODO(Yanyan Hu): Update block_device properties

        # Update basic properties of server
        if not self._update_basic_properties(obj, new_profile):
            return False

        # Update server flavor
        flavor = self.properties[self.FLAVOR]
        new_flavor = new_profile.properties[self.FLAVOR]
        if new_flavor != flavor:
            try:
                self._update_flavor(obj, flavor, new_flavor)
            except Exception as ex:
                LOG.exception(_('Failed in updating server flavor: %s'),
                              six.text_type(ex))
                return False

        # Update server image
        old_passwd = self.properties.get(self.ADMIN_PASS)
        passwd = old_passwd
        if new_profile.properties[self.ADMIN_PASS] is not None:
            passwd = new_profile.properties[self.ADMIN_PASS]
        image = self.properties[self.IMAGE]
        new_image = new_profile.properties[self.IMAGE]
        if new_image != image:
            try:
                self._update_image(obj, image, new_image, passwd)
            except Exception as ex:
                LOG.exception(_('Failed in updating server image: %s'),
                              six.text_type(ex))
                return False
        elif old_passwd != passwd:
            # TODO(Jun Xu): update server admin password
            pass

        # Update server network
        networks_current = self.properties[self.NETWORKS]
        networks_create = new_profile.properties[self.NETWORKS]
        networks_delete = copy.deepcopy(networks_current)
        for network in networks_current:
            if network in networks_create:
                networks_create.remove(network)
                networks_delete.remove(network)
        if networks_create or networks_delete:
            # We have network interfaces to be deleted and/or created
            try:
                self._update_network(obj, networks_create, networks_delete)
            except Exception as ex:
                LOG.exception(_('Failed in updating server network: %s'),
                              six.text_type(ex))
                return False

        return True

    def _update_basic_properties(self, obj, new_profile):
        '''Updating basic server properties including name, metadata'''

        # Update server metadata
        metadata = self.properties[self.METADATA]
        new_metadata = new_profile.properties[self.METADATA]
        if new_metadata != metadata:
            if new_metadata is None:
                new_metadata = {}
            try:
                self.nova(obj).server_metadata_update(self.server_id,
                                                      new_metadata)
            except Exception as ex:
                LOG.exception(_('Failed in updating server metadata: %s'),
                              six.text_type(ex))
                return False

        # Update server name
        name = self.properties[self.NAME]
        new_name = new_profile.properties[self.NAME]
        if new_name != name:
            attrs = {'name': new_name if new_name else obj.name}
            try:
                self.nova(obj).server_update(self.server_id, **attrs)
            except Exception as ex:
                LOG.exception(_('Failed in updating server name: %s'),
                              six.text_type(ex))
                return False

        return True

    def _update_flavor(self, obj, old_flavor, new_flavor):
        '''Updating server flavor'''
        res = self.nova(obj).flavor_find(old_flavor)
        old_flavor_id = res.id
        res = self.nova(obj).flavor_find(new_flavor)
        new_flavor_id = res.id
        if new_flavor_id == old_flavor_id:
            return

        try:
            self.nova(obj).server_resize(obj.physical_id, new_flavor_id)
            self.nova(obj).wait_for_server(obj.physical_id, 'VERIFY_RESIZE')
        except Exception as ex:
            LOG.error(_("Server resizing failed, revert it: %s"),
                      six.text_type(ex))
            self.nova(obj).server_resize_revert(obj.physical_id)
            self.nova(obj).wait_for_server(obj.physical_id, 'ACTIVE')
            raise exception.ResourceUpdateFailure(resource=obj.physical_id)

        self.nova(obj).server_resize_confirm(obj.physical_id)
        self.nova(obj).wait_for_server(obj.physical_id, 'ACTIVE')

    def _update_image(self, obj, old_image, new_image, admin_password):
        '''Updating server image'''

        if old_image:
            res = self.nova(obj).image_find(old_image)
            image_id = res.id
        else:
            server = self.nova(obj).server_get(obj.physical_id)
            image_id = server.image['id']

        if new_image:
            res = self.nova(obj).image_find(new_image)
            new_image_id = res.id
            if new_image_id != image_id:
                # (Jun Xu): Not update name here if name changed,
                # it should be updated  in do_update
                self.nova(obj).server_rebuild(obj.physical_id, new_image_id,
                                              self.properties.get(self.NAME),
                                              admin_password)
                self.nova(obj).wait_for_server(obj.physical_id, 'ACTIVE')
        else:
            # TODO(Yanyan Hu): Allow server update with new_image
            # set to None if Nova service supports it
            LOG.error(
                _("Updating Nova server with image set to None is "
                  "not supported by Nova."))
            raise exception.ResourceUpdateFailure(resource=obj.physical_id)

    def _update_network(self, obj, networks_create, networks_delete):
        '''Updating server network interfaces'''

        server = self.nova(obj).server_get(self.server_id)
        ports_existing = list(self.nova(obj).server_interface_list(server))
        ports = []
        for p in ports_existing:
            fixed_ips = []
            for addr in p['fixed_ips']:
                fixed_ips.append(addr['ip_address'])
            ports.append({
                'port_id': p['port_id'],
                'net_id': p['net_id'],
                'fixed_ips': fixed_ips
            })

        # Detach some existing ports
        # Step1. Accurately search port with port_id or fixed-ip/net_id
        for n in networks_delete:
            if n['port'] is not None:
                for p in ports:
                    if p['port_id'] == n['port']:
                        ports.remove(p)
                        break
                res = self.nova(obj).server_interface_delete(n['port'], server)
            elif n['fixed-ip'] is not None:
                res = self.neutron(obj).network_get(n['network'])
                net_id = res.id
                for p in ports:
                    if (n['fixed-ip'] in p['fixed_ips']) and (p['net_id']
                                                              == net_id):
                        res = self.nova(obj).server_interface_delete(
                            p['port_id'], server)
                        ports.remove(p)
                        break

        # Step2. Fuzzy search port with net_id
        for n in networks_delete:
            if n['port'] is None and n['fixed-ip'] is None:
                res = self.neutron(obj).network_get(n['network'])
                net_id = res.id
                for p in ports:
                    if p['net_id'] == net_id:
                        res = self.nova(obj).server_interface_delete(
                            p['port_id'], server)
                        ports.remove(p)
                        break

        # Attach new ports added in new network definition
        for n in networks_create:
            net_name_id = n.get(self.NETWORK, None)
            if net_name_id:
                res = self.neutron(obj).network_get(net_name_id)
                n['net_id'] = res.id
                if n['fixed-ip'] is not None:
                    n['fixed_ips'] = [{'ip_address': n['fixed-ip']}]
            if n['port'] is not None:
                n['port_id'] = n['port']
            del n['network']
            del n['port']
            del n['fixed-ip']
            self.nova(obj).server_interface_create(server, **n)

        return

    def do_check(self, obj):
        if not obj.physical_id:
            return False

        self.server_id = obj.physical_id

        try:
            server = self.nova(obj).server_get(self.server_id)
        except Exception as ex:
            LOG.error('Error: %s' % six.text_type(ex))
            return False

        if (server is None or server.status != 'ACTIVE'):
            return False

        return True

    def do_get_details(self, obj):
        known_keys = {
            'OS-DCF:diskConfig', 'OS-EXT-AZ:availability_zone',
            'OS-EXT-STS:power_state', 'OS-EXT-STS:vm_state', 'accessIPv4',
            'accessIPv6', 'config_drive', 'created', 'hostId', 'id',
            'key_name', 'locked', 'metadata', 'name',
            'os-extended-volumes:volumes_attached', 'progress', 'status',
            'updated'
        }
        if obj.physical_id is None or obj.physical_id == '':
            return {}

        try:
            server = self.nova(obj).server_get(obj.physical_id)
        except exception.InternalError as ex:
            return {'Error': {'code': ex.code, 'message': six.text_type(ex)}}

        if server is None:
            return {}
        server_data = server.to_dict()
        details = {
            'image': server_data['image']['id'],
            'flavor': server_data['flavor']['id'],
        }
        for key in known_keys:
            if key in server_data:
                details[key] = server_data[key]

        # process special keys like 'OS-EXT-STS:task_state': these keys have
        # a default value '-' when not existing
        special_keys = [
            'OS-EXT-STS:task_state',
            'OS-SRV-USG:launched_at',
            'OS-SRV-USG:terminated_at',
        ]
        for key in special_keys:
            if key in server_data:
                val = server_data[key]
                details[key] = val if val else '-'

        # process network addresses
        details['addresses'] = {}
        for net in server_data['addresses']:
            addresses = []
            for addr in server_data['addresses'][net]:
                # Ignore IPv6 address
                if addr['version'] == 4:
                    addresses.append(addr['addr'])
            details['addresses'][net] = addresses

        # process security groups
        sgroups = []
        if 'security_groups' in server_data:
            for sg in server_data['security_groups']:
                sgroups.append(sg['name'])
        if len(sgroups) == 0:
            details['security_groups'] = ''
        elif len(sgroups) == 1:
            details['security_groups'] = sgroups[0]
        else:
            details['security_groups'] = sgroups

        return dict((k, details[k]) for k in sorted(details))

    def do_join(self, obj, cluster_id):
        if not obj.physical_id:
            return False

        metadata = self.nova(obj).server_metadata_get(obj.physical_id) or {}
        metadata['cluster'] = cluster_id
        self.nova(obj).server_metadata_update(obj.physical_id, metadata)
        return super(ServerProfile, self).do_join(obj, cluster_id)

    def do_leave(self, obj):
        if not obj.physical_id:
            return False

        self.nova(obj).server_metadata_delete(obj.physical_id, ['cluster'])
        return super(ServerProfile, self).do_leave(obj)

    def do_rebuild(self, obj):
        if not obj.physical_id:
            return False

        self.server_id = obj.physical_id

        try:
            server = self.nova(obj).server_get(self.server_id)
        except Exception as ex:
            LOG.exception(_('Failed at getting server: %s'), six.text_type(ex))
            return False

        if server is None or server.image is None:
            return False

        image_id = server.image['id']
        admin_pass = self.properties.get(self.ADMIN_PASS)

        try:
            self.nova(obj).server_rebuild(self.server_id, image_id,
                                          self.properties.get(self.NAME),
                                          admin_pass)
            self.nova(obj).wait_for_server(self.server_id, 'ACTIVE')
        except Exception as ex:
            LOG.exception(_('Failed at rebuilding server: %s'),
                          six.text_type(ex))
            return False

        return True

    def do_recover(self, obj, **options):

        if 'operation' in options:
            if options['operation'] == 'REBUILD':
                return self.do_rebuild(obj)

        res = super(ServerProfile, self).do_recover(obj, **options)

        return res

    def handle_reboot(self, obj, **options):
        """Handler for the reboot operation."""
        pass