Beispiel #1
0
    def test_validate_failed_constraint(self):
        sot = schema.IntegerParam(
            constraints=[constraints.AllowedValues((123, 124))])

        ex = self.assertRaises(exc.ESchema, sot.validate, 12)

        self.assertEqual("'12' must be one of the allowed values: 123, 124",
                         six.text_type(ex))
Beispiel #2
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))
Beispiel #3
0
 def test_validate(self):
     sot = schema.IntegerParam()
     result = sot.validate(123)
     self.assertIsNone(result)
Beispiel #4
0
 def test_validate_bad_type(self):
     sot = schema.IntegerParam()
     self.assertRaises(ValueError, sot.validate, 'not int')
Beispiel #5
0
 def test_basic(self):
     sot = schema.IntegerParam()
     self.assertEqual('Integer', sot['type'])
     self.assertEqual(False, sot['required'])