zun/zun/objects/container.py

500 lines
20 KiB
Python

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_log import log as logging
from oslo_versionedobjects import fields
from zun.common import consts
from zun.common import exception
from zun.common.i18n import _
from zun.db import api as dbapi
from zun.objects import base
from zun.objects import exec_instance as exec_inst
from zun.objects import fields as z_fields
from zun.objects import pci_device
from zun.objects import registry
LOG = logging.getLogger(__name__)
CONTAINER_OPTIONAL_ATTRS = ["pci_devices", "exec_instances", "registry"]
@base.ZunObjectRegistry.register
class Cpuset(base.ZunObject):
VERSION = '1.0'
fields = {
'cpuset_cpus': fields.SetOfIntegersField(nullable=True),
'cpuset_mems': fields.SetOfIntegersField(nullable=True),
}
def _to_dict(self):
return {
'cpuset_cpus': self.cpuset_cpus,
'cpuset_mems': self.cpuset_mems
}
@classmethod
def _from_dict(cls, data_dict):
if not data_dict:
obj = cls(cpuset_cpus=None, cpuset_mems=None)
else:
cpuset_cpus = data_dict.get('cpuset_cpus')
cpuset_mems = data_dict.get('cpuset_mems')
obj = cls(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems)
obj.obj_reset_changes()
return obj
class ContainerBase(base.ZunPersistentObject, base.ZunObject):
fields = {
'id': fields.IntegerField(),
'container_id': fields.StringField(nullable=True),
'uuid': fields.UUIDField(nullable=True),
'name': fields.StringField(nullable=True),
'project_id': fields.StringField(nullable=True),
'user_id': fields.StringField(nullable=True),
'image': fields.StringField(nullable=True),
'cpu': fields.FloatField(nullable=True),
'cpu_policy': fields.StringField(nullable=True),
'cpuset': fields.ObjectField("Cpuset", nullable=True),
'memory': fields.StringField(nullable=True),
'command': fields.ListOfStringsField(nullable=True),
'status': z_fields.ContainerStatusField(nullable=True),
'status_reason': fields.StringField(nullable=True),
'task_state': z_fields.TaskStateField(nullable=True),
'environment': fields.DictOfStringsField(nullable=True),
'workdir': fields.StringField(nullable=True),
'auto_remove': fields.BooleanField(nullable=True),
'ports': z_fields.ListOfIntegersField(nullable=True),
'hostname': fields.StringField(nullable=True),
'labels': fields.DictOfStringsField(nullable=True),
'meta': fields.DictOfStringsField(nullable=True),
'addresses': z_fields.JsonField(nullable=True),
'image_pull_policy': fields.StringField(nullable=True),
'host': fields.StringField(nullable=True),
'restart_policy': fields.DictOfStringsField(nullable=True),
'status_detail': fields.StringField(nullable=True),
'interactive': fields.BooleanField(nullable=True),
'tty': fields.BooleanField(nullable=True),
'image_driver': fields.StringField(nullable=True),
'websocket_url': fields.StringField(nullable=True),
'websocket_token': fields.StringField(nullable=True),
'security_groups': fields.ListOfStringsField(nullable=True),
'runtime': fields.StringField(nullable=True),
'pci_devices': fields.ListOfObjectsField('PciDevice',
nullable=True),
'disk': fields.IntegerField(nullable=True),
'auto_heal': fields.BooleanField(nullable=True),
'started_at': fields.DateTimeField(tzinfo_aware=False, nullable=True),
'exposed_ports': z_fields.JsonField(nullable=True),
'exec_instances': fields.ListOfObjectsField('ExecInstance',
nullable=True),
'privileged': fields.BooleanField(nullable=True),
'healthcheck': z_fields.JsonField(nullable=True),
'registry_id': fields.IntegerField(nullable=True),
'registry': fields.ObjectField("Registry", nullable=True),
}
# should be redefined in subclasses
container_type = None
@staticmethod
def _from_db_object(container, db_container):
"""Converts a database entity to a formal object."""
for field in container.fields:
if field in ['pci_devices', 'exec_instances', 'registry',
'containers', 'init_containers']:
continue
if field == 'cpuset':
container.cpuset = Cpuset._from_dict(
db_container['cpuset'])
continue
setattr(container, field, db_container[field])
container.obj_reset_changes()
return container
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [cls._from_db_object(cls(context), obj)
for obj in db_objects]
@base.remotable_classmethod
def get_by_uuid(cls, context, uuid):
"""Find a container based on uuid and return a :class:`Container` object.
:param uuid: the uuid of a container.
:param context: Security context
:returns: a :class:`Container` object.
"""
db_container = dbapi.get_container_by_uuid(context, cls.container_type,
uuid)
container = cls._from_db_object(cls(context), db_container)
return container
@base.remotable_classmethod
def get_by_name(cls, context, name):
"""Find a container based on name and return a Container object.
:param name: the logical name of a container.
:param context: Security context
:returns: a :class:`Container` object.
"""
db_container = dbapi.get_container_by_name(context, cls.container_type,
name)
container = cls._from_db_object(cls(context), db_container)
return container
@staticmethod
def get_container_any_type(context, uuid):
"""Find a container of any type based on uuid.
:param uuid: the uuid of a container.
:param context: Security context
:returns: a :class:`ContainerBase` object.
"""
db_container = dbapi.get_container_by_uuid(context, consts.TYPE_ANY,
uuid)
type = db_container['container_type']
if type == consts.TYPE_CONTAINER:
container_cls = Container
elif type == consts.TYPE_CAPSULE:
container_cls = Capsule
elif type == consts.TYPE_CAPSULE_CONTAINER:
container_cls = CapsuleContainer
elif type == consts.TYPE_CAPSULE_INIT_CONTAINER:
container_cls = CapsuleInitContainer
else:
raise exception.ZunException(_('Unknown container type: %s'), type)
obj = container_cls(context)
container = container_cls._from_db_object(obj, db_container)
return container
@base.remotable_classmethod
def list(cls, context, limit=None, marker=None,
sort_key=None, sort_dir=None, filters=None):
"""Return a list of Container objects.
:param context: Security context.
:param limit: maximum number of resources to return in a single result.
:param marker: pagination marker for large data sets.
:param sort_key: column to sort results by.
:param sort_dir: direction to sort. "asc" or "desc".
:param filters: filters when list containers, the filter name could be
'name', 'image', 'project_id', 'user_id', 'memory'.
For example, filters={'image': 'nginx'}
:returns: a list of :class:`Container` object.
"""
db_containers = dbapi.list_containers(
context, cls.container_type, limit=limit, marker=marker,
sort_key=sort_key, sort_dir=sort_dir, filters=filters)
return cls._from_db_object_list(db_containers, cls, context)
@base.remotable_classmethod
def list_by_host(cls, context, host):
"""Return a list of Container objects by host.
:param context: Security context.
:param host: A compute host.
:returns: a list of :class:`Container` object.
"""
db_containers = dbapi.list_containers(context, cls.container_type,
filters={'host': host})
return cls._from_db_object_list(db_containers, cls, context)
@base.remotable
def create(self, context):
"""Create a Container record in the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
values = self.obj_get_changes()
cpuset_obj = values.pop('cpuset', None)
if cpuset_obj is not None:
values['cpuset'] = cpuset_obj._to_dict()
values['container_type'] = self.container_type
db_container = dbapi.create_container(context, values)
self._from_db_object(self, db_container)
@base.remotable
def destroy(self, context=None):
"""Delete the Container from the DB.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
dbapi.destroy_container(context, self.container_type, self.uuid)
self.obj_reset_changes()
@base.remotable
def save(self, context=None):
"""Save updates to this Container.
Updates will be made column by column based on the result
of self.what_changed().
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
updates = self.obj_get_changes()
cpuset_obj = updates.pop('cpuset', None)
if cpuset_obj is not None:
updates['cpuset'] = cpuset_obj._to_dict()
dbapi.update_container(context, self.container_type, self.uuid,
updates)
self.obj_reset_changes()
@base.remotable
def refresh(self, context=None):
"""Loads updates for this Container.
Loads a container with the same uuid from the database and
checks for updated attributes. Updates are applied from
the loaded container column by column, if there are any updates.
:param context: Security context. NOTE: This should only
be used internally by the indirection_api.
Unfortunately, RPC requires context as the first
argument, even though we don't use it.
A context should be set when instantiating the
object, e.g.: Container(context)
"""
current = self.__class__.get_by_uuid(self._context, uuid=self.uuid)
for field in self.fields:
if self.obj_attr_is_set(field) and \
getattr(self, field) != getattr(current, field):
setattr(self, field, getattr(current, field))
def obj_load_attr(self, attrname):
if attrname not in CONTAINER_OPTIONAL_ATTRS:
raise exception.ObjectActionError(
action='obj_load_attr',
reason=_('attribute %s not lazy-loadable') % attrname)
if not self._context:
raise exception.OrphanedObjectError(method='obj_load_attr',
objtype=self.obj_name())
LOG.debug("Lazy-loading '%(attr)s' on %(name)s uuid %(uuid)s",
{'attr': attrname,
'name': self.obj_name(),
'uuid': self.uuid,
})
# NOTE(danms): We handle some fields differently here so that we
# can be more efficient
if attrname == 'pci_devices':
self._load_pci_devices()
if attrname == 'exec_instances':
self._load_exec_instances()
if attrname == 'registry':
self._load_registry()
self.obj_reset_changes([attrname])
def _load_pci_devices(self):
self.pci_devices = pci_device.PciDevice.list_by_container_uuid(
self._context, self.uuid)
def _load_exec_instances(self):
self.exec_instances = exec_inst.ExecInstance.list_by_container_id(
self._context, self.id)
def _load_registry(self):
self.registry = None
if self.registry_id:
self.registry = registry.Registry.get_by_id(
self._context, self.registry_id)
@base.remotable_classmethod
def get_count(cls, context, project_id, flag):
"""Get the counts of Container objects in the database.
:param context: The request context for database access.
:param project_id: The project_id to count across.
:param flag: The name of resource, one of the following options:
- containers: Count the number of containers owned by the
project.
- memory: The sum of containers's memory.
- cpu: The sum of container's cpu.
- disk: The sum of container's disk size.
"""
usage = dbapi.count_usage(context, cls.container_type, project_id,
flag)[0] or 0
return usage
@base.ZunObjectRegistry.register
class Container(ContainerBase):
# Version 1.0: Initial version
# Version 1.1: Add container_id column
# Version 1.2: Add memory column
# Version 1.3: Add task_state column
# Version 1.4: Add cpu, workdir, ports, hostname and labels columns
# Version 1.5: Add meta column
# Version 1.6: Add addresses column
# Version 1.7: Add host column
# Version 1.8: Add restart_policy
# Version 1.9: Add status_detail column
# Version 1.10: Add tty, stdin_open
# Version 1.11: Add image_driver
# Version 1.12: Add 'Created' to ContainerStatus
# Version 1.13: Add more task states for container
# Version 1.14: Add method 'list_by_host'
# Version 1.15: Combine tty and stdin_open
# Version 1.16: Add websocket_url and token
# Version 1.17: Add security_groups
# Version 1.18: Add auto_remove
# Version 1.19: Add runtime column
# Version 1.20: Change runtime to String type
# Version 1.21: Add pci_device attribute
# Version 1.22: Add 'Deleting' to ContainerStatus
# Version 1.23: Add the missing 'pci_devices' attribute
# Version 1.24: Add the storage_opt attribute
# Version 1.25: Change TaskStateField definition
# Version 1.26: Add auto_heal
# Version 1.27: Make auto_heal field nullable
# Version 1.28: Add 'Dead' to ContainerStatus
# Version 1.29: Add 'Restarting' to ContainerStatus
# Version 1.30: Add capsule_id attribute
# Version 1.31: Add 'started_at' attribute
# Version 1.32: Add 'exec_instances' attribute
# Version 1.33: Change 'command' to List type
# Version 1.34: Add privileged to container
# Version 1.35: Add 'healthcheck' attribute
# Version 1.36: Add 'get_count' method
# Version 1.37: Add 'exposed_ports' attribute
# Version 1.38: Add 'cpuset' attribute
# Version 1.39: Add 'register' and 'registry_id' attributes
# Version 1.40: Add 'tty' attributes
VERSION = '1.40'
container_type = consts.TYPE_CONTAINER
@base.ZunObjectRegistry.register
class Capsule(ContainerBase):
# Version 1.0: Initial version
# Version 1.1: Add 'tty' attributes
VERSION = '1.1'
container_type = consts.TYPE_CAPSULE
fields = {
'containers': fields.ListOfObjectsField('CapsuleContainer',
nullable=True),
'init_containers': fields.ListOfObjectsField('CapsuleInitContainer',
nullable=True),
}
def as_dict(self):
capsule_dict = super(Capsule, self).as_dict()
capsule_dict['containers'] = [c.as_dict() for c in self.containers]
capsule_dict['init_containers'] = [c.as_dict()
for c in self.init_containers]
return capsule_dict
def obj_load_attr(self, attrname):
if attrname == 'containers':
self._load_capsule_containers()
self.obj_reset_changes([attrname])
elif attrname == 'init_containers':
self._load_capsule_init_containers()
self.obj_reset_changes([attrname])
else:
super(Capsule, self).obj_load_attr(attrname)
def _load_capsule_containers(self):
self.containers = CapsuleContainer.list_by_capsule_id(
self._context, self.id)
def _load_capsule_init_containers(self):
self.init_containers = CapsuleInitContainer.list_by_capsule_id(
self._context, self.id)
@base.ZunObjectRegistry.register
class CapsuleContainer(ContainerBase):
# Version 1.0: Initial version
# Version 1.1: Add 'tty' attributes
VERSION = '1.1'
container_type = consts.TYPE_CAPSULE_CONTAINER
fields = {
'capsule_id': fields.IntegerField(nullable=False),
}
@base.remotable_classmethod
def list_by_capsule_id(cls, context, capsule_id):
"""Return a list of Container objects by capsule_id.
:param context: Security context.
:param host: A capsule id.
:returns: a list of :class:`Container` object.
"""
db_containers = dbapi.list_containers(
context, cls.container_type, filters={'capsule_id': capsule_id})
return Container._from_db_object_list(db_containers, cls, context)
@base.ZunObjectRegistry.register
class CapsuleInitContainer(ContainerBase):
# Version 1.0: Initial version
# Version 1.1: Add 'tty' attributes
VERSION = '1.1'
container_type = consts.TYPE_CAPSULE_INIT_CONTAINER
fields = {
'capsule_id': fields.IntegerField(nullable=False),
}
@base.remotable_classmethod
def list_by_capsule_id(cls, context, capsule_id):
"""Return a list of Container objects by capsule_id.
:param context: Security context.
:param host: A capsule id.
:returns: a list of :class:`Container` object.
"""
db_containers = dbapi.list_containers(
context, cls.container_type, filters={'capsule_id': capsule_id})
return Container._from_db_object_list(db_containers, cls, context)