Move get_available_network to API layer

In before, if there is no network available, users don't know what is
wrong until the container is scheduled to a compute node and fail.
This commit moves the searching of neutron net to zun-api so that
users will receive an error response earlier if the request is
invalid.

Change-Id: Ic9fd47c6e4aff4131ebaad73a3bed0f51eeb118c
Related-Bug: #1702581
This commit is contained in:
Hongbin Lu
2017-07-12 22:08:16 +00:00
parent a8a503c7d6
commit 019055e56b
8 changed files with 150 additions and 79 deletions

View File

@@ -33,6 +33,7 @@ from zun.common import name_generator
from zun.common import policy
from zun.common import utils
from zun.common import validation
from zun.network import neutron
from zun import objects
LOG = logging.getLogger(__name__)
@@ -234,7 +235,8 @@ class ContainersController(base.Controller):
'"false", True, False, "True" and "False"')
raise exception.InvalidValue(msg)
requested_networks = container_dict.get('nets', [])
nets = container_dict.get('nets', [])
requested_networks = self._build_requested_networks(context, nets)
# Valiadtion accepts 'None' so need to convert it to None
if container_dict.get('image_driver'):
@@ -296,6 +298,20 @@ class ContainersController(base.Controller):
pecan.response.status = 202
return view.format_container(pecan.request.host_url, new_container)
def _build_requested_networks(self, context, nets):
requested_networks = []
if not nets:
# Find an available neutron net and create docker network by
# wrapping the neutron net.
neutron_api = neutron.NeutronAPI(context)
neutron_net = neutron_api.get_available_network()
requested_networks.append({'network': neutron_net['id'],
'port': '',
'v4-fixed-ip': '',
'v6-fixed-ip': ''})
return requested_networks
def _check_security_group(self, context, security_group, container):
if security_group.get("uuid"):
security_group_id = security_group.get("uuid")

View File

@@ -147,7 +147,8 @@ class Manager(periodic_task.PeriodicTasks):
rt = self._get_resource_tracker()
with rt.container_claim(context, container, container.host,
limits):
container = self.driver.create(context, container, image)
container = self.driver.create(context, container, image,
requested_networks)
self._update_task_state(context, container, None)
return container
except exception.DockerError as e:

View File

@@ -20,7 +20,6 @@ from docker import errors
from oslo_log import log as logging
from oslo_utils import timeutils
from zun.common import clients
from zun.common import consts
from zun.common import exception
from zun.common.i18n import _
@@ -106,7 +105,7 @@ class DockerDriver(driver.ContainerDriver):
with docker_utils.docker_client() as docker:
return docker.images(repo, quiet)
def create(self, context, container, image, requested_networks=None):
def create(self, context, container, image, requested_networks):
sandbox_id = container.get_sandbox_id()
network_standalone = False if sandbox_id else True
@@ -116,14 +115,9 @@ class DockerDriver(driver.ContainerDriver):
image = container.image
LOG.debug('Creating container with image %(image)s name %(name)s',
{'image': image, 'name': name})
if requested_networks is None:
if network_standalone:
network = self._provision_network(context, container,
network_api)
requested_networks = [{'network': network['Name'],
'port': '',
'v4-fixed-ip': '',
'v6-fixed-ip': ''}]
if network_standalone:
self._provision_network(context, network_api,
requested_networks)
kwargs = {
'name': self.get_container_name(container),
@@ -170,16 +164,10 @@ class DockerDriver(driver.ContainerDriver):
container.save(context)
return container
def _provision_network(self, context, container, network_api):
LOG.debug('Creating networks for container with image %(image)s '
'name %(name)s',
{'image': container.image, 'name': container.name})
# Find an available neutron net and create docker network by
# wrapping the neutron net.
neutron_net = self._get_available_network(context)
network = self._get_or_create_docker_network(
context, network_api, neutron_net['id'])
return network
def _provision_network(self, context, network_api, requested_networks):
for rq_network in requested_networks:
self._get_or_create_docker_network(
context, network_api, rq_network['network'])
def _setup_network_for_container(self, context, container,
requested_networks, network_api):
@@ -191,10 +179,11 @@ class DockerDriver(driver.ContainerDriver):
network_api.disconnect_container_from_network(container, 'bridge')
addresses = {}
for network in requested_networks:
network_name = network['network']
docker_net_name = self._get_docker_network_name(
context, network['network'])
addrs = network_api.connect_container_to_network(
container, network_name, security_groups=security_group_ids)
addresses[network_name] = addrs
container, docker_net_name, security_groups=security_group_ids)
addresses[docker_net_name] = addrs
return addresses
@@ -641,17 +630,11 @@ class DockerDriver(driver.ContainerDriver):
value = six.text_type(value)
return value.encode('utf-8')
def create_sandbox(self, context, container, image='kubernetes/pause',
requested_networks=None):
def create_sandbox(self, context, container, requested_networks,
image='kubernetes/pause'):
with docker_utils.docker_client() as docker:
network_api = zun_network.api(context=context, docker_api=docker)
if not requested_networks:
network = self._provision_network(context, container,
network_api)
requested_networks = [{'network': network['Name'],
'port': '',
'v4-fixed-ip': '',
'v6-fixed-ip': ''}]
self._provision_network(context, network_api, requested_networks)
name = self.get_sandbox_name(container)
sandbox = docker.create_container(image, name=name,
hostname=name[:63])
@@ -668,21 +651,12 @@ class DockerDriver(driver.ContainerDriver):
docker.start(sandbox['Id'])
return sandbox['Id']
def _get_available_network(self, context):
neutron = clients.OpenStackClients(context).neutron()
search_opts = {'tenant_id': context.project_id, 'shared': False}
nets = neutron.list_networks(**search_opts).get('networks', [])
if not nets:
raise exception.ZunException(_(
"There is no neutron network available"))
nets.sort(key=lambda x: x['created_at'])
return nets[0]
def _get_or_create_docker_network(self, context, network_api,
neutron_net_id):
# Append project_id to the network name to avoid name collision
# across projects.
docker_net_name = neutron_net_id + '-' + context.project_id
docker_net_name = self._get_docker_network_name(context,
neutron_net_id)
docker_networks = network_api.list_networks(names=[docker_net_name])
if not docker_networks:
network_api.create_network(neutron_net_id=neutron_net_id,
@@ -690,7 +664,10 @@ class DockerDriver(driver.ContainerDriver):
docker_networks = network_api.list_networks(
names=[docker_net_name])
return docker_networks[0]
def _get_docker_network_name(self, context, neutron_net_id):
# Append project_id to the network name to avoid name collision
# across projects.
return neutron_net_id + '-' + context.project_id
def delete_sandbox(self, context, container):
sandbox_id = container.get_sandbox_id()

View File

@@ -60,7 +60,7 @@ def load_container_driver(container_driver=None):
class ContainerDriver(object):
"""Base class for container drivers."""
def create(self, context, container, sandbox_name=None):
def create(self, context, container, **kwargs):
"""Create a container."""
raise NotImplementedError()
@@ -149,7 +149,7 @@ class ContainerDriver(object):
"""Display stats of the container."""
raise NotImplementedError()
def create_sandbox(self, context, container, **kwargs):
def create_sandbox(self, context, *args, **kwargs):
"""Create a sandbox."""
raise NotImplementedError()

31
zun/network/neutron.py Normal file
View File

@@ -0,0 +1,31 @@
# 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.common import clients
from zun.common import exception
from zun.common.i18n import _
class NeutronAPI(object):
def __init__(self, context):
self.context = context
def get_available_network(self):
neutron = clients.OpenStackClients(self.context).neutron()
search_opts = {'tenant_id': self.context.project_id, 'shared': False}
nets = neutron.list_networks(**search_opts).get('networks', [])
if not nets:
raise exception.Conflict(_(
"There is no neutron network available"))
nets.sort(key=lambda x: x['created_at'])
return nets[0]

View File

@@ -25,9 +25,11 @@ from zun.tests.unit.objects import utils as obj_utils
class TestContainerController(api_base.FunctionalTest):
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_run')
@patch('zun.compute.api.API.image_search')
def test_run_container(self, mock_search, mock_container_run):
def test_run_container(self, mock_search, mock_container_run,
mock_neutron_get_network):
mock_container_run.side_effect = lambda x, y, z, v: y
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -39,6 +41,7 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual(202, response.status_int)
self.assertTrue(mock_container_run.called)
mock_neutron_get_network.assert_called_once()
@patch('zun.compute.api.API.container_run')
@patch('zun.compute.api.API.image_search')
@@ -52,10 +55,12 @@ class TestContainerController(api_base.FunctionalTest):
self.app.post('/v1/containers?run=xyz', params=params,
content_type='application/json')
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.rpcapi.API.container_run')
@patch('zun.compute.rpcapi.API.image_search')
def test_run_container_with_false(self, mock_search,
mock_container_run):
mock_container_run,
mock_neutron_get_network):
mock_container_run.side_effect = lambda x, y, z, v: y
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -66,6 +71,7 @@ class TestContainerController(api_base.FunctionalTest):
content_type='application/json')
self.assertEqual(202, response.status_int)
self.assertFalse(mock_container_run.called)
mock_neutron_get_network.assert_called_once()
@patch('zun.compute.rpcapi.API.container_run')
@patch('zun.compute.rpcapi.API.image_search')
@@ -79,9 +85,11 @@ class TestContainerController(api_base.FunctionalTest):
params=params, content_type='application/json')
self.assertTrue(mock_container_run.not_called)
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container(self, mock_search, mock_container_create):
def test_create_container(self, mock_search, mock_container_create,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -93,6 +101,7 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual(202, response.status_int)
self.assertTrue(mock_container_create.called)
mock_neutron_get_network.assert_called_once()
@patch('zun.compute.api.API.container_create')
def test_create_container_image_not_specified(self, mock_container_create):
@@ -107,10 +116,12 @@ class TestContainerController(api_base.FunctionalTest):
content_type='application/json')
self.assertTrue(mock_container_create.not_called)
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_image_not_found(self, mock_search,
mock_container_create):
mock_container_create,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
mock_search.side_effect = exception.ImageNotFound()
@@ -119,11 +130,14 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual('application/json', response.content_type)
self.assertEqual(404, response.status_int)
self.assertFalse(mock_container_create.called)
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_set_project_id_and_user_id(
self, mock_search, mock_container_create):
self, mock_search, mock_container_create,
mock_neutron_get_network):
def _create_side_effect(cnxt, container, extra_spec, networks):
self.assertEqual(self.context.project_id, container.project_id)
self.assertEqual(self.context.user_id, container.user_id)
@@ -136,11 +150,14 @@ class TestContainerController(api_base.FunctionalTest):
self.app.post('/v1/containers/',
params=params,
content_type='application/json')
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_resp_has_status_reason(self, mock_search,
mock_container_create):
mock_container_create,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -151,7 +168,9 @@ class TestContainerController(api_base.FunctionalTest):
content_type='application/json')
self.assertEqual(202, response.status_int)
self.assertIn('status_reason', response.json.keys())
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_show')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.container_delete')
@@ -159,7 +178,8 @@ class TestContainerController(api_base.FunctionalTest):
def test_create_container_with_command(self, mock_search,
mock_container_delete,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -200,13 +220,16 @@ class TestContainerController(api_base.FunctionalTest):
c = response.json['containers']
self.assertEqual(0, len(c))
self.assertTrue(mock_container_create.called)
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_show')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_without_memory(self, mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -231,13 +254,16 @@ class TestContainerController(api_base.FunctionalTest):
self.assertIsNone(c.get('memory'))
self.assertEqual({"key1": "val1", "key2": "val2"},
c.get('environment'))
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_show')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_without_environment(self, mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -260,13 +286,16 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual('Stopped', c.get('status'))
self.assertEqual('512M', c.get('memory'))
self.assertEqual({}, c.get('environment'))
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_show')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_without_name(self, mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
# No name param
mock_container_create.side_effect = lambda x, y, z, v: y
params = ('{"image": "ubuntu", "command": "env", "memory": "512",'
@@ -290,7 +319,9 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual('512M', c.get('memory'))
self.assertEqual({"key1": "val1", "key2": "val2"},
c.get('environment'))
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.rpcapi.API.container_show')
@patch('zun.compute.rpcapi.API.container_create')
@patch('zun.compute.api.API.image_search')
@@ -298,7 +329,8 @@ class TestContainerController(api_base.FunctionalTest):
self,
mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -324,7 +356,9 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual('512M', c.get('memory'))
self.assertEqual({"Name": "no", "MaximumRetryCount": "0"},
c.get('restart_policy'))
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.rpcapi.API.container_show')
@patch('zun.compute.rpcapi.API.container_create')
@patch('zun.compute.api.API.image_search')
@@ -332,7 +366,8 @@ class TestContainerController(api_base.FunctionalTest):
self,
mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -358,7 +393,9 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual('512M', c.get('memory'))
self.assertEqual({"Name": "no", "MaximumRetryCount": "0"},
c.get('restart_policy'))
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.rpcapi.API.container_show')
@patch('zun.compute.rpcapi.API.container_create')
@patch('zun.compute.api.API.image_search')
@@ -366,7 +403,8 @@ class TestContainerController(api_base.FunctionalTest):
self,
mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -391,7 +429,9 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual('512M', c.get('memory'))
self.assertEqual({"Name": "no", "MaximumRetryCount": "0"},
c.get('restart_policy'))
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.rpcapi.API.container_show')
@patch('zun.compute.rpcapi.API.container_create')
@patch('zun.compute.api.API.image_search')
@@ -399,7 +439,8 @@ class TestContainerController(api_base.FunctionalTest):
self,
mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -425,7 +466,9 @@ class TestContainerController(api_base.FunctionalTest):
self.assertEqual('512M', c.get('memory'))
self.assertEqual({"Name": "unless-stopped", "MaximumRetryCount": "0"},
c.get('restart_policy'))
mock_neutron_get_network.assert_called_once()
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.rpcapi.API.container_show')
@patch('zun.compute.rpcapi.API.container_create')
@patch('zun.compute.api.API.image_search')
@@ -433,7 +476,8 @@ class TestContainerController(api_base.FunctionalTest):
self,
mock_search,
mock_container_create,
mock_container_show):
mock_container_show,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'
@@ -446,6 +490,7 @@ class TestContainerController(api_base.FunctionalTest):
params=params,
content_type='application/json')
self.assertTrue(mock_container_create.not_called)
mock_neutron_get_network.assert_called_once()
@patch('zun.compute.rpcapi.API.container_create')
@patch('zun.compute.rpcapi.API.image_search')
@@ -1099,10 +1144,12 @@ class TestContainerController(api_base.FunctionalTest):
container_uuid, params)
self.assertFalse(mock_container_kill.called)
@patch('zun.network.neutron.NeutronAPI.get_available_network')
@patch('zun.compute.api.API.container_create')
@patch('zun.compute.api.API.image_search')
def test_create_container_resp_has_image_driver(self, mock_search,
mock_container_create):
mock_container_create,
mock_neutron_get_network):
mock_container_create.side_effect = lambda x, y, z, v: y
# Create a container with a command
params = ('{"name": "MyDocker", "image": "ubuntu",'

View File

@@ -67,7 +67,8 @@ class TestManager(base.TestCase):
mock_save.assert_called_with(self.context)
mock_pull.assert_any_call(self.context, container.image, 'latest',
'always', 'glance')
mock_create.assert_called_once_with(self.context, container, image)
mock_create.assert_called_once_with(self.context, container, image,
networks)
@mock.patch.object(Container, 'save')
@mock.patch('zun.image.driver.pull_image')
@@ -145,7 +146,8 @@ class TestManager(base.TestCase):
mock_save.assert_called_with(self.context)
mock_pull.assert_any_call(self.context, container.image, 'latest',
'always', 'glance')
mock_create.assert_called_once_with(self.context, container, image)
mock_create.assert_called_once_with(self.context, container, image,
networks)
mock_start.assert_called_once_with(self.context, container)
@mock.patch.object(Container, 'save')
@@ -233,7 +235,7 @@ class TestManager(base.TestCase):
mock_pull.assert_any_call(self.context, container.image, 'latest',
'always', 'glance')
mock_create.assert_called_once_with(
self.context, container, {'name': 'nginx', 'path': None})
self.context, container, {'name': 'nginx', 'path': None}, networks)
@mock.patch.object(compute_node_tracker.ComputeNodeTracker,
'remove_usage_from_container')

View File

@@ -92,9 +92,8 @@ class TestDockerDriver(base.DriverTestCase):
return_value={'Id': 'val1', 'key1': 'val2'})
image = {'path': ''}
mock_container = self.mock_default_container
with mock.patch.object(self.driver, '_get_available_network'):
result_container = self.driver.create(self.context, mock_container,
image, None)
result_container = self.driver.create(self.context, mock_container,
image, [])
host_config = {}
host_config['mem_limit'] = '512m'
host_config['cpu_quota'] = 100000
@@ -334,10 +333,9 @@ class TestDockerDriver(base.DriverTestCase):
return_value={'Id': 'val1', 'key1': 'val2'})
mock_container = mock.MagicMock()
requested_networks = []
with mock.patch.object(self.driver, '_get_available_network'):
result_sandbox_id = self.driver.create_sandbox(
self.context, mock_container, 'kubernetes/pause',
requested_networks=requested_networks)
result_sandbox_id = self.driver.create_sandbox(
self.context, mock_container, requested_networks,
'kubernetes/pause')
self.mock_docker.create_container.assert_called_once_with(
'kubernetes/pause', name=sandbox_name, hostname=sandbox_name)
self.assertEqual(result_sandbox_id, 'val1')
@@ -354,11 +352,10 @@ class TestDockerDriver(base.DriverTestCase):
self.mock_docker.create_container = mock.Mock(
return_value={'Id': 'val1', 'key1': 'val2'})
mock_container = mock.MagicMock()
with mock.patch.object(self.driver, '_get_available_network'):
requested_networks = []
result_sandbox_id = self.driver.create_sandbox(
self.context, mock_container, 'kubernetes/pause',
requested_networks=requested_networks)
requested_networks = []
result_sandbox_id = self.driver.create_sandbox(
self.context, mock_container, requested_networks,
'kubernetes/pause')
self.mock_docker.create_container.assert_called_once_with(
'kubernetes/pause', name=sandbox_name, hostname=sandbox_name[:63])
self.assertEqual(result_sandbox_id, 'val1')