diff --git a/zun/common/exception.py b/zun/common/exception.py index a95c26af4..3cd043da6 100644 --- a/zun/common/exception.py +++ b/zun/common/exception.py @@ -389,6 +389,11 @@ class NetworkNotFound(HTTPNotFound): class NetworkAlreadyExists(ResourceExists): message = _("A network with %(field)s %(value)s already exists.") + def __init__(self, field, value): + self.field = field + self.value = value + super(NetworkAlreadyExists, self).__init__(field=field, value=value) + class PortNotFound(HTTPNotFound): message = _("Neutron port %(port)s could not be found.") diff --git a/zun/db/api.py b/zun/db/api.py index 4801c5d05..ae5f36dfe 100644 --- a/zun/db/api.py +++ b/zun/db/api.py @@ -1081,6 +1081,28 @@ def get_network_by_uuid(context, network_uuid): return _get_dbdriver_instance().get_network_by_uuid(context, network_uuid) +@profiler.trace("db") +def list_networks(context, filters=None, limit=None, marker=None, + sort_key=None, sort_dir=None): + """List matching networks. + + Return a list of the specified columns for all networks that match + the specified filters. + + :param context: The security context + :param filters: Filters to apply. Defaults to None. + :param limit: Maximum number of networks 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 tuples of the specified columns. + """ + return _get_dbdriver_instance().list_networks( + context, filters, limit, marker, sort_key, sort_dir) + + @profiler.trace("db") def update_network(context, uuid, values): """Update properties of a network. diff --git a/zun/db/sqlalchemy/api.py b/zun/db/sqlalchemy/api.py index f8434da13..9e079b267 100644 --- a/zun/db/sqlalchemy/api.py +++ b/zun/db/sqlalchemy/api.py @@ -1250,6 +1250,19 @@ class Connection(object): return result + def _add_networks_filters(self, query, filters): + filter_names = ['name', 'neutron_net_id', 'project_id', 'user_id'] + return self._add_filters(query, models.Network, filters=filters, + filter_names=filter_names) + + def list_networks(self, context, filters=None, limit=None, + marker=None, sort_key=None, sort_dir=None): + query = model_query(models.Network) + query = self._add_project_filters(context, query) + query = self._add_networks_filters(query, filters) + return _paginate_query(models.Network, limit, marker, + sort_key, sort_dir, query) + def create_network(self, context, values): # ensure defaults are present for new networks if not values.get('uuid'): diff --git a/zun/network/kuryr_network.py b/zun/network/kuryr_network.py index 3ef21fdd0..9dd5d06ca 100644 --- a/zun/network/kuryr_network.py +++ b/zun/network/kuryr_network.py @@ -118,7 +118,15 @@ class KuryrNetwork(network.Network): # which will guarantee only one request can create the network in here # (and call docker.create_network later) if there are concurrent # requests on creating networks for the same neutron net. - network.create(self.context) + try: + network.create(self.context) + except exception.NetworkAlreadyExists as e: + if e.field == 'neutron_net_id': + network = objects.Network.list( + self.context, + filters={'neutron_net_id': network.neutron_net_id})[0] + else: + raise LOG.debug("Calling docker.create_network to create network %s, " "ipam_options %s, options %s", name, ipam_options, options) diff --git a/zun/objects/network.py b/zun/objects/network.py index ffa8b15d5..b5fede4ed 100644 --- a/zun/objects/network.py +++ b/zun/objects/network.py @@ -75,6 +75,25 @@ class Network(base.ZunPersistentObject, base.ZunObject): db_network = dbapi.create_network(context, values) self._from_db_object(self, db_network) + @base.remotable_classmethod + def list(cls, context, limit=None, marker=None, + sort_key=None, sort_dir=None, filters=None): + """Return a list of Network objects. + + :param context: Security context. + :param limit: maximum number of resources to return in a single result. + :param marker: pagination marker for large data sets. + :param sort_key: column to sort results by. + :param sort_dir: direction to sort. "asc" or "desc". + :param filters: filters when list networks. + :returns: a list of :class:`Network` object. + + """ + db_networks = dbapi.list_networks( + context, limit=limit, marker=marker, sort_key=sort_key, + sort_dir=sort_dir, filters=filters) + return Network._from_db_object_list(db_networks, cls, context) + @base.remotable def save(self, context=None): """Save updates to this Network. diff --git a/zun/tests/unit/db/test_network.py b/zun/tests/unit/db/test_network.py index 20eaed983..ff29bccde 100644 --- a/zun/tests/unit/db/test_network.py +++ b/zun/tests/unit/db/test_network.py @@ -10,8 +10,12 @@ # 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 @@ -36,3 +40,16 @@ class DbNetworkTestCase(base.DbTestCase): 'A network with neutron_net_id 456.*'): utils.create_test_network(context=self.context, neutron_net_id='456') + + def test_list_networks(self): + uuids = [] + for i in range(1, 6): + network = utils.create_test_network( + uuid=uuidutils.generate_uuid(), + context=self.context, + neutron_net_id=uuidutils.generate_uuid(), + name='network' + str(i)) + uuids.append(six.text_type(network['uuid'])) + res = dbapi.list_networks(self.context) + res_uuids = [r.uuid for r in res] + self.assertEqual(sorted(uuids), sorted(res_uuids)) diff --git a/zun/tests/unit/network/test_kuryr_network.py b/zun/tests/unit/network/test_kuryr_network.py index 78b56652e..f38357c5e 100644 --- a/zun/tests/unit/network/test_kuryr_network.py +++ b/zun/tests/unit/network/test_kuryr_network.py @@ -194,6 +194,36 @@ class KuryrNetworkTestCase(base.TestCase): 'neutron.net.shared': 'False', 'neutron.subnet.uuid': 'fake-subnet-id'}) + @mock.patch.object(Network, 'create') + @mock.patch.object(Network, 'save') + @mock.patch.object(Network, 'list') + @mock.patch('zun.network.neutron.NeutronAPI') + def test_create_network_already_exist( + self, mock_neutron_api_cls, mock_list, mock_save, mock_create): + mock_neutron_api_cls.return_value = self.network_api.neutron_api + name = 'test_kuryr_network' + neutron_net_id = 'fake-net-id' + mock_create.side_effect = exception.NetworkAlreadyExists( + field='neutron_net_id', value=neutron_net_id) + with mock.patch.object(self.network_api.docker, 'create_network', + return_value={'Id': 'docker-net'} + ) as mock_create_network: + network = self.network_api.create_network(name, neutron_net_id) + self.assertEqual('docker-net', network.network_id) + mock_list.assert_called_once_with( + self.context, filters={'neutron_net_id': neutron_net_id}) + mock_create_network.assert_called_once_with( + name=name, + driver='kuryr', + enable_ipv6=False, + ipam={'Config': [{'Subnet': '10.5.0.0/16', 'Gateway': '10.5.0.1'}], + 'Driver': 'kuryr', + 'Options': {'neutron.net.shared': 'False', + 'neutron.subnet.uuid': 'fake-subnet-id'}}, + options={'neutron.net.uuid': 'fake-net-id', + 'neutron.net.shared': 'False', + 'neutron.subnet.uuid': 'fake-subnet-id'}) + def test_remove_network(self): network_name = 'c02afe4e-8350-4263-8078' self.network_api.remove_network(network_name) diff --git a/zun/tests/unit/objects/test_network.py b/zun/tests/unit/objects/test_network.py index 607793c3b..49c367445 100644 --- a/zun/tests/unit/objects/test_network.py +++ b/zun/tests/unit/objects/test_network.py @@ -15,6 +15,8 @@ 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 @@ -53,3 +55,13 @@ class TestNetworkObject(base.DbTestCase): uuid, params) self.assertEqual(self.context, network._context) + + def test_list(self): + with mock.patch.object(self.dbapi, 'list_networks', + autospec=True) as mock_get_list: + mock_get_list.return_value = [self.fake_network] + networks = objects.Network.list(self.context) + self.assertEqual(1, mock_get_list.call_count) + self.assertThat(networks, HasLength(1)) + self.assertIsInstance(networks[0], objects.Network) + self.assertEqual(self.context, networks[0]._context) diff --git a/zun/tests/unit/objects/test_objects.py b/zun/tests/unit/objects/test_objects.py index dddb5141e..a98361044 100644 --- a/zun/tests/unit/objects/test_objects.py +++ b/zun/tests/unit/objects/test_objects.py @@ -365,7 +365,7 @@ object_data = { 'ContainerPCIRequests': '1.0-7b8f7f044661fe4e24e6949c035af2c4', 'ContainerAction': '1.1-b0c721f9e10c6c0d1e41e512c49eb877', 'ContainerActionEvent': '1.0-2974d0a6f5d4821fd4e223a88c10181a', - 'Network': '1.1-f57547d39a95cf36f2b026aa4a863879', + 'Network': '1.1-26e8d37a54e5fc905ede657744a221d9', 'ExecInstance': '1.0-59464e7b96db847c0abb1e96d3cec30a', }