summaryrefslogtreecommitdiff
path: root/openstackclient
diff options
context:
space:
mode:
Diffstat (limited to 'openstackclient')
-rw-r--r--openstackclient/common/project_purge.py168
-rw-r--r--openstackclient/network/v2/network.py30
-rw-r--r--openstackclient/tests/unit/common/test_project_purge.py314
-rw-r--r--openstackclient/tests/unit/network/v2/test_network.py3
4 files changed, 497 insertions, 18 deletions
diff --git a/openstackclient/common/project_purge.py b/openstackclient/common/project_purge.py
new file mode 100644
index 00000000..dff954e7
--- /dev/null
+++ b/openstackclient/common/project_purge.py
@@ -0,0 +1,168 @@
+# Copyright 2012 OpenStack Foundation
+#
+# 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 logging
+
+from osc_lib.command import command
+from osc_lib import utils
+
+from openstackclient.i18n import _
+from openstackclient.identity import common as identity_common
+
+
+LOG = logging.getLogger(__name__)
+
+
+class ProjectPurge(command.Command):
+ _description = _("Clean resources associated with a project")
+
+ def get_parser(self, prog_name):
+ parser = super(ProjectPurge, self).get_parser(prog_name)
+ parser.add_argument(
+ '--dry-run',
+ action='store_true',
+ help=_("List a project's resources"),
+ )
+ parser.add_argument(
+ '--keep-project',
+ action='store_true',
+ help=_("Clean project resources, but don't delete the project"),
+ )
+ project_group = parser.add_mutually_exclusive_group(required=True)
+ project_group.add_argument(
+ '--auth-project',
+ action='store_true',
+ help=_('Delete resources of the project used to authenticate'),
+ )
+ project_group.add_argument(
+ '--project',
+ metavar='<project>',
+ help=_('Project to clean (name or ID)'),
+ )
+ identity_common.add_project_domain_option_to_parser(parser)
+ return parser
+
+ def take_action(self, parsed_args):
+ identity_client = self.app.client_manager.identity
+
+ if parsed_args.auth_project:
+ project_id = self.app.client_manager.auth_ref.project_id
+ elif parsed_args.project:
+ try:
+ project_id = identity_common.find_project(
+ identity_client,
+ parsed_args.project,
+ parsed_args.project_domain,
+ ).id
+ except AttributeError: # using v2 auth and supplying a domain
+ project_id = utils.find_resource(
+ identity_client.tenants,
+ parsed_args.project,
+ ).id
+
+ # delete all non-identity resources
+ self.delete_resources(parsed_args.dry_run, project_id)
+
+ # clean up the project
+ if not parsed_args.keep_project:
+ LOG.warning(_('Deleting project: %s'), project_id)
+ if not parsed_args.dry_run:
+ identity_client.projects.delete(project_id)
+
+ def delete_resources(self, dry_run, project_id):
+ # servers
+ try:
+ compute_client = self.app.client_manager.compute
+ search_opts = {'tenant_id': project_id}
+ data = compute_client.servers.list(search_opts=search_opts)
+ self.delete_objects(
+ compute_client.servers.delete, data, 'server', dry_run)
+ except Exception:
+ pass
+
+ # images
+ try:
+ image_client = self.app.client_manager.image
+ data = image_client.images.list(owner=project_id)
+ self.delete_objects(
+ image_client.images.delete, data, 'image', dry_run)
+ except Exception:
+ pass
+
+ # volumes, snapshots, backups
+ volume_client = self.app.client_manager.volume
+ search_opts = {'project_id': project_id}
+ try:
+ data = volume_client.volume_snapshots.list(search_opts=search_opts)
+ self.delete_objects(
+ self.delete_one_volume_snapshot,
+ data,
+ 'volume snapshot',
+ dry_run)
+ except Exception:
+ pass
+ try:
+ data = volume_client.backups.list(search_opts=search_opts)
+ self.delete_objects(
+ self.delete_one_volume_backup,
+ data,
+ 'volume backup',
+ dry_run)
+ except Exception:
+ pass
+ try:
+ data = volume_client.volumes.list(search_opts=search_opts)
+ self.delete_objects(
+ volume_client.volumes.force_delete, data, 'volume', dry_run)
+ except Exception:
+ pass
+
+ def delete_objects(self, func_delete, data, resource, dry_run):
+ result = 0
+ for i in data:
+ LOG.warning(_('Deleting %(resource)s : %(id)s') %
+ {'resource': resource, 'id': i.id})
+ if not dry_run:
+ try:
+ func_delete(i.id)
+ except Exception as e:
+ result += 1
+ LOG.error(_("Failed to delete %(resource)s with "
+ "ID '%(id)s': %(e)s")
+ % {'resource': resource, 'id': i.id, 'e': e})
+ if result > 0:
+ total = len(data)
+ msg = (_("%(result)s of %(total)s %(resource)ss failed "
+ "to delete.") %
+ {'result': result,
+ 'total': total,
+ 'resource': resource})
+ LOG.error(msg)
+
+ def delete_one_volume_snapshot(self, snapshot_id):
+ volume_client = self.app.client_manager.volume
+ try:
+ volume_client.volume_snapshots.delete(snapshot_id)
+ except Exception:
+ # Only volume v2 support deleting by force
+ volume_client.volume_snapshots.delete(snapshot_id, force=True)
+
+ def delete_one_volume_backup(self, backup_id):
+ volume_client = self.app.client_manager.volume
+ try:
+ volume_client.backups.delete(backup_id)
+ except Exception:
+ # Only volume v2 support deleting by force
+ volume_client.backups.delete(backup_id, force=True)
diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py
index e4cf54bf..58ed6841 100644
--- a/openstackclient/network/v2/network.py
+++ b/openstackclient/network/v2/network.py
@@ -127,11 +127,6 @@ def _get_attrs_network(client_manager, parsed_args):
attrs['qos_policy_id'] = _qos_policy.id
if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy:
attrs['qos_policy_id'] = None
- # Update VLAN Transparency for networks
- if parsed_args.transparent_vlan:
- attrs['vlan_transparent'] = True
- if parsed_args.no_transparent_vlan:
- attrs['vlan_transparent'] = False
return attrs
@@ -170,16 +165,6 @@ def _add_additional_network_options(parser):
help=_("VLAN ID for VLAN networks or Tunnel ID for "
"GENEVE/GRE/VXLAN networks"))
- vlan_transparent_grp = parser.add_mutually_exclusive_group()
- vlan_transparent_grp.add_argument(
- '--transparent-vlan',
- action='store_true',
- help=_("Make the network VLAN transparent"))
- vlan_transparent_grp.add_argument(
- '--no-transparent-vlan',
- action='store_true',
- help=_("Do not make the network VLAN transparent"))
-
# TODO(sindhu): Use the SDK resource mapped attribute names once the
# OSC minimum requirements include SDK 1.0.
@@ -282,6 +267,16 @@ class CreateNetwork(common.NetworkAndComputeShowOne):
metavar='<qos-policy>',
help=_("QoS policy to attach to this network (name or ID)")
)
+ vlan_transparent_grp = parser.add_mutually_exclusive_group()
+ vlan_transparent_grp.add_argument(
+ '--transparent-vlan',
+ action='store_true',
+ help=_("Make the network VLAN transparent"))
+ vlan_transparent_grp.add_argument(
+ '--no-transparent-vlan',
+ action='store_true',
+ help=_("Do not make the network VLAN transparent"))
+
_add_additional_network_options(parser)
return parser
@@ -296,6 +291,11 @@ class CreateNetwork(common.NetworkAndComputeShowOne):
def take_action_network(self, client, parsed_args):
attrs = _get_attrs_network(self.app.client_manager, parsed_args)
+ if parsed_args.transparent_vlan:
+ attrs['vlan_transparent'] = True
+ if parsed_args.no_transparent_vlan:
+ attrs['vlan_transparent'] = False
+
obj = client.create_network(**attrs)
display_columns, columns = _get_columns_network(obj)
data = utils.get_item_properties(obj, columns, formatters=_formatters)
diff --git a/openstackclient/tests/unit/common/test_project_purge.py b/openstackclient/tests/unit/common/test_project_purge.py
new file mode 100644
index 00000000..05a8aa3e
--- /dev/null
+++ b/openstackclient/tests/unit/common/test_project_purge.py
@@ -0,0 +1,314 @@
+# 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
+
+from osc_lib import exceptions
+
+from openstackclient.common import project_purge
+from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes
+from openstackclient.tests.unit import fakes
+from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes
+from openstackclient.tests.unit.image.v2 import fakes as image_fakes
+from openstackclient.tests.unit import utils as tests_utils
+from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes
+
+
+class TestProjectPurgeInit(tests_utils.TestCommand):
+
+ def setUp(self):
+ super(TestProjectPurgeInit, self).setUp()
+ compute_client = compute_fakes.FakeComputev2Client(
+ endpoint=fakes.AUTH_URL,
+ token=fakes.AUTH_TOKEN,
+ )
+ self.app.client_manager.compute = compute_client
+ self.servers_mock = compute_client.servers
+ self.servers_mock.reset_mock()
+
+ volume_client = volume_fakes.FakeVolumeClient(
+ endpoint=fakes.AUTH_URL,
+ token=fakes.AUTH_TOKEN,
+ )
+ self.app.client_manager.volume = volume_client
+ self.volumes_mock = volume_client.volumes
+ self.volumes_mock.reset_mock()
+ self.snapshots_mock = volume_client.volume_snapshots
+ self.snapshots_mock.reset_mock()
+ self.backups_mock = volume_client.backups
+ self.backups_mock.reset_mock()
+
+ identity_client = identity_fakes.FakeIdentityv3Client(
+ endpoint=fakes.AUTH_URL,
+ token=fakes.AUTH_TOKEN,
+ )
+ self.app.client_manager.identity = identity_client
+ self.domains_mock = identity_client.domains
+ self.domains_mock.reset_mock()
+ self.projects_mock = identity_client.projects
+ self.projects_mock.reset_mock()
+
+ image_client = image_fakes.FakeImagev2Client(
+ endpoint=fakes.AUTH_URL,
+ token=fakes.AUTH_TOKEN,
+ )
+ self.app.client_manager.image = image_client
+ self.images_mock = image_client.images
+ self.images_mock.reset_mock()
+
+
+class TestProjectPurge(TestProjectPurgeInit):
+
+ project = identity_fakes.FakeProject.create_one_project()
+ server = compute_fakes.FakeServer.create_one_server()
+ image = image_fakes.FakeImage.create_one_image()
+ volume = volume_fakes.FakeVolume.create_one_volume()
+ backup = volume_fakes.FakeBackup.create_one_backup()
+ snapshot = volume_fakes.FakeSnapshot.create_one_snapshot()
+
+ def setUp(self):
+ super(TestProjectPurge, self).setUp()
+ self.projects_mock.get.return_value = self.project
+ self.projects_mock.delete.return_value = None
+ self.images_mock.list.return_value = [self.image]
+ self.images_mock.delete.return_value = None
+ self.servers_mock.list.return_value = [self.server]
+ self.servers_mock.delete.return_value = None
+ self.volumes_mock.list.return_value = [self.volume]
+ self.volumes_mock.delete.return_value = None
+ self.volumes_mock.force_delete.return_value = None
+ self.snapshots_mock.list.return_value = [self.snapshot]
+ self.snapshots_mock.delete.return_value = None
+ self.backups_mock.list.return_value = [self.backup]
+ self.backups_mock.delete.return_value = None
+
+ self.cmd = project_purge.ProjectPurge(self.app, None)
+
+ def test_project_no_options(self):
+ arglist = []
+ verifylist = []
+
+ self.assertRaises(tests_utils.ParserException, self.check_parser,
+ self.cmd, arglist, verifylist)
+
+ def test_project_purge_with_project(self):
+ arglist = [
+ '--project', self.project.id,
+ ]
+ verifylist = [
+ ('dry_run', False),
+ ('keep_project', False),
+ ('auth_project', False),
+ ('project', self.project.id),
+ ('project_domain', None),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.projects_mock.get.assert_called_once_with(self.project.id)
+ self.projects_mock.delete.assert_called_once_with(self.project.id)
+ self.servers_mock.list.assert_called_once_with(
+ search_opts={'tenant_id': self.project.id})
+ self.images_mock.list.assert_called_once_with(
+ owner=self.project.id)
+ volume_search_opts = {'project_id': self.project.id}
+ self.volumes_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.snapshots_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.backups_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.servers_mock.delete.assert_called_once_with(self.server.id)
+ self.images_mock.delete.assert_called_once_with(self.image.id)
+ self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
+ self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
+ self.backups_mock.delete.assert_called_once_with(self.backup.id)
+ self.assertIsNone(result)
+
+ def test_project_purge_with_dry_run(self):
+ arglist = [
+ '--dry-run',
+ '--project', self.project.id,
+ ]
+ verifylist = [
+ ('dry_run', True),
+ ('keep_project', False),
+ ('auth_project', False),
+ ('project', self.project.id),
+ ('project_domain', None),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.projects_mock.get.assert_called_once_with(self.project.id)
+ self.projects_mock.delete.assert_not_called()
+ self.servers_mock.list.assert_called_once_with(
+ search_opts={'tenant_id': self.project.id})
+ self.images_mock.list.assert_called_once_with(
+ owner=self.project.id)
+ volume_search_opts = {'project_id': self.project.id}
+ self.volumes_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.snapshots_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.backups_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.servers_mock.delete.assert_not_called()
+ self.images_mock.delete.assert_not_called()
+ self.volumes_mock.force_delete.assert_not_called()
+ self.snapshots_mock.delete.assert_not_called()
+ self.backups_mock.delete.assert_not_called()
+ self.assertIsNone(result)
+
+ def test_project_purge_with_keep_project(self):
+ arglist = [
+ '--keep-project',
+ '--project', self.project.id,
+ ]
+ verifylist = [
+ ('dry_run', False),
+ ('keep_project', True),
+ ('auth_project', False),
+ ('project', self.project.id),
+ ('project_domain', None),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.projects_mock.get.assert_called_once_with(self.project.id)
+ self.projects_mock.delete.assert_not_called()
+ self.servers_mock.list.assert_called_once_with(
+ search_opts={'tenant_id': self.project.id})
+ self.images_mock.list.assert_called_once_with(
+ owner=self.project.id)
+ volume_search_opts = {'project_id': self.project.id}
+ self.volumes_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.snapshots_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.backups_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.servers_mock.delete.assert_called_once_with(self.server.id)
+ self.images_mock.delete.assert_called_once_with(self.image.id)
+ self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
+ self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
+ self.backups_mock.delete.assert_called_once_with(self.backup.id)
+ self.assertIsNone(result)
+
+ def test_project_purge_with_auth_project(self):
+ self.app.client_manager.auth_ref = mock.Mock()
+ self.app.client_manager.auth_ref.project_id = self.project.id
+ arglist = [
+ '--auth-project',
+ ]
+ verifylist = [
+ ('dry_run', False),
+ ('keep_project', False),
+ ('auth_project', True),
+ ('project', None),
+ ('project_domain', None),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.projects_mock.get.assert_not_called()
+ self.projects_mock.delete.assert_called_once_with(self.project.id)
+ self.servers_mock.list.assert_called_once_with(
+ search_opts={'tenant_id': self.project.id})
+ self.images_mock.list.assert_called_once_with(
+ owner=self.project.id)
+ volume_search_opts = {'project_id': self.project.id}
+ self.volumes_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.snapshots_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.backups_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.servers_mock.delete.assert_called_once_with(self.server.id)
+ self.images_mock.delete.assert_called_once_with(self.image.id)
+ self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
+ self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
+ self.backups_mock.delete.assert_called_once_with(self.backup.id)
+ self.assertIsNone(result)
+
+ @mock.patch.object(project_purge.LOG, 'error')
+ def test_project_purge_with_exception(self, mock_error):
+ self.servers_mock.delete.side_effect = exceptions.CommandError()
+ arglist = [
+ '--project', self.project.id,
+ ]
+ verifylist = [
+ ('dry_run', False),
+ ('keep_project', False),
+ ('auth_project', False),
+ ('project', self.project.id),
+ ('project_domain', None),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.projects_mock.get.assert_called_once_with(self.project.id)
+ self.projects_mock.delete.assert_called_once_with(self.project.id)
+ self.servers_mock.list.assert_called_once_with(
+ search_opts={'tenant_id': self.project.id})
+ self.images_mock.list.assert_called_once_with(
+ owner=self.project.id)
+ volume_search_opts = {'project_id': self.project.id}
+ self.volumes_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.snapshots_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.backups_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.servers_mock.delete.assert_called_once_with(self.server.id)
+ self.images_mock.delete.assert_called_once_with(self.image.id)
+ self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
+ self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
+ self.backups_mock.delete.assert_called_once_with(self.backup.id)
+ mock_error.assert_called_with("1 of 1 servers failed to delete.")
+ self.assertIsNone(result)
+
+ def test_project_purge_with_force_delete_backup(self):
+ self.backups_mock.delete.side_effect = [exceptions.CommandError, None]
+ arglist = [
+ '--project', self.project.id,
+ ]
+ verifylist = [
+ ('dry_run', False),
+ ('keep_project', False),
+ ('auth_project', False),
+ ('project', self.project.id),
+ ('project_domain', None),
+ ]
+ parsed_args = self.check_parser(self.cmd, arglist, verifylist)
+
+ result = self.cmd.take_action(parsed_args)
+ self.projects_mock.get.assert_called_once_with(self.project.id)
+ self.projects_mock.delete.assert_called_once_with(self.project.id)
+ self.servers_mock.list.assert_called_once_with(
+ search_opts={'tenant_id': self.project.id})
+ self.images_mock.list.assert_called_once_with(
+ owner=self.project.id)
+ volume_search_opts = {'project_id': self.project.id}
+ self.volumes_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.snapshots_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.backups_mock.list.assert_called_once_with(
+ search_opts=volume_search_opts)
+ self.servers_mock.delete.assert_called_once_with(self.server.id)
+ self.images_mock.delete.assert_called_once_with(self.image.id)
+ self.volumes_mock.force_delete.assert_called_once_with(self.volume.id)
+ self.snapshots_mock.delete.assert_called_once_with(self.snapshot.id)
+ self.assertEqual(2, self.backups_mock.delete.call_count)
+ self.backups_mock.delete.assert_called_with(self.backup.id, force=True)
+ self.assertIsNone(result)
diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py
index 1bd7bac6..4065e5ca 100644
--- a/openstackclient/tests/unit/network/v2/test_network.py
+++ b/openstackclient/tests/unit/network/v2/test_network.py
@@ -821,7 +821,6 @@ class TestSetNetwork(TestNetwork):
'--provider-network-type', 'vlan',
'--provider-physical-network', 'physnet1',
'--provider-segment', '400',
- '--no-transparent-vlan',
'--enable-port-security',
'--qos-policy', self.qos_policy.name,
]
@@ -836,7 +835,6 @@ class TestSetNetwork(TestNetwork):
('provider_network_type', 'vlan'),
('physical_network', 'physnet1'),
('segmentation_id', '400'),
- ('no_transparent_vlan', True),
('enable_port_security', True),
('qos_policy', self.qos_policy.name),
]
@@ -854,7 +852,6 @@ class TestSetNetwork(TestNetwork):
'provider:network_type': 'vlan',
'provider:physical_network': 'physnet1',
'provider:segmentation_id': '400',
- 'vlan_transparent': False,
'port_security_enabled': True,
'qos_policy_id': self.qos_policy.id,
}