From 0b4c53e3630608de14713e8dde7d7e8359bc2086 Mon Sep 17 00:00:00 2001 From: Kevin Zhao Date: Mon, 17 Jul 2017 22:40:45 +0800 Subject: [PATCH] Add the Object Capsule and database table Part of blueprint introduce-compose Change-Id: I8ed3711d14e8cda92b5bb5cf2874980cbd66cad0 Signed-off-by: Kevin Zhao --- zun/db/etcd/models.py | 23 +++ .../a251f1f61217_create_capsule_table.py | 58 +++++++ zun/db/sqlalchemy/models.py | 27 ++++ zun/objects/__init__.py | 3 + zun/objects/capsule.py | 152 ++++++++++++++++++ zun/tests/unit/objects/test_objects.py | 1 + zun/websocket/websocketclient.py | 2 +- zun/websocket/websocketproxy.py | 2 +- 8 files changed, 266 insertions(+), 2 deletions(-) create mode 100644 zun/db/sqlalchemy/alembic/versions/a251f1f61217_create_capsule_table.py create mode 100644 zun/objects/capsule.py diff --git a/zun/db/etcd/models.py b/zun/db/etcd/models.py index 7f96d938c..df24426d7 100644 --- a/zun/db/etcd/models.py +++ b/zun/db/etcd/models.py @@ -177,3 +177,26 @@ class ResourceClass(Base): @classmethod def fields(cls): return cls._fields + + +class Capsule(Base): + """Represents a capsule.""" + + _path = '/capsules' + + _fields = objects.Capsule.fields.keys() + + def __init__(self, capsule_data): + self.path = Capsule.path() + for f in Capsule.fields(): + setattr(self, f, None) + self.id = 1 + self.update(capsule_data) + + @classmethod + def path(cls): + return cls._path + + @classmethod + def fields(cls): + return cls._fields diff --git a/zun/db/sqlalchemy/alembic/versions/a251f1f61217_create_capsule_table.py b/zun/db/sqlalchemy/alembic/versions/a251f1f61217_create_capsule_table.py new file mode 100644 index 000000000..5dc87b5a3 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/a251f1f61217_create_capsule_table.py @@ -0,0 +1,58 @@ +# Copyright 2017 ARM Holdings +# +# 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. + +"""create capsule table + +Revision ID: a251f1f61217 +Revises: 75315e219cfb +Create Date: 2017-06-20 17:12:56.105277 + +""" + +# revision identifiers, used by Alembic. +revision = 'a251f1f61217' +down_revision = '75315e219cfb' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + +from zun.db.sqlalchemy import models + + +def upgrade(): + op.create_table( + 'capsule', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('capsule_version', sa.String(length=255), nullable=True), + sa.Column('kind', sa.String(length=36), nullable=True), + sa.Column('project_id', sa.String(length=255), nullable=True), + sa.Column('user_id', sa.String(length=255), nullable=True), + sa.Column('restart_policy', sa.String(length=255), nullable=True), + sa.Column('host_selector', sa.String(length=255), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('uuid', sa.String(length=36), nullable=False), + sa.Column('status', sa.String(length=255), nullable=True), + sa.Column('status_reason', sa.Text(), nullable=True), + sa.Column('message', models.JSONEncodedDict(), nullable=True), + sa.Column('spec', models.JSONEncodedDict(), nullable=True), + sa.Column('cpu', sa.Float(), nullable=True), + sa.Column('memory', sa.String(length=255), nullable=True), + sa.Column('meta_name', sa.String(length=255), nullable=True), + sa.Column('meta_labels', models.JSONEncodedList(), nullable=True), + sa.Column('containers_uuids', models.JSONEncodedList(), nullable=True), + sa.PrimaryKeyConstraint('id'), + ) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index cf21bd44a..b28c9ff46 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -288,3 +288,30 @@ class ComputeNode(Base): os = Column(String(64), nullable=True) kernel_version = Column(String(128), nullable=True) labels = Column(JSONEncodedDict) + + +class Capsule(Base): + """Represents a capsule.""" + + __tablename__ = 'capsule' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_capsule0uuid'), + table_args() + ) + uuid = Column(String(36), nullable=False) + id = Column(Integer, primary_key=True, nullable=False) + host_selector = Column(String(255)) + capsule_version = Column(String(255)) + kind = Column(String(255)) + restart_policy = Column(JSONEncodedDict) + project_id = Column(String(255)) + user_id = Column(String(255)) + + status = Column(String(20)) + status_reason = Column(Text, nullable=True) + meta_labels = Column(JSONEncodedList) + meta_name = Column(String(255)) + spec = Column(JSONEncodedDict) + containers_uuids = Column(JSONEncodedList) + cpu = Column(Float) + memory = Column(String(255)) diff --git a/zun/objects/__init__.py b/zun/objects/__init__.py index 6f1fa56f8..e26351159 100644 --- a/zun/objects/__init__.py +++ b/zun/objects/__init__.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from zun.objects import capsule from zun.objects import compute_node from zun.objects import container from zun.objects import image @@ -26,6 +27,7 @@ NUMATopology = numa.NUMATopology ResourceProvider = resource_provider.ResourceProvider ResourceClass = resource_class.ResourceClass ComputeNode = compute_node.ComputeNode +Capsule = capsule.Capsule __all__ = ( Container, @@ -36,4 +38,5 @@ __all__ = ( NUMANode, NUMATopology, ComputeNode, + Capsule, ) diff --git a/zun/objects/capsule.py b/zun/objects/capsule.py new file mode 100644 index 000000000..ed158ed54 --- /dev/null +++ b/zun/objects/capsule.py @@ -0,0 +1,152 @@ +# Copyright 2017 ARM Holdings. +# +# 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_versionedobjects import fields + +from zun.db import api as dbapi +from zun.objects import base +from zun.objects import fields as z_fields + + +@base.ZunObjectRegistry.register +class Capsule(base.ZunPersistentObject, base.ZunObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'capsule_version': fields.StringField(nullable=True), + 'kind': fields.StringField(nullable=True), + 'restart_policy': fields.DictOfStringsField(nullable=True), + 'host_selector': fields.StringField(nullable=True), + # uuid is the infra-container id + 'id': fields.IntegerField(), + 'uuid': fields.UUIDField(nullable=True), + 'project_id': fields.StringField(nullable=True), + 'user_id': fields.StringField(nullable=True), + + 'status': z_fields.ContainerStatusField(nullable=True), + 'status_reason': fields.StringField(nullable=True), + 'cpu': fields.FloatField(nullable=True), + 'memory': fields.StringField(nullable=True), + + # conclude the readable message + # 'key': 'value'--> 'time':'message' + # wait until zun notify is finished + # 'message': fields.DictOfStringsField(nullable=True), + + 'spec': z_fields.JsonField(nullable=True), + 'meta_name': fields.StringField(nullable=True), + 'meta_labels': z_fields.JsonField(nullable=True), + 'containers': fields.ListOfObjectsField('Container', nullable=True), + 'containers_uuids': fields.ListOfStringsField(nullable=True), + # add volumes after Cinder integration is ready + # 'volumes': fields.ListOfObjectsField(nullable=True), + } + + @staticmethod + def _from_db_object(capsule, db_capsule): + """Converts a database entity to a formal object.""" + for field in capsule.fields: + if field != 'containers': + setattr(capsule, field, db_capsule[field]) + capsule.obj_reset_changes() + return capsule + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Converts a list of database entities to a list of formal objects.""" + return [Capsule._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_capsule = dbapi.get_capsule_by_uuid(context, uuid) + capsule = Capsule._from_db_object(cls(context), db_capsule) + return capsule + + @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_capsules = dbapi.list_capsules( + context, limit=limit, marker=marker, sort_key=sort_key, + sort_dir=sort_dir, filters=filters) + return Capsule._from_db_object_list(db_capsules, 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() + db_capsule = dbapi.create_capsule(context, values) + self._from_db_object(self, db_capsule) + + @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_capsule(context, 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() + dbapi.update_capsule(context, self.uuid, updates) + + self.obj_reset_changes() diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index e64d0522c..73c198f28 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -353,6 +353,7 @@ object_data = { 'ResourceProvider': '1.0-92b427359d5a4cf9ec6c72cbe630ee24', 'ZunService': '1.1-b1549134bfd5271daec417ca8cabc77e', 'ComputeNode': '1.6-33a173b969781644a95ea2925eb5cca2', + 'Capsule': '1.0-0dce1bd569773c35193d75c285226e75', } diff --git a/zun/websocket/websocketclient.py b/zun/websocket/websocketclient.py index e00f250c1..688846cdc 100644 --- a/zun/websocket/websocketclient.py +++ b/zun/websocket/websocketclient.py @@ -1,4 +1,4 @@ -# Copyright 2017 Linaro Limited +# Copyright 2017 ARM Holdings. # # 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 diff --git a/zun/websocket/websocketproxy.py b/zun/websocket/websocketproxy.py index 8844d9b25..72838be90 100644 --- a/zun/websocket/websocketproxy.py +++ b/zun/websocket/websocketproxy.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Linaro Limited +# Copyright 2017 ARM Holdings. # # 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