summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2016-03-02 00:16:54 +0000
committerGerrit Code Review <review@openstack.org>2016-03-02 00:16:54 +0000
commit76010bc09b8186c0462fc9f5c5ad92e534d1ef1c (patch)
tree319593d0f5cc923ac5c2aaa050c2f659b58f9aea
parent07dc616c472d4d20c32751c8da5e9bba8793290a (diff)
parent88fbd3870cf3872fb0c9b4269503c62458664b3b (diff)
downloadpython-neutronclient-76010bc09b8186c0462fc9f5c5ad92e534d1ef1c.tar.gz
Merge "Support cleanup of tenant resources with a single API call"
-rw-r--r--neutronclient/neutron/v2_0/purge.py147
-rw-r--r--neutronclient/shell.py2
-rw-r--r--neutronclient/tests/functional/core/test_purge.py172
-rw-r--r--neutronclient/tests/unit/test_cli20_purge.py100
-rw-r--r--releasenotes/notes/add-neutron-purge-a89e3d1179dce4b1.yaml16
5 files changed, 437 insertions, 0 deletions
diff --git a/neutronclient/neutron/v2_0/purge.py b/neutronclient/neutron/v2_0/purge.py
new file mode 100644
index 0000000..6d8e9d9
--- /dev/null
+++ b/neutronclient/neutron/v2_0/purge.py
@@ -0,0 +1,147 @@
+# Copyright 2016 Cisco Systems
+# All Rights Reserved
+#
+# 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 sys
+
+from neutronclient._i18n import _
+from neutronclient.neutron import v2_0 as neutronV20
+
+
+class Purge(neutronV20.NeutronCommand):
+
+ def _pluralize(self, string):
+ return string + 's'
+
+ def _get_resources(self, neutron_client, resource_types, tenant_id):
+ resources = []
+ for resource_type in resource_types:
+ resources.append([])
+ resource_type_plural = self._pluralize(resource_type)
+ opts = {'fields': ['id', 'tenant_id']}
+ if resource_type_plural == 'ports':
+ opts['fields'].append('device_id')
+ opts['fields'].append('device_owner')
+ function = getattr(neutron_client, 'list_%s' %
+ resource_type_plural)
+ if callable(function):
+ returned_resources = function(**opts).get(resource_type_plural,
+ [])
+ for resource in returned_resources:
+ if resource['tenant_id'] == tenant_id:
+ index = resource_types.index(resource_type)
+ resources[index].append(resource)
+ self.total_resources += 1
+ return resources
+
+ def _delete_resource(self, neutron_client, resource_type, resource):
+ resource_id = resource['id']
+ if resource_type == 'port':
+ if resource.get('device_owner', '') == 'network:router_interface':
+ body = {'port_id': resource_id}
+ neutron_client.remove_interface_router(resource['device_id'],
+ body)
+ return
+ function = getattr(neutron_client, 'delete_%s' % resource_type)
+ if callable(function):
+ function(resource_id)
+
+ def _purge_resources(self, neutron_client, resource_types,
+ tenant_resources):
+ deleted = {}
+ failed = {}
+ failures = False
+ for resources in tenant_resources:
+ index = tenant_resources.index(resources)
+ resource_type = resource_types[index]
+ failed[resource_type] = 0
+ deleted[resource_type] = 0
+ for resource in resources:
+ try:
+ self._delete_resource(neutron_client, resource_type,
+ resource)
+ deleted[resource_type] += 1
+ self.deleted_resources += 1
+ except Exception:
+ failures = True
+ failed[resource_type] += 1
+ self.total_resources -= 1
+ percent_complete = 100
+ if self.total_resources > 0:
+ percent_complete = (self.deleted_resources /
+ float(self.total_resources)) * 100
+ sys.stdout.write("\rPurging resources: %d%% complete." %
+ percent_complete)
+ sys.stdout.flush()
+ return (deleted, failed, failures)
+
+ def _build_message(self, deleted, failed, failures):
+ msg = ''
+ deleted_msg = []
+ for resource, value in deleted.items():
+ if value:
+ if not msg:
+ msg = 'Deleted'
+ if not value == 1:
+ resource = self._pluralize(resource)
+ deleted_msg.append(" %d %s" % (value, resource))
+ if deleted_msg:
+ msg += ','.join(deleted_msg)
+
+ failed_msg = []
+ if failures:
+ if msg:
+ msg += '. '
+ msg += 'The following resources could not be deleted:'
+ for resource, value in failed.items():
+ if value:
+ if not value == 1:
+ resource = self._pluralize(resource)
+ failed_msg.append(" %d %s" % (value, resource))
+ msg += ','.join(failed_msg)
+
+ if msg:
+ msg += '.'
+ else:
+ msg = _('Tenant has no supported resources.')
+
+ return msg
+
+ def get_parser(self, prog_name):
+ parser = super(Purge, self).get_parser(prog_name)
+ parser.add_argument(
+ 'tenant', metavar='TENANT',
+ help=_('ID of Tenant owning the resources to be deleted.'))
+ return parser
+
+ def run(self, parsed_args):
+ neutron_client = self.get_client()
+
+ self.any_failures = False
+
+ # A list of the types of resources supported in the order in which
+ # they should be deleted.
+ resource_types = ['floatingip', 'port', 'router',
+ 'network', 'security_group']
+
+ deleted = {}
+ failed = {}
+ self.total_resources = 0
+ self.deleted_resources = 0
+ resources = self._get_resources(neutron_client, resource_types,
+ parsed_args.tenant)
+ deleted, failed, failures = self._purge_resources(neutron_client,
+ resource_types,
+ resources)
+ print('\n%s' % self._build_message(deleted, failed, failures))
diff --git a/neutronclient/shell.py b/neutronclient/shell.py
index fef1134..2a0b334 100644
--- a/neutronclient/shell.py
+++ b/neutronclient/shell.py
@@ -71,6 +71,7 @@ from neutronclient.neutron.v2_0 import network
from neutronclient.neutron.v2_0.nsx import networkgateway
from neutronclient.neutron.v2_0.nsx import qos_queue
from neutronclient.neutron.v2_0 import port
+from neutronclient.neutron.v2_0 import purge
from neutronclient.neutron.v2_0.qos import bandwidth_limit_rule
from neutronclient.neutron.v2_0.qos import policy as qos_policy
from neutronclient.neutron.v2_0.qos import rule as qos_rule
@@ -176,6 +177,7 @@ COMMAND_V2 = {
'port-create': port.CreatePort,
'port-delete': port.DeletePort,
'port-update': port.UpdatePort,
+ 'purge': purge.Purge,
'quota-list': quota.ListQuota,
'quota-show': quota.ShowQuota,
'quota-delete': quota.DeleteQuota,
diff --git a/neutronclient/tests/functional/core/test_purge.py b/neutronclient/tests/functional/core/test_purge.py
new file mode 100644
index 0000000..91d92a9
--- /dev/null
+++ b/neutronclient/tests/functional/core/test_purge.py
@@ -0,0 +1,172 @@
+# Copyright 2016 Cisco Systems
+# All Rights Reserved
+#
+# 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 neutronclient.tests.functional import base
+
+from tempest_lib import exceptions
+
+
+class PurgeNeutronClientCLITest(base.ClientTestBase):
+
+ def _safe_cleanup(self, delete_command):
+ try:
+ self.neutron(delete_command)
+ except exceptions.CommandFailed:
+ # This resource was already purged successfully
+ pass
+
+ def _create_subnet(self, name, tenant_id, cidr):
+ params = ('%(name)s --name %(name)s --tenant-id %(tenant)s '
+ '%(cidr)s' % {'name': name,
+ 'tenant': tenant_id,
+ 'cidr': cidr})
+ subnet = self.parser.listing(self.neutron('subnet-create',
+ params=params))
+ for row in subnet:
+ if row['Field'] == 'id':
+ return row['Value']
+
+ def _create_router(self, name, tenant_id):
+ params = ('%(name)s --tenant_id %(tenant)s' % {'name': name,
+ 'tenant': tenant_id})
+ router = self.parser.listing(self.neutron('router-create',
+ params=params))
+ for row in router:
+ if row['Field'] == 'id':
+ return row['Value']
+
+ def _create_floatingip(self, network, tenant_id):
+ params = ('%(network)s --tenant-id %(tenant)s' %
+ {'network': network, 'tenant': tenant_id})
+ floatingip = self.parser.listing(self.neutron('floatingip-create',
+ params=params))
+ for row in floatingip:
+ if row['Field'] == 'id':
+ return row['Value']
+
+ def _create_resources(self, name, tenant_id, shared_tenant_id=None):
+ # If no shared_tenant_id is provided, create the resources for the
+ # current tenant to test that they will be deleted when not in use.
+ if not shared_tenant_id:
+ shared_tenant_id = tenant_id
+
+ self.neutron('net-create',
+ params=('%(name)s --router:external True '
+ '--tenant-id %(tenant)s' % {'name': name,
+ 'tenant': tenant_id}))
+ self.addCleanup(self._safe_cleanup, 'net-delete %s' % name)
+
+ self.neutron('net-create',
+ params=('%(name)s-shared --shared '
+ '--tenant-id %(tenant)s' %
+ {'name': name, 'tenant': shared_tenant_id}))
+ self.addCleanup(self._safe_cleanup,
+ 'net-delete %s-shared' % name)
+
+ subnet = self._create_subnet(name, tenant_id, '192.168.71.0/24')
+ self.addCleanup(self._safe_cleanup, 'subnet-delete %s' % name)
+
+ subnet = self._create_subnet('%s-shared' % name, tenant_id,
+ '192.168.81.0/24')
+ self.addCleanup(self._safe_cleanup, 'subnet-delete %s-shared' % name)
+
+ router = self._create_router(name, tenant_id)
+ self.addCleanup(self._safe_cleanup, 'router-delete %s' % name)
+
+ self.neutron('router-interface-add',
+ params=('%(router)s %(subnet)s '
+ '--tenant-id %(tenant)s' % {'router': router,
+ 'subnet': subnet,
+ 'tenant': tenant_id}))
+
+ self.neutron('port-create',
+ params=('%(name)s --name %(name)s '
+ '--tenant-id %(tenant)s' % {'name': name,
+ 'tenant': tenant_id}))
+ self.addCleanup(self._safe_cleanup, 'port-delete %s' % name)
+
+ self.neutron('port-create',
+ params=('%(name)s-shared --name %(name)s-shared '
+ '--tenant-id %(tenant)s' % {'name': name,
+ 'tenant': tenant_id}))
+ self.addCleanup(self._safe_cleanup, 'port-delete %s-shared' % name)
+
+ self.neutron('security-group-create',
+ params=('%(name)s --tenant-id %(tenant)s' %
+ {'name': name, 'tenant': tenant_id}))
+ self.addCleanup(self._safe_cleanup, 'security-group-delete %s' % name)
+
+ floatingip = self._create_floatingip(name, tenant_id)
+ self.addCleanup(self._safe_cleanup, ('floatingip-delete '
+ '%s' % floatingip))
+ return floatingip
+
+ def _verify_deletion(self, resources, resource_type):
+ purged = True
+ no_purge_purged = True
+ for row in resources:
+ if resource_type == 'port' and row.get('id', None):
+ port = self.parser.listing(self.neutron('port-show',
+ params=row['id']))
+ port_dict = {}
+ for row in port:
+ port_dict[row['Field']] = row['Value']
+ if port_dict['device_owner'] == 'network:router_interface':
+ if port_dict['tenant_id'] == 'purge-tenant':
+ purged = False
+ elif port_dict['tenant_id'] == 'no-purge-tenant':
+ no_purge_purged = False
+ if not purged or not no_purge_purged:
+ self.addCleanup(self.neutron,
+ ('router-interface-delete %(router)s '
+ 'port=%(port)s' %
+ {'router': port_dict['device_id'],
+ 'port': port_dict['id']}))
+ if (row.get('name') == 'purge-me' or
+ row.get('id') == self.purge_floatingip):
+ purged = False
+ elif ('no-purge' in row.get('name', '') or
+ row.get('id') == self.no_purge_floatingip):
+ no_purge_purged = False
+
+ if not purged:
+ self.fail('%s not deleted by neutron purge' % resource_type)
+
+ if no_purge_purged:
+ self.fail('%s owned by another tenant incorrectly deleted '
+ 'by neutron purge' % resource_type)
+
+ def test_purge(self):
+ self.purge_floatingip = self._create_resources('purge-me',
+ 'purge-tenant')
+ self.no_purge_floatingip = self._create_resources('no-purge',
+ 'no-purge-tenant',
+ 'purge-tenant')
+
+ purge_output = self.neutron('purge', params='purge-tenant').strip()
+ if not purge_output:
+ self.fail('Purge command did not return feedback')
+
+ networks = self.parser.listing(self.neutron('net-list'))
+ subnets = self.parser.listing(self.neutron('subnet-list'))
+ routers = self.parser.listing(self.neutron('router-list'))
+ ports = self.parser.listing(self.neutron('port-list'))
+ floatingips = self.parser.listing(self.neutron('floatingip-list'))
+
+ self._verify_deletion(networks, 'network')
+ self._verify_deletion(subnets, 'subnet')
+ self._verify_deletion(ports, 'port')
+ self._verify_deletion(routers, 'router')
+ self._verify_deletion(floatingips, 'floatingip')
diff --git a/neutronclient/tests/unit/test_cli20_purge.py b/neutronclient/tests/unit/test_cli20_purge.py
new file mode 100644
index 0000000..9bfd91c
--- /dev/null
+++ b/neutronclient/tests/unit/test_cli20_purge.py
@@ -0,0 +1,100 @@
+# Copyright 2016 Cisco Systems
+# All Rights Reserved
+#
+# 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 sys
+
+from neutronclient.neutron.v2_0 import purge
+from neutronclient.tests.unit import test_cli20
+
+
+class CLITestV20Purge(test_cli20.CLITestV20Base):
+
+ def setUp(self):
+ super(CLITestV20Purge, self).setUp()
+ self.resource_types = ['floatingip', 'port', 'router',
+ 'network', 'security_group']
+
+ def _generate_resources_dict(self, value=0):
+ resources_dict = {}
+ resources_dict['true'] = value
+ for resource_type in self.resource_types:
+ resources_dict[resource_type] = value
+ return resources_dict
+
+ def _verify_suffix(self, resources, message):
+ for resource, value in resources.items():
+ if value > 0:
+ suffix = list('%(value)d %(resource)s' %
+ {'value': value, 'resource': resource})
+ if value != 1:
+ suffix.append('s')
+ suffix = ''.join(suffix)
+ self.assertIn(suffix, message)
+ else:
+ self.assertNotIn(resource, message)
+
+ def _verify_message(self, message, deleted, failed):
+ message = message.split('.')
+ success_prefix = "Deleted "
+ failure_prefix = "The following resources could not be deleted: "
+ if not deleted['true']:
+ for msg in message:
+ self.assertNotIn(success_prefix, msg)
+ message = message[0]
+ if not failed['true']:
+ expected = 'Tenant has no supported resources'
+ self.assertEqual(expected, message)
+ else:
+ self.assertIn(failure_prefix, message)
+ self._verify_suffix(failed, message)
+ else:
+ resources_deleted = message[0]
+ self.assertIn(success_prefix, resources_deleted)
+ self._verify_suffix(deleted, resources_deleted)
+ if failed['true']:
+ resources_failed = message[1]
+ self.assertIn(failure_prefix, resources_failed)
+ self._verify_suffix(failed, resources_failed)
+ else:
+ for msg in message:
+ self.assertNotIn(failure_prefix, msg)
+
+ def _verify_result(self, my_purge, deleted, failed):
+ message = my_purge._build_message(deleted, failed, failed['true'])
+ self._verify_message(message, deleted, failed)
+
+ def test_build_message(self):
+ my_purge = purge.Purge(test_cli20.MyApp(sys.stdout), None)
+
+ # Verify message when tenant has no supported resources
+ deleted = self._generate_resources_dict()
+ failed = self._generate_resources_dict()
+ self._verify_result(my_purge, deleted, failed)
+
+ # Verify message when tenant has supported resources,
+ # and they are all deleteable
+ deleted = self._generate_resources_dict(1)
+ self._verify_result(my_purge, deleted, failed)
+
+ # Verify message when tenant has supported resources,
+ # and some are not deleteable
+ failed = self._generate_resources_dict(1)
+ self._verify_result(my_purge, deleted, failed)
+
+ # Verify message when tenant has supported resources,
+ # and all are not deleteable
+ deleted = self._generate_resources_dict()
+ self._verify_result(my_purge, deleted, failed)
diff --git a/releasenotes/notes/add-neutron-purge-a89e3d1179dce4b1.yaml b/releasenotes/notes/add-neutron-purge-a89e3d1179dce4b1.yaml
new file mode 100644
index 0000000..a689b89
--- /dev/null
+++ b/releasenotes/notes/add-neutron-purge-a89e3d1179dce4b1.yaml
@@ -0,0 +1,16 @@
+---
+features:
+ - |
+ New command 'neutron purge <tenant_id>' will delete all
+ supported resources owned by the given tenant, provided
+ that the user has sufficient authorization and the
+ resources in question are not shared, in use, or
+ otherwise undeletable.
+
+ Supported resources are:
+ * Networks
+ * Subnets
+ * Routers
+ * Ports
+ * Floating IPs
+ * Security Groups