From 5a43580a702adc7a1425e06f7a463138d56929aa Mon Sep 17 00:00:00 2001 From: caishan Date: Fri, 27 Apr 2018 08:05:31 -0700 Subject: [PATCH] Add network-create to server side Add network-create to server side, so that zun have the ability to mange the network based on the kuryr driver. Change-Id: Ifdd8804280bc015d93888efc77afecc72ec77cff --- zun/api/controllers/v1/__init__.py | 9 ++ zun/api/controllers/v1/networks.py | 72 ++++++++++++++ zun/api/controllers/v1/schemas/network.py | 25 +++++ zun/api/controllers/v1/views/network_view.py | 34 +++++++ zun/api/validation/parameter_types.py | 14 +++ zun/common/policies/network.py | 6 +- zun/compute/api.py | 3 + zun/compute/manager.py | 20 ++++ zun/compute/rpcapi.py | 4 + zun/container/docker/driver.py | 12 +++ zun/container/driver.py | 6 ++ zun/db/api.py | 45 +++++++++ .../3298c6a5c3d9_create_network_table.py | 49 ++++++++++ zun/db/sqlalchemy/api.py | 46 +++++++++ zun/db/sqlalchemy/models.py | 19 ++++ zun/objects/__init__.py | 3 + zun/objects/network.py | 94 +++++++++++++++++++ zun/tests/unit/api/controllers/test_root.py | 4 + .../unit/api/controllers/v1/test_networks.py | 29 ++++++ zun/tests/unit/compute/test_compute_api.py | 8 ++ .../unit/compute/test_compute_manager.py | 11 +++ zun/tests/unit/db/utils.py | 14 +++ zun/tests/unit/objects/test_network.py | 55 +++++++++++ zun/tests/unit/objects/test_objects.py | 3 +- 24 files changed, 581 insertions(+), 4 deletions(-) create mode 100644 zun/api/controllers/v1/networks.py create mode 100644 zun/api/controllers/v1/schemas/network.py create mode 100644 zun/api/controllers/v1/views/network_view.py create mode 100644 zun/db/sqlalchemy/alembic/versions/3298c6a5c3d9_create_network_table.py create mode 100644 zun/objects/network.py create mode 100644 zun/tests/unit/api/controllers/v1/test_networks.py create mode 100644 zun/tests/unit/objects/test_network.py diff --git a/zun/api/controllers/v1/__init__.py b/zun/api/controllers/v1/__init__.py index a1d22fea4..4dc33b41f 100644 --- a/zun/api/controllers/v1/__init__.py +++ b/zun/api/controllers/v1/__init__.py @@ -28,6 +28,7 @@ from zun.api.controllers.v1 import capsules as capsule_controller from zun.api.controllers.v1 import containers as container_controller from zun.api.controllers.v1 import hosts as host_controller from zun.api.controllers.v1 import images as image_controller +from zun.api.controllers.v1 import networks as network_controller from zun.api.controllers.v1 import zun_services from zun.api.controllers import versions as ver from zun.api import http_error @@ -67,6 +68,7 @@ class V1(controllers_base.APIBase): 'services', 'containers', 'images', + 'networks', 'hosts', 'capsules', 'availability_zones' @@ -103,6 +105,12 @@ class V1(controllers_base.APIBase): pecan.request.host_url, 'images', '', bookmark=True)] + v1.networks = [link.make_link('self', pecan.request.host_url, + 'networks', ''), + link.make_link('bookmark', + pecan.request.host_url, + 'networks', '', + bookmark=True)] v1.hosts = [link.make_link('self', pecan.request.host_url, 'hosts', ''), link.make_link('bookmark', @@ -130,6 +138,7 @@ class Controller(controllers_base.Controller): services = zun_services.ZunServiceController() containers = container_controller.ContainersController() images = image_controller.ImagesController() + networks = network_controller.NetworkController() hosts = host_controller.HostController() availability_zones = a_zone.AvailabilityZoneController() capsules = capsule_controller.CapsuleController() diff --git a/zun/api/controllers/v1/networks.py b/zun/api/controllers/v1/networks.py new file mode 100644 index 000000000..6aa8fbbb7 --- /dev/null +++ b/zun/api/controllers/v1/networks.py @@ -0,0 +1,72 @@ +# 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 +import pecan + +from zun.api.controllers import base +from zun.api.controllers.v1 import collection +from zun.api.controllers.v1.schemas import network as schema +from zun.api.controllers.v1.views import network_view as view +from zun.api import utils as api_utils +from zun.api import validation +from zun.common import exception +from zun.common import policy +from zun import objects + +LOG = logging.getLogger(__name__) + + +class NetworkCollection(collection.Collection): + """API representation of a collection of network.""" + + fields = { + 'network' + } + + """A list containing network objects""" + + def __init__(self, **kwargs): + super(NetworkCollection, self).__init__(**kwargs) + self._type = 'network' + + @staticmethod + def convert_with_links(rpc_network, limit, url=None, + expand=False, **kwargs): + collection = NetworkCollection() + collection.network = [view.format_network(url, p) for p in rpc_network] + collection.next = collection.get_next(limit, url=url, **kwargs) + return collection + + +class NetworkController(base.Controller): + """Controller for Network""" + + @pecan.expose('json') + @api_utils.enforce_content_types(['application/json']) + @exception.wrap_pecan_controller_exception + @validation.validated(schema.network_create) + def post(self, **network_dict): + """Create a new network. + + :param network_dict: a network within the request body. + """ + context = pecan.request.context + policy.enforce(context, "network:create", + action="network:create") + network_dict['project_id'] = context.project_id + network_dict['user_id'] = context.user_id + new_network = objects.Network(context, **network_dict) + new_network.create(context) + pecan.request.compute_api.network_create(context, new_network) + pecan.response.status = 202 + return view.format_network(pecan.request.host_url, new_network) diff --git a/zun/api/controllers/v1/schemas/network.py b/zun/api/controllers/v1/schemas/network.py new file mode 100644 index 000000000..41f6e65f2 --- /dev/null +++ b/zun/api/controllers/v1/schemas/network.py @@ -0,0 +1,25 @@ +# 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 zun.api.validation import parameter_types + +_network_properties = { + 'neutron_net_id': parameter_types.neutron_net_id, + 'name': parameter_types.network_name +} + +network_create = { + 'type': 'object', + 'properties': _network_properties, + 'required': ['name'], + 'additionalProperties': False +} diff --git a/zun/api/controllers/v1/views/network_view.py b/zun/api/controllers/v1/views/network_view.py new file mode 100644 index 000000000..4f71a176d --- /dev/null +++ b/zun/api/controllers/v1/views/network_view.py @@ -0,0 +1,34 @@ +# +# 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 itertools + +_basic_keys = ( + 'uuid', + 'name', + 'project_id', + 'user_id', +) + + +def format_network(url, network): + def transform(key, value): + if key not in _basic_keys: + return + if key == 'uuid': + yield ('uuid', value) + else: + yield (key, value) + + return dict(itertools.chain.from_iterable( + transform(k, v) for k, v in network.as_dict().items())) diff --git a/zun/api/validation/parameter_types.py b/zun/api/validation/parameter_types.py index b74cd7097..3ae475930 100644 --- a/zun/api/validation/parameter_types.py +++ b/zun/api/validation/parameter_types.py @@ -481,3 +481,17 @@ capsule_template = { "additionalProperties": False, "required": ['kind', 'spec', 'metadata'] } + +neutron_net_id = { + 'type': ['string', 'null'], + 'minLength': 2, + 'maxLength': 255, + 'pattern': '[a-zA-Z0-9][a-zA-Z0-9_.-]' +} + +network_name = { + 'type': ['string', 'null'], + 'minLength': 2, + 'maxLength': 255, + 'pattern': '[a-zA-Z0-9][a-zA-Z0-9_.-]' +} diff --git a/zun/common/policies/network.py b/zun/common/policies/network.py index e7a4ca8fb..324ca7839 100644 --- a/zun/common/policies/network.py +++ b/zun/common/policies/network.py @@ -18,12 +18,12 @@ NETWORK = 'network:%s' rules = [ policy.DocumentedRuleDefault( - name=NETWORK % 'attach_external_network', + name=NETWORK % 'create', check_str=base.ROLE_ADMIN, - description='Attach an unshared external network to a container', + description='Create a network', operations=[ { - 'path': '/v1/containers', + 'path': '/v1/networks', 'method': 'POST' } ] diff --git a/zun/compute/api.py b/zun/compute/api.py index 0ac4c47e0..938271a54 100644 --- a/zun/compute/api.py +++ b/zun/compute/api.py @@ -225,3 +225,6 @@ class API(object): self._record_action_start(context, container, container_actions.NETWORK_ATTACH) return self.rpcapi.network_attach(context, container, *args) + + def network_create(self, context, network): + return self.rpcapi.network_create(context, network) diff --git a/zun/compute/manager.py b/zun/compute/manager.py index 7f1b8e9b6..98710d1b3 100644 --- a/zun/compute/manager.py +++ b/zun/compute/manager.py @@ -1194,3 +1194,23 @@ class Manager(periodic_task.PeriodicTasks): consts.NETWORK_ATTACHING) self.driver.network_attach(context, container, network) self._update_task_state(context, container, None) + + def network_create(self, context, network): + utils.spawn_n(self._do_create_network, context, network) + + def _do_create_network(self, context, network): + LOG.debug('Create network') + try: + docker_network = self.driver.create_network(context, network) + network.network_id = docker_network['Id'] + network.save() + except exception.NetworkNotFound as e: + LOG.error(six.text_type(e)) + return + except exception.DockerError as e: + LOG.error("Error occurred while calling Docker network API: %s", + six.text_type(e)) + raise + except Exception as e: + LOG.exception("Unexpected exception: %s", six.text_type(e)) + raise diff --git a/zun/compute/rpcapi.py b/zun/compute/rpcapi.py index f91409cbe..720394235 100644 --- a/zun/compute/rpcapi.py +++ b/zun/compute/rpcapi.py @@ -202,3 +202,7 @@ class API(rpc_service.API): def network_attach(self, context, container, network): return self._call(container.host, 'network_attach', container=container, network=network) + + def network_create(self, context, new_network): + host = None + return self._cast(host, 'network_create', network=new_network) diff --git a/zun/container/docker/driver.py b/zun/container/docker/driver.py index 99d7ee9a1..a141fbbcf 100644 --- a/zun/container/docker/driver.py +++ b/zun/container/docker/driver.py @@ -1123,3 +1123,15 @@ class DockerDriver(driver.ContainerDriver): addresses.update(update) container.addresses = addresses container.save(context) + + def create_network(self, context, network): + with docker_utils.docker_client() as docker: + network_api = zun_network.api(context, + docker_api=docker) + docker_net_name = self._get_docker_network_name( + context, network.neutron_net_id) + docker_network = network_api.create_network( + neutron_net_id=network.neutron_net_id, + name=docker_net_name + ) + return docker_network diff --git a/zun/container/driver.py b/zun/container/driver.py index 6dc044166..d42868f4f 100644 --- a/zun/container/driver.py +++ b/zun/container/driver.py @@ -261,6 +261,12 @@ class ContainerDriver(object): def network_attach(self, context, container, network): raise NotImplementedError() + def create_network(self, context, network): + raise NotImplementedError() + + def inspect_network(self, network): + raise NotImplementedError() + def node_support_disk_quota(self): raise NotImplementedError() diff --git a/zun/db/api.py b/zun/db/api.py index 8fe70f9c6..e852021a2 100644 --- a/zun/db/api.py +++ b/zun/db/api.py @@ -972,3 +972,48 @@ def quota_class_update(context, class_name, resource, limit): """Update a quota class or raise if it does not exist""" return _get_dbdriver_instance().quota_class_update(context, class_name, resource, limit) + + +@profiler.trace("db") +def create_network(context, values): + """Create a new network. + + :param context: The security context + :param values: A dict containing several items used to identify + and track the network, and several dicts which are + passed + into the Drivers when managing this network. For + example: + :: + { + 'uuid': uuidutils.generate_uuid(), + 'name': 'example', + 'type': 'virt' + } + :returns: A network. + """ + return _get_dbdriver_instance().create_network(context, values) + + +@profiler.trace("db") +def get_network_by_uuid(context, network_uuid): + """Return a network. + + :param context: The security context + :param network_uuid: The uuid of a network. + :returns: A network. + """ + return _get_dbdriver_instance().get_network_by_uuid(context, network_uuid) + + +@profiler.trace("db") +def update_network(context, uuid, values): + """Update properties of a network. + + :param context: Request context + :param uuid: The id or uuid of a network. + :param values: The properties to be updated + :returns: A network. + """ + return _get_dbdriver_instance().update_network( + context, uuid, values) diff --git a/zun/db/sqlalchemy/alembic/versions/3298c6a5c3d9_create_network_table.py b/zun/db/sqlalchemy/alembic/versions/3298c6a5c3d9_create_network_table.py new file mode 100644 index 000000000..23b1134e0 --- /dev/null +++ b/zun/db/sqlalchemy/alembic/versions/3298c6a5c3d9_create_network_table.py @@ -0,0 +1,49 @@ +# 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. + +"""empty message + +Revision ID: 3298c6a5c3d9 +Revises: 271c7f45982d +Create Date: 2018-04-28 06:32:26.493248 + +""" + +# revision identifiers, used by Alembic. +revision = '3298c6a5c3d9' +down_revision = '271c7f45982d' +branch_labels = None +depends_on = None + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.create_table( + 'network', + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('project_id', sa.String(length=255), nullable=True), + sa.Column('user_id', sa.String(length=255), nullable=True), + sa.Column('name', sa.String(length=255), nullable=True), + sa.Column('uuid', sa.String(length=36), nullable=True), + sa.Column('network_id', sa.String(length=255), nullable=True), + sa.Column('neutron_net_id', sa.String(length=255), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('uuid', name='uniq_network0uuid'), + sa.UniqueConstraint('neutron_net_id', + name='uniq_network0neutron_net_id'), + mysql_charset='utf8', + mysql_engine='InnoDB' + ) diff --git a/zun/db/sqlalchemy/api.py b/zun/db/sqlalchemy/api.py index 5b36522e0..31c73556e 100644 --- a/zun/db/sqlalchemy/api.py +++ b/zun/db/sqlalchemy/api.py @@ -1157,3 +1157,49 @@ class Connection(object): if not result: raise exception.QuotaClassNotFound(class_name=class_name) + + def create_network(self, context, values): + # ensure defaults are present for new containers + if not values.get('uuid'): + values['uuid'] = uuidutils.generate_uuid() + + if values.get('name'): + self._validate_unique_container_name(context, values['name']) + + network = models.Network() + network.update(values) + try: + network.save() + except db_exc.DBDuplicateEntry: + raise exception.ContainerAlreadyExists(field='UUID', + value=values['uuid']) + return network + + def update_network(self, context, network_uuid, values): + # NOTE(dtantsur): this can lead to very strange errors + if 'uuid' in values: + msg = _("Cannot overwrite UUID for an existing docker network.") + raise exception.InvalidParameterValue(err=msg) + return self._do_update_network(network_uuid, values) + + def _do_update_network(self, network_uuid, values): + session = get_session() + with session.begin(): + query = model_query(models.Network, session=session) + query = add_identity_filter(query, network_uuid) + try: + ref = query.with_lockmode('update').one() + except NoResultFound: + raise exception.NetworkNotFound(network=network_uuid) + + ref.update(values) + return ref + + def get_network_by_uuid(self, context, network_uuid): + query = model_query(models.Network) + query = self._add_project_filters(context, query) + query = query.filter_by(uuid=network_uuid) + try: + return query.one() + except NoResultFound: + raise exception.NetworkNotFound(network=network_uuid) diff --git a/zun/db/sqlalchemy/models.py b/zun/db/sqlalchemy/models.py index e0904effc..d469373e3 100644 --- a/zun/db/sqlalchemy/models.py +++ b/zun/db/sqlalchemy/models.py @@ -499,3 +499,22 @@ class QuotaClass(Base): class_name = Column(String(255)) resource = Column(String(255)) hard_limit = Column(Integer) + + +class Network(Base): + """Represents a network. """ + + __tablename__ = 'network' + __table_args__ = ( + schema.UniqueConstraint('uuid', name='uniq_network0uuid'), + schema.UniqueConstraint('neutron_net_id', + name='uniq_network0neutron_net_id'), + table_args() + ) + id = Column(Integer, primary_key=True) + name = Column(String(255)) + neutron_net_id = Column(String(255)) + network_id = Column(String(255)) + project_id = Column(String(255)) + user_id = Column(String(255)) + uuid = Column(String(36)) diff --git a/zun/objects/__init__.py b/zun/objects/__init__.py index de4c70719..3d968acc5 100644 --- a/zun/objects/__init__.py +++ b/zun/objects/__init__.py @@ -16,6 +16,7 @@ from zun.objects import container from zun.objects import container_action from zun.objects import container_pci_requests from zun.objects import image +from zun.objects import network from zun.objects import numa from zun.objects import pci_device from zun.objects import pci_device_pool @@ -31,6 +32,7 @@ Container = container.Container VolumeMapping = volume_mapping.VolumeMapping ZunService = zun_service.ZunService Image = image.Image +Network = network.Network NUMANode = numa.NUMANode NUMATopology = numa.NUMATopology ResourceProvider = resource_provider.ResourceProvider @@ -51,6 +53,7 @@ __all__ = ( 'VolumeMapping', 'ZunService', 'Image', + 'Network', 'ResourceProvider', 'ResourceClass', 'NUMANode', diff --git a/zun/objects/network.py b/zun/objects/network.py new file mode 100644 index 000000000..86b06b258 --- /dev/null +++ b/zun/objects/network.py @@ -0,0 +1,94 @@ +# 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 + + +@base.ZunObjectRegistry.register +class Network(base.ZunPersistentObject, base.ZunObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.IntegerField(), + 'uuid': fields.UUIDField(nullable=True), + 'project_id': fields.StringField(nullable=True), + 'user_id': fields.StringField(nullable=True), + 'name': fields.StringField(nullable=True), + 'network_id': fields.StringField(nullable=True), + 'neutron_net_id': fields.StringField(nullable=True), + } + + @staticmethod + def _from_db_object(network, db_network): + """Converts a database entity to a formal object.""" + for field in network.fields: + setattr(network, field, db_network[field]) + + network.obj_reset_changes() + return network + + @staticmethod + def _from_db_object_list(db_objects, cls, context): + """Converts a list of database entities to a list of formal objects.""" + return [Network._from_db_object(cls(context), obj) + for obj in db_objects] + + @base.remotable_classmethod + def get_by_uuid(cls, context, uuid): + """Find an network based on uuid and return a :class:`Network` object. + + :param uuid: the uuid of a network. + :param context: Security context + :returns: a :class:`Network` object. + """ + db_network = dbapi.get_network_by_uuid(context, uuid) + network = Network._from_db_object(cls(context), db_network) + return network + + @base.remotable + def create(self, context): + """Create a Network 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.: Network(context) + + """ + values = self.obj_get_changes() + db_network = dbapi.create_network(context, values) + self._from_db_object(self, db_network) + + @base.remotable + def save(self, context=None): + """Save updates to this Network. + + 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.: Network(context) + """ + updates = self.obj_get_changes() + dbapi.update_network(context, self.uuid, updates) + + self.obj_reset_changes() diff --git a/zun/tests/unit/api/controllers/test_root.py b/zun/tests/unit/api/controllers/test_root.py index 852d18aee..64bee3282 100644 --- a/zun/tests/unit/api/controllers/test_root.py +++ b/zun/tests/unit/api/controllers/test_root.py @@ -73,6 +73,10 @@ class TestRootController(api_base.FunctionalTest): 'rel': 'self'}, {'href': 'http://localhost/images/', 'rel': 'bookmark'}], + 'networks': [{'href': 'http://localhost/v1/networks/', + 'rel': 'self'}, + {'href': 'http://localhost/networks/', + 'rel': 'bookmark'}], 'capsules': [{'href': 'http://localhost/v1/capsules/', 'rel': 'self'}, {'href': 'http://localhost/capsules/', diff --git a/zun/tests/unit/api/controllers/v1/test_networks.py b/zun/tests/unit/api/controllers/v1/test_networks.py new file mode 100644 index 000000000..c5b14cf1a --- /dev/null +++ b/zun/tests/unit/api/controllers/v1/test_networks.py @@ -0,0 +1,29 @@ +# 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 mock import patch +from zun.tests.unit.api import base as api_base + + +class TestNetworkController(api_base.FunctionalTest): + @patch('zun.common.policy.enforce') + @patch('zun.compute.api.API.network_create') + def test_network_create(self, mock_network_create, mock_policy): + mock_policy.return_value = True + mock_network_create.side_effect = lambda x, y: y + params = ('{"name": "network-test", "neutron_net_id": "test-id"}') + response = self.post('/v1/networks/', + params=params, + content_type='application/json') + + self.assertEqual(202, response.status_int) + self.assertTrue(mock_network_create.called) diff --git a/zun/tests/unit/compute/test_compute_api.py b/zun/tests/unit/compute/test_compute_api.py index a100cd9ec..9cf4ce68b 100644 --- a/zun/tests/unit/compute/test_compute_api.py +++ b/zun/tests/unit/compute/test_compute_api.py @@ -34,6 +34,8 @@ class TestAPI(base.TestCase): self.compute_api = api.API(self.context) self.container = objects.Container( self.context, **utils.get_test_container()) + self.network = objects.Network( + self.context, **utils.get_test_network()) @mock.patch('zun.compute.api.API._record_action_start') @mock.patch('zun.compute.rpcapi.API.container_create') @@ -461,3 +463,9 @@ class TestAPI(base.TestCase): container_actions.NETWORK_DETACH, want_result=False) mock_call.assert_called_once_with( container.host, "network_detach", container=container, network={}) + + @mock.patch('zun.compute.rpcapi.API.network_create') + def test_network_create(self, mock_network_create): + network = self.network + self.compute_api.network_create(self.context, network) + self.assertTrue(mock_network_create.called) diff --git a/zun/tests/unit/compute/test_compute_manager.py b/zun/tests/unit/compute/test_compute_manager.py index d7117b293..320d3af41 100644 --- a/zun/tests/unit/compute/test_compute_manager.py +++ b/zun/tests/unit/compute/test_compute_manager.py @@ -24,6 +24,7 @@ import zun.conf from zun.objects.container import Container from zun.objects.container_action import ContainerActionEvent from zun.objects.image import Image +from zun.objects.network import Network from zun.objects.volume_mapping import VolumeMapping from zun.tests import base from zun.tests.unit.container.fake_driver import FakeDriver as fake_driver @@ -1225,3 +1226,13 @@ class TestManager(base.TestCase): container) mock_is_volume_available.assert_called_once() mock_fail.assert_not_called() + + @mock.patch.object(Network, 'save') + @mock.patch.object(fake_driver, 'create_network') + def test_network_create(self, mock_create, mock_save): + network = Network(self.context, **utils.get_test_network()) + ret = ({'Id': '0eeftestnetwork'}) + mock_create.return_value = ret + self.compute_manager._do_create_network(self.context, network) + mock_create.assert_any_call(self.context, network) + mock_save.assert_called_once() diff --git a/zun/tests/unit/db/utils.py b/zun/tests/unit/db/utils.py index f7c3a9693..0ad8706b2 100644 --- a/zun/tests/unit/db/utils.py +++ b/zun/tests/unit/db/utils.py @@ -565,3 +565,17 @@ def get_test_quota_class(**kwargs): setattr(fake_quota_class, k, v) return fake_quota_class + + +def get_test_network(**kwargs): + return { + 'id': kwargs.get('id', 42), + 'name': kwargs.get('name', 'fake_name'), + 'uuid': kwargs.get('uuid', 'z2b96c5b-242a-41a0-a736-b6e1fada071b'), + 'network_id': kwargs.get('network_id', '0eeftestnetwork'), + 'project_id': kwargs.get('project_id', 'fake_project'), + 'user_id': kwargs.get('user_id', 'fake_user'), + 'created_at': kwargs.get('created_at'), + 'updated_at': kwargs.get('updated_at'), + 'neutron_net_id': kwargs.get('neutron_net_id', 'bar'), + } diff --git a/zun/tests/unit/objects/test_network.py b/zun/tests/unit/objects/test_network.py new file mode 100644 index 000000000..607793c3b --- /dev/null +++ b/zun/tests/unit/objects/test_network.py @@ -0,0 +1,55 @@ +# 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 zun import objects +from zun.tests.unit.db import base +from zun.tests.unit.db import utils + + +class TestNetworkObject(base.DbTestCase): + + def setUp(self): + super(TestNetworkObject, self).setUp() + self.fake_network = utils.get_test_network() + + def test_create(self): + with mock.patch.object(self.dbapi, 'create_network', + autospec=True) as mock_create_network: + mock_create_network.return_value = self.fake_network + network = objects.Network(self.context, **self.fake_network) + network.create(self.context) + mock_create_network.assert_called_once_with(self.context, + self.fake_network) + self.assertEqual(self.context, network._context) + + def test_save(self): + uuid = self.fake_network['uuid'] + with mock.patch.object(self.dbapi, 'get_network_by_uuid', + autospec=True) as mock_get_network: + mock_get_network.return_value = self.fake_network + with mock.patch.object(self.dbapi, 'update_network', + autospec=True) as mock_update_network: + network = objects.Network.get_by_uuid(self.context, uuid) + network.name = 'network-test' + network.neutron_net_id = 'test-id' + network.save() + mock_get_network.assert_called_once_with(self.context, uuid) + params = {'name': 'network-test', 'neutron_net_id': 'test-id'} + mock_update_network.assert_called_once_with(None, + uuid, + params) + self.assertEqual(self.context, network._context) diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index 1e16fdd7a..0adf1153c 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -363,7 +363,8 @@ object_data = { 'ContainerPCIRequest': '1.0-b060f9f9f734bedde79a71a4d3112ee0', 'ContainerPCIRequests': '1.0-7b8f7f044661fe4e24e6949c035af2c4', 'ContainerAction': '1.1-b0c721f9e10c6c0d1e41e512c49eb877', - 'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a' + 'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a', + 'Network': '1.0-235ba13359282107f27c251af9aaffcd', }