From e134f81d85f42b87238f7786b0b50066129c968d Mon Sep 17 00:00:00 2001 From: Dan Smith Date: Mon, 13 Jan 2014 13:32:37 -0800 Subject: [PATCH] Add Network object This adds a Network object to provide services and modeling for nova-network and its use of the current SQLAlchemy Network model. Related to blueprint nova-network-objects Change-Id: I7f943cda32fc98b3b03cdbb5d68e0c741053afaa --- nova/objects/__init__.py | 1 + nova/objects/network.py | 182 ++++++++++++++++++++++++++ nova/tests/objects/test_network.py | 203 +++++++++++++++++++++++++++++ 3 files changed, 386 insertions(+) create mode 100644 nova/objects/network.py create mode 100644 nova/tests/objects/test_network.py diff --git a/nova/objects/__init__.py b/nova/objects/__init__.py index d62752ffdc2d..6ac6ea2d0d50 100644 --- a/nova/objects/__init__.py +++ b/nova/objects/__init__.py @@ -24,3 +24,4 @@ def register_all(): __import__('nova.objects.migration') __import__('nova.objects.quotas') __import__('nova.objects.virtual_interface') + __import__('nova.objects.network') diff --git a/nova/objects/network.py b/nova/objects/network.py new file mode 100644 index 000000000000..e584db72dba8 --- /dev/null +++ b/nova/objects/network.py @@ -0,0 +1,182 @@ +# Copyright 2014 Red Hat, Inc. +# +# 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 netaddr + +from nova import db +from nova import exception +from nova.objects import base as obj_base +from nova.objects import fields + + +class Network(obj_base.NovaPersistentObject, obj_base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.IntegerField(), + 'label': fields.StringField(), + 'injected': fields.BooleanField(), + 'cidr': fields.IPV4NetworkField(nullable=True), + 'cidr_v6': fields.IPV6NetworkField(nullable=True), + 'multi_host': fields.BooleanField(), + 'netmask': fields.IPV4AddressField(nullable=True), + 'gateway': fields.IPV4AddressField(nullable=True), + 'broadcast': fields.IPV4AddressField(nullable=True), + 'netmask_v6': fields.IPV6AddressField(nullable=True), + 'gateway_v6': fields.IPV6AddressField(nullable=True), + 'bridge': fields.StringField(nullable=True), + 'bridge_interface': fields.StringField(nullable=True), + 'dns1': fields.IPAddressField(nullable=True), + 'dns2': fields.IPAddressField(nullable=True), + 'vlan': fields.IntegerField(nullable=True), + 'vpn_public_address': fields.IPAddressField(nullable=True), + 'vpn_public_port': fields.IntegerField(nullable=True), + 'vpn_private_address': fields.IPAddressField(nullable=True), + 'dhcp_start': fields.IPV4AddressField(nullable=True), + 'rxtx_base': fields.IntegerField(nullable=True), + 'project_id': fields.UUIDField(nullable=True), + 'priority': fields.IntegerField(nullable=True), + 'host': fields.StringField(nullable=True), + 'uuid': fields.UUIDField(), + } + + @staticmethod + def _convert_legacy_ipv6_netmask(netmask): + """Handle netmask_v6 possibilities from the database. + + Historically, this was stored as just an integral CIDR prefix, + but in the future it should be stored as an actual netmask. + Be tolerant of either here. + """ + try: + prefix = int(netmask) + return netaddr.IPNetwork('1::/%i' % prefix).netmask + except ValueError: + pass + + try: + return netaddr.IPNetwork(netmask).netmask + except netaddr.AddrFormatError: + raise ValueError('IPv6 netmask "%s" must be a netmask ' + 'or integral prefix' % netmask) + + @staticmethod + def _from_db_object(context, network, db_network): + for field in network.fields: + db_value = db_network[field] + if field is 'netmask_v6' and db_value is not None: + db_value = network._convert_legacy_ipv6_netmask(db_value) + network[field] = db_value + network._context = context + network.obj_reset_changes() + return network + + @obj_base.remotable_classmethod + def get_by_id(cls, context, network_id, project_only='allow_none'): + db_network = db.network_get(context, network_id, + project_only=project_only) + return cls._from_db_object(context, cls(), db_network) + + @obj_base.remotable_classmethod + def get_by_uuid(cls, context, network_uuid): + db_network = db.network_get_by_uuid(context, network_uuid) + return cls._from_db_object(context, cls(), db_network) + + @obj_base.remotable_classmethod + def get_by_cidr(cls, context, cidr): + db_network = db.network_get_by_cidr(context, cidr) + return cls._from_db_object(context, cls(), db_network) + + @obj_base.remotable_classmethod + def associate(cls, context, project_id, network_id=None, force=False): + db.network_associate(context, project_id, network_id=network_id, + force=force) + + @obj_base.remotable_classmethod + def disassociate(cls, context, network_id, host=False, project=False): + db.network_disassociate(context, network_id, host, project) + + def _get_primitive_changes(self): + changes = {} + for key, value in self.obj_get_changes().items(): + if isinstance(value, netaddr.IPAddress): + changes[key] = str(value) + else: + changes[key] = value + return changes + + @obj_base.remotable + def create(self, context): + updates = self._get_primitive_changes() + if 'id' in updates: + raise exception.ObjectActionError(action='create', + reason='already created') + db_network = db.network_create_safe(context, updates) + self._from_db_object(context, self, db_network) + + @obj_base.remotable + def destroy(self, context): + db.network_delete_safe(context, self.id) + self.deleted = True + self.obj_reset_changes(['deleted']) + + @obj_base.remotable + def save(self, context): + updates = self._get_primitive_changes() + if 'netmask_v6' in updates: + # NOTE(danms): For some reason, historical code stores the + # IPv6 netmask as just the CIDR mask length, so convert that + # back here before saving for now. + updates['netmask_v6'] = netaddr.IPNetwork( + updates['netmask_v6']).netmask + set_host = 'host' in updates + if set_host: + db.network_set_host(context, self.id, updates.pop('host')) + if updates: + db_network = db.network_update(context, self.id, updates) + elif set_host: + db_network = db.network_get(context, self.id) + else: + db_network = None + if db_network is not None: + self._from_db_object(context, self, db_network) + + +class NetworkList(obj_base.ObjectListBase, obj_base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objects': fields.ListOfObjectsField('Network'), + } + child_versions = { + '1.0': '1.0', + } + + @obj_base.remotable_classmethod + def get_all(cls, context, project_only='allow_none'): + db_networks = db.network_get_all(context, project_only) + return obj_base.obj_make_list(context, cls(), Network, db_networks) + + @obj_base.remotable_classmethod + def get_by_uuids(cls, context, network_uuids, project_only='allow_none'): + db_networks = db.network_get_all_by_uuids(context, network_uuids, + project_only) + return obj_base.obj_make_list(context, cls(), Network, db_networks) + + @obj_base.remotable_classmethod + def get_by_host(cls, context, host): + db_networks = db.network_get_all_by_host(context, host) + return obj_base.obj_make_list(context, cls(), Network, db_networks) diff --git a/nova/tests/objects/test_network.py b/nova/tests/objects/test_network.py new file mode 100644 index 000000000000..8b0389b3fd62 --- /dev/null +++ b/nova/tests/objects/test_network.py @@ -0,0 +1,203 @@ +# Copyright 2014 Red Hat, Inc. +# +# 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 +import netaddr + +from nova.objects import network as network_obj +from nova.tests.objects import test_objects + + +fake_network = { + 'deleted': False, + 'created_at': None, + 'updated_at': None, + 'deleted_at': None, + 'id': 1, + 'label': 'Fake Network', + 'injected': False, + 'cidr': '192.168.1.0/24', + 'cidr_v6': '1234::/64', + 'multi_host': False, + 'netmask': '255.255.255.0', + 'gateway': '192.168.1.1', + 'broadcast': '192.168.1.255', + 'netmask_v6': 64, + 'gateway_v6': '1234::1', + 'bridge': 'br100', + 'bridge_interface': 'eth0', + 'dns1': '8.8.8.8', + 'dns2': '8.8.4.4', + 'vlan': None, + 'vpn_public_address': None, + 'vpn_public_port': None, + 'vpn_private_address': None, + 'dhcp_start': '192.168.1.10', + 'rxtx_base': None, + 'project_id': None, + 'priority': None, + 'host': None, + 'uuid': 'fake-uuid', +} + + +class _TestNetworkObject(object): + def _compare(self, obj, db_obj): + for field in obj.fields: + db_val = db_obj[field] + obj_val = obj[field] + if isinstance(obj_val, netaddr.IPAddress): + obj_val = str(obj_val) + if isinstance(obj_val, netaddr.IPNetwork): + obj_val = str(obj_val) + if field == 'netmask_v6': + db_val = str(netaddr.IPNetwork('1::/%i' % db_val).netmask) + self.assertEqual(db_val, obj_val) + + @mock.patch('nova.db.network_get') + def test_get_by_id(self, get): + get.return_value = fake_network + network = network_obj.Network.get_by_id(self.context, 'foo') + self._compare(network, fake_network) + get.assert_called_once_with(self.context, 'foo', + project_only='allow_none') + + @mock.patch('nova.db.network_get_by_uuid') + def test_get_by_uuid(self, get): + get.return_value = fake_network + network = network_obj.Network.get_by_uuid(self.context, 'foo') + self._compare(network, fake_network) + get.assert_called_once_with(self.context, 'foo') + + @mock.patch('nova.db.network_get_by_cidr') + def test_get_by_cidr(self, get): + get.return_value = fake_network + network = network_obj.Network.get_by_cidr(self.context, + '192.168.1.0/24') + self._compare(network, fake_network) + get.assert_called_once_with(self.context, '192.168.1.0/24') + + @mock.patch('nova.db.network_update') + @mock.patch('nova.db.network_set_host') + def test_save(self, set_host, update): + result = dict(fake_network, injected=True) + network = network_obj.Network._from_db_object(self.context, + network_obj.Network(), + fake_network) + network.obj_reset_changes() + network.save() + network.label = 'bar' + update.return_value = result + network.save() + update.assert_called_once_with(self.context, network.id, + {'label': 'bar'}) + self.assertFalse(set_host.called) + self._compare(network, result) + + @mock.patch('nova.db.network_update') + @mock.patch('nova.db.network_set_host') + @mock.patch('nova.db.network_get') + def test_save_with_host(self, get, set_host, update): + result = dict(fake_network, injected=True) + network = network_obj.Network._from_db_object(self.context, + network_obj.Network(), + fake_network) + network.obj_reset_changes() + network.host = 'foo' + get.return_value = result + network.save() + set_host.assert_called_once_with(self.context, network.id, 'foo') + self.assertFalse(update.called) + self._compare(network, result) + + @mock.patch('nova.db.network_update') + @mock.patch('nova.db.network_set_host') + def test_save_with_host_and_other(self, set_host, update): + result = dict(fake_network, injected=True) + network = network_obj.Network._from_db_object(self.context, + network_obj.Network(), + fake_network) + network.obj_reset_changes() + network.host = 'foo' + network.label = 'bar' + update.return_value = result + network.save() + set_host.assert_called_once_with(self.context, network.id, 'foo') + update.assert_called_once_with(self.context, network.id, + {'label': 'bar'}) + self._compare(network, result) + + @mock.patch('nova.db.network_associate') + def test_associate(self, associate): + network_obj.Network.associate(self.context, 'project', + network_id=123) + associate.assert_called_once_with(self.context, 'project', + network_id=123, force=False) + + @mock.patch('nova.db.network_disassociate') + def test_disassociate(self, disassociate): + network_obj.Network.disassociate(self.context, 123, + host=True, project=True) + disassociate.assert_called_once_with(self.context, 123, True, True) + + @mock.patch('nova.db.network_create_safe') + def test_create(self, create): + create.return_value = fake_network + network = network_obj.Network(context=self.context, label='foo') + network.create() + create.assert_called_once_with(self.context, {'label': 'foo'}) + self._compare(network, fake_network) + + @mock.patch('nova.db.network_delete_safe') + def test_destroy(self, delete): + network = network_obj.Network(context=self.context, id=123) + network.destroy() + delete.assert_called_once_with(self.context, 123) + self.assertTrue(network.deleted) + self.assertNotIn('deleted', network.obj_what_changed()) + + @mock.patch('nova.db.network_get_all') + def test_get_all(self, get_all): + get_all.return_value = [fake_network] + networks = network_obj.NetworkList.get_all(self.context) + self.assertEqual(1, len(networks)) + get_all.assert_called_once_with(self.context, 'allow_none') + self._compare(networks[0], fake_network) + + @mock.patch('nova.db.network_get_all_by_uuids') + def test_get_all_by_uuids(self, get_all): + get_all.return_value = [fake_network] + networks = network_obj.NetworkList.get_by_uuids(self.context, + ['foo']) + self.assertEqual(1, len(networks)) + get_all.assert_called_once_with(self.context, ['foo'], 'allow_none') + self._compare(networks[0], fake_network) + + @mock.patch('nova.db.network_get_all_by_host') + def test_get_all_by_host(self, get_all): + get_all.return_value = [fake_network] + networks = network_obj.NetworkList.get_by_host(self.context, 'host') + self.assertEqual(1, len(networks)) + get_all.assert_called_once_with(self.context, 'host') + self._compare(networks[0], fake_network) + + +class TestNetworkObject(test_objects._LocalTest, + _TestNetworkObject): + pass + + +class TestRemoteNetworkObject(test_objects._RemoteTest, + _TestNetworkObject): + pass