From ff25b8eb3bff5b162fc112a694f3838928f524ec Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 24 Mar 2015 19:07:31 +0000 Subject: [PATCH] Add methods for logical router management Adds API methods to list existing routers, create a new logical router, update an existing router, and delete an existing logical router by name or UUID. It is considered an error if more than one router with the same name exists and you attempt to delete by name. Also... MOAR TESTS!!! ZOMG Change-Id: Ie6ea4eb5f2322bdda07e6db87e2cdbabea492ee9 --- shade/__init__.py | 100 +++++++++++++++++++++++++++++++++ shade/tests/unit/test_shade.py | 70 ++++++++++++++++++++++- test-requirements.txt | 1 + 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/shade/__init__.py b/shade/__init__.py index db28a15c7..51f0d9649 100644 --- a/shade/__init__.py +++ b/shade/__init__.py @@ -652,6 +652,15 @@ class OpenStackCloud(object): return network return None + def list_routers(self): + return self.neutron_client.list_routers()['routers'] + + def get_router(self, name_or_id): + for router in self.list_routers(): + if name_or_id in (router['id'], router['name']): + return router + return None + # TODO(Shrews): This will eventually need to support tenant ID and # provider networks, which are admin-level params. def create_network(self, name, shared=False, admin_state_up=True): @@ -698,6 +707,97 @@ class OpenStackCloud(object): raise OpenStackCloudException( "Error in deleting network %s: %s" % (name_or_id, e.message)) + def create_router(self, name=None, admin_state_up=True): + """Create a logical router. + + :param name: The router name. + :param admin_state_up: The administrative state of the router. + + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + router = { + 'admin_state_up': admin_state_up + } + if name: + router['name'] = name + + try: + new_router = neutron.create_router(dict(router=router)) + except Exception as e: + self.log.debug("Router create failed", exc_info=True) + raise OpenStackCloudException( + "Error creating router %s: %s" % (name, e)) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_router['router'] + + def update_router(self, router_id, name=None, admin_state_up=None): + """Update an existing logical router. + + :param router_id: The router UUID. + :param name: The router name. + :param admin_state_up: The administrative state of the router. + + :returns: The router object. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + router = {} + if name: + router['name'] = name + if admin_state_up: + router['admin_state_up'] = admin_state_up + + if not router: + self.log.debug("No router data to update") + return + + try: + new_router = neutron.update_router(router_id, dict(router=router)) + except Exception as e: + self.log.debug("Router update failed", exc_info=True) + raise OpenStackCloudException( + "Error updating router %s: %s" % (name, e)) + # Turns out neutron returns an actual dict, so no need for the + # use of meta.obj_to_dict() here (which would not work against + # a dict). + return new_router['router'] + + def delete_router(self, name_or_id): + """Delete a logical router. + + If a name, instead of a unique UUID, is supplied, it is possible + that we could find more than one matching router since names are + not required to be unique. An error will be raised in this case. + + :param name_or_id: Name or ID of the router being deleted. + :raises: OpenStackCloudException on operation error. + """ + neutron = self.neutron_client + + routers = [] + for router in self.list_routers(): + if name_or_id in (router['id'], router['name']): + routers.append(router) + + if not routers: + raise OpenStackCloudException( + "Router %s not found." % name_or_id) + + if len(routers) > 1: + raise OpenStackCloudException( + "More than one router named %s. Use ID." % name_or_id) + + try: + neutron.delete_router(routers[0]['id']) + except Exception as e: + self.log.debug("Router delete failed", exc_info=True) + raise OpenStackCloudException( + "Error deleting router %s: %s" % (name_or_id, e)) + def _get_images_from_cloud(self, filter_deleted): # First, try to actually get images from glance, it's more efficient images = dict() diff --git a/shade/tests/unit/test_shade.py b/shade/tests/unit/test_shade.py index bf222e6f6..305ec61ce 100644 --- a/shade/tests/unit/test_shade.py +++ b/shade/tests/unit/test_shade.py @@ -12,11 +12,79 @@ # License for the specific language governing permissions and limitations # under the License. +import mock + import shade from shade.tests import base class TestShade(base.TestCase): + def setUp(self): + super(TestShade, self).setUp() + self.cloud = shade.openstack_cloud() + def test_openstack_cloud(self): - self.assertIsInstance(shade.openstack_cloud(), shade.OpenStackCloud) + self.assertIsInstance(self.cloud, shade.OpenStackCloud) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + def test_get_router(self, mock_list): + router1 = dict(id='123', name='mickey') + mock_list.return_value = [router1] + r = self.cloud.get_router('mickey') + self.assertIsNotNone(r) + self.assertDictEqual(router1, r) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + def test_get_router_not_found(self, mock_list): + mock_list.return_value = [] + r = self.cloud.get_router('goofy') + self.assertIsNone(r) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_create_router(self, mock_client): + self.cloud.create_router(name='goofy', admin_state_up=True) + self.assertTrue(mock_client.create_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_update_router(self, mock_client): + self.cloud.update_router(router_id=123, name='goofy') + self.assertTrue(mock_client.update_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + mock_list.return_value = [router1] + self.cloud.delete_router('mickey') + self.assertTrue(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_not_found(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + mock_list.return_value = [router1] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_router, + 'goofy') + self.assertFalse(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_multiple_found(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + router2 = dict(id='456', name='mickey') + mock_list.return_value = [router1, router2] + self.assertRaises(shade.OpenStackCloudException, + self.cloud.delete_router, + 'mickey') + self.assertFalse(mock_client.delete_router.called) + + @mock.patch.object(shade.OpenStackCloud, 'list_routers') + @mock.patch.object(shade.OpenStackCloud, 'neutron_client') + def test_delete_router_multiple_using_id(self, mock_client, mock_list): + router1 = dict(id='123', name='mickey') + router2 = dict(id='456', name='mickey') + mock_list.return_value = [router1, router2] + self.cloud.delete_router('123') + self.assertTrue(mock_client.delete_router.called) diff --git a/test-requirements.txt b/test-requirements.txt index 9dab35b8f..6957d91b4 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -3,6 +3,7 @@ hacking>=0.5.6,<0.8 coverage>=3.6 discover fixtures>=0.3.14 +mock>=1.0 python-subunit sphinx>=1.1.2 oslo.sphinx