Adding the disable feature

This feature allow the disable of most of the project resources
in some cases the admin want to disable the project and wait
for period of time before deleting the resources just in case
some resources are still usable or need to be moved to another
project

for nova the disable will stop the servers
for neutron it will change the admin_state_up to false which
will change the status of resource to down
for glance it will deactivate the images
for swift it will remove the read/write acls to container
for octaivia chenge the admin_state_up to false making the
status of loadbalancer down
for cinder it will change the volume to readonly

for the resources that dont have a way of disabling the
resource it will just log a warning

Change-Id: Ic2af6ad1ffb1e749a3d1ba687950264b5098bcdb
This commit is contained in:
Hamza Alqtaishat 2019-10-07 16:24:45 +00:00
parent 3ca9f412b4
commit 39a159f8c7
24 changed files with 332 additions and 11 deletions

View File

@ -72,6 +72,11 @@ def create_argument_parser():
"--os-identity-api-version", default=3,
help="Identity API version, default=3"
)
parser.add_argument(
"--disable-only", action="store_true",
help="Disable resources of the project rather than purging them. "
"Useful if you want to keep the resources but disable them."
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
@ -164,7 +169,9 @@ class CredentialsManager(object):
def runner(resource_mngr, options, exit):
try:
if not (options.dry_run or options.resource):
if not (options.dry_run or
options.resource or
options.disable_only):
resource_mngr.wait_for_check_prerequisite(exit)
for resource in resource_mngr.list():
@ -172,18 +179,27 @@ def runner(resource_mngr, options, exit):
if exit.is_set():
return
if resource_mngr.should_delete(resource):
resource_func_to_call = None
if options.disable_only:
resource_func_to_call = resource_mngr.disable
logging.info("Going to disable resource %s",
resource_mngr.to_str(resource))
elif resource_mngr.should_delete(resource):
resource_func_to_call = resource_mngr.delete
logging.info("Going to delete %s",
resource_mngr.to_str(resource))
# If we are in dry run mode, don't actually delete the resource
if resource_func_to_call:
# If we are in dry run mode, don't actually delete/disable
# the resource
if options.dry_run:
continue
# If we want to delete only specific resources, many things
# can go wrong, so we basically ignore all exceptions.
# If we want to delete/disable only specific resources, many
# things can go wrong, so we basically ignore all exceptions.
exc = os_exceptions.OpenStackCloudException
utils.call_and_ignore_exc(exc, resource_mngr.delete, resource)
utils.call_and_ignore_exc(exc, resource_func_to_call, resource)
except Exception as exc:
log = logging.error

View File

@ -136,6 +136,14 @@ class ServiceResource(six.with_metaclass(CodingStyleMixin,
def delete(self, resource):
raise NotImplementedError
def disable(self, resource):
msg = "The disable feature is not supported for %s, No action will" \
"be taken against the resource(id=%s, name=%s)."
logging.warning(
msg, self.__class__.__name__,
resource.get('id'), resource.get('name')
)
@staticmethod
@abc.abstractmethod
def to_str(resource):

View File

@ -59,6 +59,14 @@ class Volumes(base.ServiceResource):
def delete(self, resource):
self.cloud.delete_volume(resource['id'])
def disable(self, resource):
# Since there is no disable for volume setting it to readonly
# is good enough so no update on it
self.cloud.update_volume(
resource['id'],
metadata={'readonly': 'true'}
)
@staticmethod
def to_str(resource):
return "Volume (id='{}', name='{}')".format(

View File

@ -53,6 +53,9 @@ class Volumes(base.ServiceResource):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...

View File

@ -43,6 +43,9 @@ class Images(base.ServiceResource, ListImagesMixin):
def delete(self, resource):
self.cloud.delete_image(resource['id'])
def disable(self, resource):
self.cloud.image.deactivate_image(resource['id'])
@staticmethod
def to_str(resource):
return "Image (id='{}', name='{}')".format(

View File

@ -32,6 +32,9 @@ class Images(base.ServiceResource, ListImagesMixin):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...

View File

@ -27,6 +27,12 @@ class FloatingIPs(base.ServiceResource):
def delete(self, resource):
self.cloud.delete_floating_ip(resource['id'])
def disable(self, resource):
self.cloud.network.update_ip(
resource['id'],
port_id=None,
)
@staticmethod
def to_str(resource):
return "Floating IP (id='{}')".format(resource['id'])
@ -76,6 +82,12 @@ class Routers(base.ServiceResource):
def delete(self, resource):
self.cloud.delete_router(resource['id'])
def disable(self, resource):
self.cloud.update_router(
resource['id'],
admin_state_up=False
)
@staticmethod
def to_str(resource):
return "Router (id='{}', name='{}')".format(
@ -97,6 +109,9 @@ class Ports(base.ServiceResource):
def delete(self, resource):
self.cloud.delete_port(resource['id'])
def disable(self, resource):
self.cloud.update_port(resource['id'], admin_state_up=False)
@staticmethod
def to_str(resource):
return "Port (id='{}', network_id='{}, device_owner='{}')'".format(
@ -128,6 +143,9 @@ class Networks(base.ServiceResource):
def delete(self, resource):
self.cloud.delete_network(resource['id'])
def disable(self, resource):
self.cloud.update_network(resource['id'], admin_state_up=False)
@staticmethod
def to_str(resource):
return "Network (id='{}', name='{}')".format(

View File

@ -26,6 +26,9 @@ class FloatingIPs(base.ServiceResource):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...
@ -56,6 +59,9 @@ class Routers(base.ServiceResource):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...
@ -68,6 +74,9 @@ class Ports(base.ServiceResource):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...
@ -83,6 +92,9 @@ class Networks(base.ServiceResource):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...

View File

@ -21,6 +21,9 @@ class Servers(base.ServiceResource):
def delete(self, resource):
self.cloud.delete_server(resource['id'])
def disable(self, resource):
self.cloud.compute.stop_server(resource['id'])
@staticmethod
def to_str(resource):
return "VM (id='{}', name='{}')".format(

View File

@ -23,6 +23,9 @@ class Servers(base.ServiceResource):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...

View File

@ -25,6 +25,12 @@ class LoadBalancers(base.ServiceResource):
self.cloud.load_balancer.delete_load_balancer(
resource['id'], cascade=True)
def disable(self, resource):
self.cloud.load_balancer.update_load_balancer(
resource['id'],
admin_state_up=False
)
@staticmethod
def to_str(resource):
return "Octavia LoadBalancer (id='{}', name='{}')".format(

View File

@ -23,6 +23,9 @@ class LoadBalancers(base.ServiceResource):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...

View File

@ -62,6 +62,14 @@ class Containers(base.ServiceResource, ListObjectsMixin):
def delete(self, resource):
self.cloud.delete_container(urllib_parse.quote(resource['name']))
def disable(self, resource):
# There is no disable for the swift container just removing the
# write/read access to the conatianer, so only admin can access
self.cloud.object_store.set_container_metadata(
urllib_parse.quote(resource['name']),
write_acl=None, read_acl=None
)
@staticmethod
def to_str(resource):
return "Container (name='{}')".format(resource['name'])

View File

@ -49,6 +49,9 @@ class Containers(base.ServiceResource, ListObjectsMixin):
def delete(self, resource: Dict[str, Any]) -> None:
...
def disable(self, resource: Dict[str, Any]) -> None:
...
@staticmethod
def to_str(resource: Dict[str, Any]) -> str:
...

View File

@ -32,6 +32,11 @@ class TestBackups(unittest.TestCase):
self.assertIsNone(cinder.Backups(self.creds_manager).delete(backup))
self.cloud.delete_volume_backup.assert_called_once_with(backup['id'])
def test_disable(self):
backup = mock.MagicMock()
with self.assertLogs(level='WARNING'):
cinder.Backups(self.creds_manager).disable(backup)
def test_to_string(self):
backup = mock.MagicMock()
self.assertIn("Volume Backup",
@ -55,6 +60,11 @@ class TestSnapshots(unittest.TestCase):
self.cloud.delete_volume_snapshot.assert_called_once_with(
snapshot['id'])
def test_disable(self):
snapshot = mock.MagicMock()
with self.assertLogs(level='WARNING'):
cinder.Snapshots(self.creds_manager).disable(snapshot)
def test_to_string(self):
snapshot = mock.MagicMock()
self.assertIn("Volume Snapshot ",
@ -97,6 +107,14 @@ class TestVolumes(unittest.TestCase):
self.assertIsNone(cinder.Volumes(self.creds_manager).delete(volume))
self.cloud.delete_volume.assert_called_once_with(volume['id'])
def test_disable(self):
volume = mock.MagicMock()
cinder.Volumes(self.creds_manager).disable(volume)
self.cloud.update_volume.assert_called_once_with(
volume['id'],
metadata={'readonly': 'true'}
)
def test_to_string(self):
volume = mock.MagicMock()
self.assertIn("Volume ",

View File

@ -38,6 +38,11 @@ class TestZones(unittest.TestCase):
self.assertIsNone(designate.Zones(self.creds_manager).delete(zone))
self.cloud.delete_zone.assert_called_once_with(zone['id'])
def test_disable(self):
zone = mock.MagicMock()
with self.assertLogs(level='WARNING'):
designate.Zones(self.creds_manager).disable(zone)
def test_to_string(self):
stack = mock.MagicMock()
self.assertIn("Designate Zone",

View File

@ -79,6 +79,13 @@ class TestImages(unittest.TestCase):
self.assertIsNone(glance.Images(self.creds_manager).delete(image))
self.cloud.delete_image.assert_called_once_with(image['id'])
def test_disable(self):
image = mock.MagicMock()
self.assertIsNone(glance.Images(self.creds_manager).disable(image))
self.cloud.image.deactivate_image.assert_called_once_with(
image['id']
)
def test_to_string(self):
image = mock.MagicMock()
self.assertIn("Image (",

View File

@ -38,6 +38,11 @@ class TestStacks(unittest.TestCase):
self.assertIsNone(heat.Stacks(self.creds_manager).delete(stack))
self.cloud.delete_stack.assert_called_once_with(stack['id'], wait=True)
def test_disable(self):
stack = mock.MagicMock()
with self.assertLogs(level='WARNING'):
heat.Stacks(self.creds_manager).disable(stack)
def test_to_string(self):
stack = mock.MagicMock()
self.assertIn("Heat Stack",

View File

@ -47,6 +47,14 @@ class TestFloatingIPs(unittest.TestCase):
self.cloud.delete_floating_ip.assert_called_once_with(
fip['id'])
def test_disable(self):
fip = mock.MagicMock()
self.assertIsNone(neutron.FloatingIPs(self.creds_manager).disable(
fip
))
self.cloud.network.update_ip.assert_called_once_with(
fip['id'], port_id=None)
def test_to_string(self):
fip = mock.MagicMock()
self.assertIn("Floating IP ",
@ -93,6 +101,13 @@ class TestRouterInterfaces(unittest.TestCase):
port_id=iface['id']
)
def test_disable(self):
iface = mock.MagicMock()
with self.assertLogs(level='WARNING'):
neutron.RouterInterfaces(self.creds_manager).disable(
iface
)
def test_to_string(self):
iface = mock.MagicMock()
self.assertIn(
@ -131,6 +146,14 @@ class TestRouters(unittest.TestCase):
self.assertIsNone(neutron.Routers(self.creds_manager).delete(router))
self.cloud.delete_router.assert_called_once_with(router['id'])
def test_disable(self):
router = mock.MagicMock()
self.assertIsNone(neutron.Routers(self.creds_manager).disable(router))
self.cloud.update_router.assert_called_once_with(
router['id'],
admin_state_up=False
)
def test_to_string(self):
router = mock.MagicMock()
self.assertIn("Router (",
@ -159,6 +182,14 @@ class TestPorts(unittest.TestCase):
self.assertIsNone(neutron.Ports(self.creds_manager).delete(port))
self.cloud.delete_port.assert_called_once_with(port['id'])
def test_disable(self):
port = mock.MagicMock()
self.assertIsNone(neutron.Ports(self.creds_manager).disable(port))
self.cloud.update_port.assert_called_once_with(
port['id'],
admin_state_up=False
)
def test_to_string(self):
port = mock.MagicMock()
self.assertIn("Port (",
@ -203,6 +234,14 @@ class TestNetworks(unittest.TestCase):
self.assertIsNone(neutron.Networks(self.creds_manager).delete(nw))
self.cloud.delete_network.assert_called_once_with(nw['id'])
def test_disable(self):
nw = mock.MagicMock()
self.assertIsNone(neutron.Networks(self.creds_manager).disable(nw))
self.cloud.update_network.assert_called_once_with(
nw['id'],
admin_state_up=False
)
def test_to_string(self):
nw = mock.MagicMock()
self.assertIn("Network (",
@ -230,6 +269,11 @@ class TestSecurityGroups(unittest.TestCase):
neutron.SecurityGroups(self.creds_manager).delete(sg))
self.cloud.delete_security_group.assert_called_once_with(sg['id'])
def test_disable(self):
sg = mock.MagicMock()
with self.assertLogs(level='WARNING'):
neutron.SecurityGroups(self.creds_manager).disable(sg)
def test_to_string(self):
sg = mock.MagicMock()
self.assertIn("Security Group (",

View File

@ -32,6 +32,11 @@ class TestServers(unittest.TestCase):
self.assertIsNone(nova.Servers(self.creds_manager).delete(server))
self.cloud.delete_server.assert_called_once_with(server['id'])
def test_disable(self):
server = mock.MagicMock()
self.assertIsNone(nova.Servers(self.creds_manager).disable(server))
self.cloud.compute.stop_server.assert_called_once_with(server['id'])
def test_to_string(self):
server = mock.MagicMock()
self.assertIn("VM (",

View File

@ -44,6 +44,13 @@ class TestLoadBalancers(unittest.TestCase):
(self.cloud.load_balancer.delete_load_balancer
.assert_called_once_with(lb['id'], cascade=True))
def test_disable(self):
lb = mock.MagicMock()
self.assertIsNone(octavia.LoadBalancers(self.creds_manager).disable(
lb))
(self.cloud.load_balancer.update_load_balancer
.assert_called_once_with(lb['id'], admin_state_up=False))
def test_to_string(self):
stack = mock.MagicMock()
self.assertIn("Octavia LoadBalancer",

View File

@ -89,6 +89,11 @@ class TestObjects(unittest.TestCase):
urllib_parse.quote(obj['name'])
)
def test_disable(self):
obj = {'name': 'toto', 'container_name': 'foo'}
with self.assertLogs(level='WARNING'):
swift.Objects(self.creds_manager).disable(obj)
def test_to_string(self):
obj = mock.MagicMock()
self.assertIn("Object '",
@ -128,6 +133,17 @@ class TestContainers(unittest.TestCase):
urllib_parse.quote(cont['name'])
)
def test_disable(self):
cont = {'bytes': 8,
'count': 2,
'last_modified': '2019-06-05T15:20:59.450120',
'name': 'Pouet éêù #'}
self.assertIsNone(swift.Containers(self.creds_manager).disable(cont))
self.cloud.object_store.set_container_metadata.assert_called_once_with(
urllib_parse.quote(cont['name']),
read_acl=None, write_acl=None
)
def test_to_string(self):
container = mock.MagicMock()
self.assertIn("Container (",

View File

@ -74,10 +74,10 @@ class TestFunctions(unittest.TestCase):
self.assertEqual('foo', options.purge_project)
self.assertEqual(['Networks', 'Volumes'], options.resource)
def test_runner(self):
def test_runner_delete(self):
resources = [mock.Mock(), mock.Mock(), mock.Mock()]
resource_manager = mock.Mock(list=mock.Mock(return_value=resources))
options = mock.Mock(dry_run=False, resource=False)
options = mock.Mock(dry_run=False, resource=False, disable_only=False)
exit = mock.Mock(is_set=mock.Mock(side_effect=[False, False, True]))
main.runner(resource_manager, options, exit)
@ -95,10 +95,43 @@ class TestFunctions(unittest.TestCase):
resource_manager.delete.call_args_list
)
def test_runner_dry_run(self):
def test_runner_disable(self):
resources = [mock.Mock(), mock.Mock(), mock.Mock()]
resource_manager = mock.Mock(list=mock.Mock(return_value=resources))
options = mock.Mock(dry_run=False, resource=False, disable_only=True)
exit = mock.Mock(is_set=mock.Mock(side_effect=[False, False, True]))
main.runner(resource_manager, options, exit)
resource_manager.list.assert_called_once_with()
resource_manager.wait_for_check_prerequisite.assert_not_called()
resource_manager.should_delete.assert_not_called()
resource_manager.delete.assert_not_called()
self.assertEqual(2, resource_manager.disable.call_count)
self.assertEqual(
[mock.call(resources[0]), mock.call(resources[1])],
resource_manager.disable.call_args_list
)
def test_runner_disable_dry_run(self):
resources = [mock.Mock(), mock.Mock(), mock.Mock()]
resource_manager = mock.Mock(list=mock.Mock(return_value=resources))
options = mock.Mock(dry_run=True, resource=False, disable_only=True)
exit = mock.Mock(is_set=mock.Mock(side_effect=[False, False, True]))
main.runner(resource_manager, options, exit)
resource_manager.list.assert_called_once_with()
resource_manager.wait_for_check_prerequisite.assert_not_called()
resource_manager.should_delete.assert_not_called()
resource_manager.delete.assert_not_called()
resource_manager.disable.assert_not_called()
resource_manager.assert_not_called()
def test_runner_delete_dry_run(self):
resources = [mock.Mock(), mock.Mock()]
resource_manager = mock.Mock(list=mock.Mock(return_value=resources))
options = mock.Mock(dry_run=True)
options = mock.Mock(dry_run=True, disable_only=False)
exit = mock.Mock(is_set=mock.Mock(return_value=False))
main.runner(resource_manager, options, exit)
@ -109,7 +142,7 @@ class TestFunctions(unittest.TestCase):
def test_runner_resource(self):
resources = [mock.Mock()]
resource_manager = mock.Mock(list=mock.Mock(return_value=resources))
options = mock.Mock(dry_run=False, resource=True)
options = mock.Mock(dry_run=False, resource=True, disable_only=False)
exit = mock.Mock(is_set=mock.Mock(return_value=False))
main.runner(resource_manager, options, exit)
resource_manager.wait_for_check_prerequisite.assert_not_called()

View File

@ -72,6 +72,77 @@ function assert_volume {
fi
}
########################
# Disable asserts
########################
function test_neutron_disable {
if [[ $(openstack port list -c Status -f value --device-owner compute:nova --project $demo_project_id | grep -ic 'active' ) -gt 0 ]]; then
echo "Some of the ports is not disabled yet :)"
exit 1
fi
if [[ $(openstack port list -c Status -f value --device-owner '' --project $demo_project_id | grep -ic 'active' ) -gt 0 ]]; then
echo "Some of the ports is not disabled yet :)"
exit 1
fi
if [[ $(openstack network list --no-share --long -c State -f value --project $demo_project_id | grep -ic 'UP' ) -gt 0 ]]; then
echo "Some of the networks is not disabled yet :)"
exit 1
fi
for router in $(openstack router list -c ID -f value --project $demo_project_id); do
if [[ $(openstack router show $router -c admin_state_up -f value | grep -ic 'true' ) -gt 0 ]]; then
echo "Some of the routers is not disabled yet :)"
exit 1
fi
done
}
function test_cinder_disable {
if [[ $(openstack volume list --long -c Properties -f value | grep -qvi 'readonly') ]]; then
echo "Cinder volume is not disabled :)"
exit 1
fi
}
function test_glance_disable {
if [[ $(openstack image list --long -c Project -c Status -f value| grep $demo_project_id | grep -ic 'active' ) -gt 0 ]]; then
echo "Some of the images is not disabled yet :)"
exit 1
fi
}
function test_nova_disable {
if [[ $(openstack server list -c Status -f value | grep -ic 'active' ) -gt 0 ]]; then
echo "Some of the servers is not disabled yet :)"
exit 1
fi
}
function test_loadbalancer_disable {
for loadbalancer in $(openstack loadbalancer list -c id -f value --project $demo_project_id); do
if [[ $(openstack loadbalancer show $loadbalancer -c admin_state_up -f value | grep -ic 'true' ) -gt 0 ]]; then
echo "Some of the loadbalancers is not disabled yet :)"
exit 1
fi
done
}
function test_swift_disable {
for container in $(openstack container list -c Name -f value); do
if [[ $(openstack container show $container -f json | grep -iq 'read[-_]acl:.*' ) ]]; then
echo "Some of the containers is not disabled yet :)"
exit 1
fi
if [[ $(openstack container show $container -f json | grep -iq 'write[-_]acl:.*' ) ]]; then
echo "Some of the containers is not disabled yet :)"
exit 1
fi
done
}
########################
@ -113,9 +184,22 @@ done
echo "Done populating. Moving on to cleanup."
########################
# Disable
########################
source $DEVSTACK_DIR/openrc admin admin
demo_project_id=$(openstack project show demo -c id -f value | awk '{print $1}')
source $DEVSTACK_DIR/openrc demo demo
assert_compute && assert_network && assert_volume
tox -e run -- --os-cloud devstack --purge-own-project --verbose --disable-only # disable demo/demo
test_neutron_disable && test_cinder_disable && test_glance_disable \
&& test_nova_disable && test_loadbalancer_disable && test_swift_disable
########################
### Cleanup
########################
source $DEVSTACK_DIR/openrc admin admin
tox -e run -- --os-cloud devstack-admin --purge-own-project --verbose # purges admin/admin
source $DEVSTACK_DIR/openrc demo demo