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