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))
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))
def test_validate(self): sot = schema.IntegerParam() result = sot.validate(123) self.assertIsNone(result)
def test_validate_bad_type(self): sot = schema.IntegerParam() self.assertRaises(ValueError, sot.validate, 'not int')
def test_basic(self): sot = schema.IntegerParam() self.assertEqual('Integer', sot['type']) self.assertEqual(False, sot['required'])