Add exec_instances to data model

Zun is going to implement a proxy for interactive execute.
The first step is to introduce data model for tracking each
exec instances of a container. An exec instance in Zun is
an one-to-one mapping for exec instance in Docker.
We will use it to track a docker's exec instance as well as
a generated token for granting access of this exec instance.
In the future, the websocket proxy will retrieve the token from
incoming request and match it to the one stored in DB.
The request will be rejected if the token doesn't match.

This patch introduces the data model of exec_instance
in DB api and objects layer. It basically contains three
fields: container_id, exec_id, url, token.
The container_id is the id of the container record in Zun.
The exec_id is the ID of the docker's exec instance.
The url is the docker daemon endpoint for the exec instance
The token is as described above.

Partial-Bug: #1735076
Change-Id: Ib38a46c0e3f3aae58e1f562536b858bc4cd23bf8
This commit is contained in:
Hongbin Lu 2018-06-03 21:52:13 +00:00
parent 4db4adf6e5
commit 48075909a9
13 changed files with 397 additions and 5 deletions

View File

@ -421,6 +421,11 @@ class ContainerAlreadyExists(ResourceExists):
message = _("A container with %(field)s %(value)s already exists.")
class ExecInstanceAlreadyExists(ResourceExists):
message = _("An exec instance with exec_id %(exec_id)s already exists"
"in container.")
class ComputeNodeAlreadyExists(ResourceExists):
message = _("A compute node with %(field)s %(value)s already exists.")

View File

@ -1030,3 +1030,37 @@ def update_network(context, uuid, values):
"""
return _get_dbdriver_instance().update_network(
context, uuid, values)
@profiler.trace("db")
def create_exec_instance(context, values):
"""Create a new exec instance.
:param context: The security context
:param values: A dict containing several items used to identify
and track the exec instance, and several dicts which are
passed into the Drivers when managing this exec instance.
:returns: An exec instance.
"""
return _get_dbdriver_instance().create_exec_instance(context, values)
@profiler.trace("db")
def list_exec_instances(context, filters=None, limit=None, marker=None,
sort_key=None, sort_dir=None):
"""List matching exec instances.
Return a list exec instances that match the specified filters.
:param context: The security context
:param filters: Filters to apply. Defaults to None.
:param limit: Maximum number of containers to return.
:param marker: the last item of the previous page; we return the next
result set.
:param sort_key: Attribute by which results should be sorted.
:param sort_dir: Direction in which results should be sorted.
(asc, desc)
:returns: A list of exec instances.
"""
return _get_dbdriver_instance().list_exec_instances(
context, filters, limit, marker, sort_key, sort_dir)

View File

@ -0,0 +1,46 @@
# 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 exec_instance table
Revision ID: 26896d5f9053
Revises: 012a730926e8
Create Date: 2018-06-03 17:24:33.192354
"""
# revision identifiers, used by Alembic.
revision = '26896d5f9053'
down_revision = '012a730926e8'
branch_labels = None
depends_on = None
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'exec_instance',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), primary_key=True, nullable=False),
sa.Column('container_id', sa.Integer(), nullable=False),
sa.Column('exec_id', sa.String(255), nullable=False),
sa.Column('token', sa.String(255), nullable=True),
sa.Column('url', sa.String(255), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.ForeignKeyConstraint(['container_id'], ['container.id'],
ondelete='CASCADE'),
sa.UniqueConstraint('container_id', 'exec_id',
name='uniq_exec_instance0container_id_exec_id'),
)

View File

@ -1234,3 +1234,25 @@ class Connection(object):
return query.one()
except NoResultFound:
raise exception.NetworkNotFound(network=network_uuid)
def list_exec_instances(self, context, filters=None, limit=None,
marker=None, sort_key=None, sort_dir=None):
query = model_query(models.ExecInstance)
query = self._add_exec_instances_filters(query, filters)
return _paginate_query(models.ExecInstance, limit, marker,
sort_key, sort_dir, query)
def _add_exec_instances_filters(self, query, filters):
filter_names = ['container_id', 'exec_id', 'token']
return self._add_filters(query, filters=filters,
filter_names=filter_names)
def create_exec_instance(self, context, values):
exec_inst = models.ExecInstance()
exec_inst.update(values)
try:
exec_inst.save()
except db_exc.DBDuplicateEntry:
raise exception.ExecInstanceAlreadyExists(
exec_id=values['exec_id'])
return exec_inst

View File

@ -197,6 +197,30 @@ class VolumeMapping(Base):
auto_remove = Column(Boolean, default=False)
class ExecInstance(Base):
"""Represents an exec instance."""
__tablename__ = 'exec_instance'
__table_args__ = (
schema.UniqueConstraint(
'container_id', 'exec_id',
name='uniq_exec_instance0container_id_exec_id'),
table_args()
)
id = Column(Integer, primary_key=True, nullable=False)
container_id = Column(Integer,
ForeignKey('container.id', ondelete="CASCADE"),
nullable=False)
exec_id = Column(String(255), nullable=False)
token = Column(String(255), nullable=True)
url = Column(String(255), nullable=True)
container = orm.relationship(
Container,
backref=orm.backref('exec_instances'),
foreign_keys=container_id,
primaryjoin='and_(ExecInstance.container_id==Container.id)')
class Image(Base):
"""Represents an image. """

View File

@ -15,6 +15,7 @@ from zun.objects import compute_node
from zun.objects import container
from zun.objects import container_action
from zun.objects import container_pci_requests
from zun.objects import exec_instance
from zun.objects import image
from zun.objects import network
from zun.objects import numa
@ -47,6 +48,7 @@ ContainerPCIRequest = container_pci_requests.ContainerPCIRequest
ContainerPCIRequests = container_pci_requests.ContainerPCIRequests
ContainerAction = container_action.ContainerAction
ContainerActionEvent = container_action.ContainerActionEvent
ExecInstance = exec_instance.ExecInstance
__all__ = (
'Container',
@ -68,4 +70,5 @@ __all__ = (
'ContainerPCIRequests',
'ContainerAction',
'ContainerActionEvent',
'ExecInstance',
)

View File

@ -16,6 +16,7 @@ from oslo_versionedobjects import fields
from zun.common import exception
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
@ -23,7 +24,7 @@ from zun.objects import pci_device
LOG = logging.getLogger(__name__)
CONTAINER_OPTIONAL_ATTRS = ["pci_devices"]
CONTAINER_OPTIONAL_ATTRS = ["pci_devices", "exec_instances"]
@base.ZunObjectRegistry.register
@ -60,7 +61,8 @@ class Container(base.ZunPersistentObject, base.ZunObject):
# Version 1.29: Add 'Restarting' to ContainerStatus
# Version 1.30: Add capsule_id attribute
# Version 1.31: Add 'started_at' attribute
VERSION = '1.31'
# Version 1.32: Add 'exec_instances' attribute
VERSION = '1.32'
fields = {
'id': fields.IntegerField(),
@ -100,13 +102,15 @@ class Container(base.ZunPersistentObject, base.ZunObject):
'auto_heal': fields.BooleanField(nullable=True),
'capsule_id': fields.IntegerField(nullable=True),
'started_at': fields.DateTimeField(tzinfo_aware=False, nullable=True),
'exec_instances': fields.ListOfObjectsField('ExecInstance',
nullable=True),
}
@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']:
if field in ['pci_devices', 'exec_instances']:
continue
setattr(container, field, db_container[field])
@ -293,8 +297,15 @@ class Container(base.ZunPersistentObject, base.ZunObject):
if attrname == 'pci_devices':
self._load_pci_devices()
if attrname == 'exec_instances':
self._load_exec_instances()
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)

View File

@ -0,0 +1,61 @@
# 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.db import api as dbapi
from zun.objects import base
LOG = logging.getLogger(__name__)
@base.ZunObjectRegistry.register
class ExecInstance(base.ZunPersistentObject, base.ZunObject):
# Version 1.0: Initial version
VERSION = '1.0'
fields = {
'id': fields.IntegerField(),
'container_id': fields.IntegerField(nullable=False),
'exec_id': fields.StringField(nullable=False),
'token': fields.StringField(nullable=True),
'url': fields.StringField(nullable=True),
}
@staticmethod
def _from_db_object(exec_inst, db_exec_inst):
"""Converts a database entity to a formal object."""
for field in exec_inst.fields:
setattr(exec_inst, field, db_exec_inst[field])
exec_inst.obj_reset_changes()
return exec_inst
@staticmethod
def _from_db_object_list(db_objects, cls, context):
"""Converts a list of database entities to a list of formal objects."""
return [ExecInstance._from_db_object(cls(context), obj)
for obj in db_objects]
@base.remotable_classmethod
def list_by_container_id(cls, context, container_id):
db_objects = dbapi.list_exec_instances(
context, filters={'container_id': container_id})
return ExecInstance._from_db_object_list(db_objects, cls, context)
@base.remotable
def create(self, context):
values = self.obj_get_changes()
db_exec_inst = dbapi.create_exec_instance(context, values)
self._from_db_object(self, db_exec_inst)

View File

@ -0,0 +1,90 @@
# 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_utils import uuidutils
import six
from zun.common import exception
import zun.conf
from zun.db import api as dbapi
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
CONF = zun.conf.CONF
class DbExecInstanceTestCase(base.DbTestCase):
def setUp(self):
super(DbExecInstanceTestCase, self).setUp()
def test_create_exec_instance(self):
utils.create_test_exec_instance(context=self.context)
def test_create_exec_instance_already_exists(self):
utils.create_test_exec_instance(context=self.context,
container_id=1, exec_id='test-id')
with self.assertRaisesRegex(
exception.ExecInstanceAlreadyExists,
'An exec instance with exec_id test-id .*'):
utils.create_test_exec_instance(
context=self.context, container_id=1, exec_id='test-id')
def test_list_exec_instances(self):
exec_ids = []
for i in range(1, 6):
exec_inst = utils.create_test_exec_instance(
id=i,
context=self.context,
container_id=1,
exec_id=uuidutils.generate_uuid())
exec_ids.append(six.text_type(exec_inst['exec_id']))
res = dbapi.list_exec_instances(self.context)
res_exec_ids = [r.exec_id for r in res]
self.assertEqual(sorted(exec_ids), sorted(res_exec_ids))
def test_list_exec_instances_sorted(self):
exec_ids = []
for i in range(5):
exec_inst = utils.create_test_exec_instance(
id=i,
context=self.context,
container_id=1,
exec_id=uuidutils.generate_uuid())
exec_ids.append(six.text_type(exec_inst['exec_id']))
res = dbapi.list_exec_instances(self.context, sort_key='exec_id')
res_exec_ids = [r.exec_id for r in res]
self.assertEqual(sorted(exec_ids), res_exec_ids)
def test_list_exec_instances_with_filters(self):
exec_inst1 = utils.create_test_exec_instance(
id=1,
context=self.context,
container_id=1,
exec_id='exec-one')
exec_inst2 = utils.create_test_exec_instance(
id=2,
context=self.context,
container_id=2,
exec_id='exec-two')
res = dbapi.list_exec_instances(
self.context, filters={'container_id': 1})
self.assertEqual([exec_inst1.id], [r.id for r in res])
res = dbapi.list_exec_instances(
self.context, filters={'container_id': 2})
self.assertEqual([exec_inst2.id], [r.id for r in res])
res = dbapi.list_exec_instances(
self.context, filters={'container_id': 777})
self.assertEqual([], [r.id for r in res])

View File

@ -581,6 +581,33 @@ def get_test_network(**kwargs):
}
def get_test_exec_instance(**kwargs):
return {
'id': kwargs.get('id', 43),
'container_id': kwargs.get('container_id', 42),
'exec_id': kwargs.get('exec_id', 'fake-exec-id'),
'token': kwargs.get('token', 'fake-exec-token'),
'url': kwargs.get('url', 'fake-url'),
'created_at': kwargs.get('created_at'),
'updated_at': kwargs.get('updated_at'),
}
def create_test_exec_instance(**kwargs):
"""Create test exec instance entry in DB and return ExecInstance DB object.
Function to be used to create test ExecInstance objects in the database.
:param kwargs: kwargs with overriding values for default attributes.
:returns: Test ExecInstance DB object.
"""
exec_inst = get_test_exec_instance(**kwargs)
# Let DB generate ID if it isn't specified explicitly
if 'id' not in kwargs:
del exec_inst['id']
dbapi = _get_dbapi()
return dbapi.create_exec_instance(kwargs['context'], exec_inst)
def create_test_network(**kwargs):
network = get_test_network(**kwargs)
# Let DB generate ID if it isn't specified explicitly

View File

@ -159,7 +159,7 @@ class TestContainerObject(base.DbTestCase):
self.assertEqual(self.context, container._context)
@mock.patch('zun.objects.PciDevice.list_by_container_uuid')
def test_obj_load_attr(self, mock_list):
def test_obj_load_attr_pci_devices(self, mock_list):
uuid = self.fake_container['uuid']
dev_dict = {'dev_id': 'fake_dev_id',
'container_uuid': 'fake_container_uuid'}
@ -171,3 +171,19 @@ class TestContainerObject(base.DbTestCase):
container = objects.Container.get_by_uuid(self.context, uuid)
container.obj_load_attr('pci_devices')
self.assertEqual(pci_devices, container.pci_devices)
@mock.patch('zun.objects.ExecInstance.list_by_container_id')
def test_obj_load_attr_exec_instances(self, mock_list):
uuid = self.fake_container['uuid']
exec_dict = {'exec_id': 'fake_exec_id',
'container_id': self.fake_container['id']}
exec_inst = objects.ExecInstance(self.context, **exec_dict)
exec_inst.create(self.context)
exec_insts = [exec_inst]
mock_list.return_value = exec_insts
with mock.patch.object(self.dbapi, 'get_container_by_uuid',
autospec=True) as mock_get_container:
mock_get_container.return_value = self.fake_container
container = objects.Container.get_by_uuid(self.context, uuid)
container.obj_load_attr('exec_instances')
self.assertEqual(exec_insts, container.exec_instances)

View File

@ -0,0 +1,52 @@
# Copyright 2015 OpenStack Foundation
# All Rights Reserved.
#
# 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.
import mock
from testtools.matchers import HasLength
from zun import objects
from zun.tests.unit.db import base
from zun.tests.unit.db import utils
class TestExecInstanceObject(base.DbTestCase):
def setUp(self):
super(TestExecInstanceObject, self).setUp()
self.fake_exec_inst = utils.get_test_exec_instance()
def test_list_by_container_id(self):
with mock.patch.object(self.dbapi, 'list_exec_instances',
autospec=True) as mock_get_list:
mock_get_list.return_value = [self.fake_exec_inst]
exec_insts = objects.ExecInstance.list_by_container_id(
self.context, 111)
mock_get_list.assert_called_once_with(
self.context, {'container_id': 111}, None, None, None, None)
self.assertThat(exec_insts, HasLength(1))
self.assertIsInstance(exec_insts[0], objects.ExecInstance)
self.assertEqual(self.context, exec_insts[0]._context)
def test_create(self):
with mock.patch.object(self.dbapi, 'create_exec_instance',
autospec=True) as mock_create_exec_instance:
mock_create_exec_instance.return_value = self.fake_exec_inst
exec_inst = objects.ExecInstance(
self.context, **self.fake_exec_inst)
exec_inst.create(self.context)
mock_create_exec_instance.assert_called_once_with(
self.context, self.fake_exec_inst)
self.assertEqual(self.context, exec_inst._context)

View File

@ -344,7 +344,7 @@ class TestObject(test_base.TestCase, _TestObject):
# For more information on object version testing, read
# https://docs.openstack.org/zun/latest/
object_data = {
'Container': '1.31-4e1f27e1326bc42c7fa7ca0681bbe883',
'Container': '1.32-9e9a594ca58e978fb7b580292692a1a7',
'VolumeMapping': '1.1-50df6202f7846a136a91444c38eba841',
'Image': '1.1-330e6205c80b99b59717e1cfc6a79935',
'MyObj': '1.0-34c4b1aadefd177b13f9a2f894cc23cd',
@ -365,6 +365,7 @@ object_data = {
'ContainerAction': '1.1-b0c721f9e10c6c0d1e41e512c49eb877',
'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a',
'Network': '1.0-235ba13359282107f27c251af9aaffcd',
'ExecInstance': '1.0-59464e7b96db847c0abb1e96d3cec30a',
}