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
This commit is contained in:
caishan 2018-04-27 08:05:31 -07:00 committed by Hongbin LU
parent c714b11fdd
commit 5a43580a70
24 changed files with 581 additions and 4 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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
}

View File

@ -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()))

View File

@ -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_.-]'
}

View File

@ -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'
}
]

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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'
)

View File

@ -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)

View File

@ -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))

View File

@ -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',

94
zun/objects/network.py Normal file
View File

@ -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()

View File

@ -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/',

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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'),
}

View File

@ -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)

View File

@ -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',
}