589 lines
20 KiB
Python
Executable File
589 lines
20 KiB
Python
Executable File
#!/usr/bin/env python
|
|
# -*- encoding: utf-8 -*-
|
|
#
|
|
# This software is released under the MIT License.
|
|
#
|
|
# Copyright (c) 2014 Cloudwatt
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
import argparse
|
|
import itertools
|
|
import logging
|
|
import os
|
|
from requests.exceptions import ConnectionError
|
|
import sys
|
|
import time
|
|
|
|
from ceilometerclient.v2 import client as ceilometer_client
|
|
from cinderclient.v1 import client as cinder_client
|
|
from glanceclient.v1 import client as glance_client
|
|
from keystoneclient.apiclient import exceptions as api_exceptions
|
|
from keystoneclient.v2_0 import client as keystone_client
|
|
from neutronclient.v2_0 import client as neutron_client
|
|
from novaclient.v1_1 import client as nova_client
|
|
from swiftclient import client as swift_client
|
|
|
|
|
|
RETRIES = 3
|
|
TIMEOUT = 5 # 5 seconds timeout between retries
|
|
|
|
class EndpointNotFound(Exception):
|
|
pass
|
|
|
|
class NoSuchProject(Exception):
|
|
ERROR_CODE = 2
|
|
|
|
AUTHENTICATION_FAILED_ERROR_CODE = 3
|
|
|
|
class DeletionFailed(Exception):
|
|
ERROR_CODE = 4
|
|
|
|
CONNECTION_ERROR_CODE = 5
|
|
|
|
class ConnectionFailed(Exception):
|
|
ERROR_CODE = 6
|
|
|
|
|
|
# Available resources classes
|
|
|
|
RESOURCES_CLASSES = ['CinderSnapshots',
|
|
'NovaServers',
|
|
'NeutronFloatingIps',
|
|
'NeutronInterfaces',
|
|
'NeutronRouters',
|
|
'NeutronNetworks',
|
|
'NeutronSecgroups',
|
|
'GlanceImages',
|
|
'SwiftObjects',
|
|
'SwiftContainers',
|
|
'CinderVolumes',
|
|
'CeilometerAlarms']
|
|
|
|
|
|
### Decorators
|
|
|
|
def retry(service_name):
|
|
def factory(func):
|
|
"""Decorator allowing to retry in case of failure"""
|
|
def wrapper(*args, **kwargs):
|
|
n = 0
|
|
while True :
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except Exception as e:
|
|
if n == RETRIES:
|
|
raise DeletionFailed(service_name)
|
|
n += 1
|
|
logging.info("* Deletion failed - "
|
|
"Retrying in {} seconds - "
|
|
"Retry count {}".format(TIMEOUT, n))
|
|
time.sleep(TIMEOUT)
|
|
return wrapper
|
|
return factory
|
|
|
|
|
|
|
|
### Classes
|
|
|
|
class Session(object):
|
|
"""
|
|
A Session stores information that can be user by the different
|
|
Openstack Clients. The most important data is:
|
|
* self.token - The Openstack token to be used accross services;
|
|
* self.catalog - Allowing to retrieve services' endpoints.
|
|
"""
|
|
|
|
def __init__(self, username, password, project_name,
|
|
auth_url, endpoint_type="publicURL"):
|
|
client = keystone_client.Client(
|
|
username=username, password=password,
|
|
tenant_name=project_name, auth_url=auth_url)
|
|
# Storing username, password, project_name and auth_url for
|
|
# use by clients librarties that cannot use an existing token.
|
|
self.username = username
|
|
self.password = password
|
|
self.project_name = project_name
|
|
self.auth_url = auth_url
|
|
# Session variables to be used by clients when possible
|
|
self.token = client.auth_token
|
|
self.user_id = client.user_id
|
|
self.project_id = client.project_id
|
|
self.endpoint_type = endpoint_type
|
|
self.catalog = client.service_catalog.get_endpoints()
|
|
|
|
def get_endpoint(self, service_type):
|
|
try:
|
|
return self.catalog[service_type][0][self.endpoint_type]
|
|
except KeyError:
|
|
# Endpoint could not be found
|
|
raise EndpointNotFound(service_type)
|
|
|
|
|
|
class Resources(object):
|
|
"""
|
|
Abstract base class for all resources to be removed.
|
|
"""
|
|
def __init__(self, session):
|
|
self.session = session
|
|
|
|
def list(self):
|
|
pass
|
|
|
|
def delete(self, resource):
|
|
"""
|
|
Displays informational message about a resource deletion.
|
|
"""
|
|
logging.info("* Deleting {}.".format(self.resource_str(resource)))
|
|
|
|
def purge(self):
|
|
"Delete all resources."
|
|
c_name = self.__class__.__name__
|
|
logging.info("* Purging {}".format(c_name))
|
|
for resource in self.list():
|
|
retry(c_name)(self.delete)(resource)
|
|
|
|
def dump(self):
|
|
"Display all available resources."
|
|
c_name = self.__class__.__name__
|
|
print "Resources type: {}".format(c_name)
|
|
for resource in self.list():
|
|
print self.resource_str(resource)
|
|
|
|
|
|
class SwiftResources(Resources):
|
|
|
|
def __init__(self, session):
|
|
super(SwiftResources, self).__init__(session)
|
|
self.endpoint = self.session.get_endpoint("object-store")
|
|
self.token = self.session.token
|
|
|
|
# This method is used to retrieve Objects as well as Containers.
|
|
def list_containers(self):
|
|
containers = swift_client.get_account(self.endpoint, self.token)[1]
|
|
return (cont['name'] for cont in containers)
|
|
|
|
class SwiftObjects(SwiftResources):
|
|
|
|
def list(self):
|
|
swift_objects = []
|
|
for cont in self.list_containers():
|
|
objs = [{'container': cont, 'name': obj['name']} for obj in
|
|
swift_client.get_container(self.endpoint, self.token, cont)[1]]
|
|
swift_objects.extend(objs)
|
|
return swift_objects
|
|
|
|
def delete(self, obj):
|
|
super(SwiftObjects, self).delete(obj)
|
|
swift_client.delete_object(self.endpoint, token=self.token,
|
|
container=obj['container'], name=obj['name'])
|
|
|
|
def resource_str(self, obj):
|
|
return "object {} in container {}".format(obj['name'], obj['container'])
|
|
|
|
|
|
class SwiftContainers(SwiftResources):
|
|
|
|
def list(self):
|
|
return self.list_containers()
|
|
|
|
def delete(self, container):
|
|
"""Container must be empty for deletion to succeed."""
|
|
super(SwiftContainers, self).delete(container)
|
|
swift_client.delete_container(self.endpoint, self.token, container)
|
|
|
|
def resource_str(self, obj):
|
|
return "container {}".format(obj)
|
|
|
|
|
|
class CinderResources(Resources):
|
|
def __init__(self, session):
|
|
super(CinderResources, self).__init__(session)
|
|
# Cinder client library can't use an existing token. When
|
|
# using this library, we have to reauthenticate.
|
|
self.client = cinder_client.Client(
|
|
session.username, session.password,
|
|
session.project_name, session.auth_url,
|
|
endpoint_type=session.endpoint_type)
|
|
|
|
|
|
class CinderSnapshots(CinderResources):
|
|
def list(self):
|
|
return self.client.volume_snapshots.list()
|
|
|
|
def delete(self, snap):
|
|
super(CinderResources, self).delete(snap)
|
|
self.client.volume_snapshots.delete(snap)
|
|
|
|
def resource_str(self, snap):
|
|
return "snapshot {} (id {})".format(snap.display_name, snap.id)
|
|
|
|
|
|
class CinderVolumes(CinderResources):
|
|
def list(self):
|
|
return self.client.volumes.list()
|
|
|
|
def delete(self, vol):
|
|
"""Snapshots created from the volume must be deleted first"""
|
|
super(CinderVolumes, self).delete(vol)
|
|
self.client.volumes.delete(vol)
|
|
|
|
def resource_str(self, vol):
|
|
return "volume {} (id {})".format(vol.display_name, vol.id)
|
|
|
|
|
|
class NeutronResources(Resources):
|
|
def __init__(self, session):
|
|
super(NeutronResources, self).__init__(session)
|
|
self.client = neutron_client.Client(
|
|
username=session.username, password=session.password,
|
|
tenant_name=session.project_name, auth_url=session.auth_url,
|
|
endpoint_type=session.endpoint_type)
|
|
self.project_id = session.project_id
|
|
|
|
# This method is used for routers and interfaces removal
|
|
def list_routers(self):
|
|
return filter(self._owned_resource, self.client.list_routers()['routers'])
|
|
|
|
def _owned_resource(self, res):
|
|
# Only considering resources owned by project
|
|
return res['tenant_id'] == self.project_id
|
|
|
|
|
|
class NeutronRouters(NeutronResources):
|
|
|
|
def list(self):
|
|
return self.list_routers()
|
|
|
|
def delete(self, router):
|
|
"""interfaces must be deleted first"""
|
|
super(NeutronRouters, self).delete(router)
|
|
# Remove router gateway prior to remove the router itself
|
|
self.client.remove_gateway_router(router['id'])
|
|
self.client.delete_router(router['id'])
|
|
|
|
def resource_str(self, router):
|
|
return "router {} (id {})".format(router['name'], router['id'])
|
|
|
|
|
|
class NeutronInterfaces(NeutronResources):
|
|
|
|
def list(self):
|
|
def get_ports(router):
|
|
# Only considering "router_interface" ports (not gateways)
|
|
ports = [port for port in
|
|
self.client.list_ports(device_id=router['id'])['ports']
|
|
if port["device_owner"] == "network:router_interface"]
|
|
return [{'router_id': router['id'], 'interface_id': port['id']}
|
|
for port in ports ]
|
|
interfaces = [get_ports(rout) for rout in self.list_routers()]
|
|
return itertools.chain(*interfaces)
|
|
|
|
def delete(self, interface):
|
|
super(NeutronInterfaces, self).delete(interface)
|
|
self.client.remove_interface_router(interface['router_id'],
|
|
{'port_id':interface['interface_id']})
|
|
|
|
def resource_str(self, interface):
|
|
return "interfaces {} (id)".format(interface['interface_id'])
|
|
|
|
class NeutronNetworks(NeutronResources):
|
|
|
|
def list(self):
|
|
return filter(self._owned_resource,
|
|
self.client.list_networks()['networks'])
|
|
|
|
def delete(self, net):
|
|
"""
|
|
Interfaces connected to the network must be deleted first.
|
|
Implying there must not be any VM on the network.
|
|
"""
|
|
super(NeutronNetworks, self).delete(net)
|
|
self.client.delete_network(net['id'])
|
|
|
|
def resource_str(self, net):
|
|
return "network {} (id {})".format(net['name'], net['id'])
|
|
|
|
class NeutronSecgroups(NeutronResources):
|
|
|
|
def list(self):
|
|
return filter(self._owned_resource,
|
|
self.client.list_security_groups()['security_groups'])
|
|
|
|
def delete(self, secgroup):
|
|
"""VMs using the security group should be deleted first"""
|
|
super(NeutronSecgroups, self).delete(secgroup)
|
|
self.client.delete_security_group(secgroup['id'])
|
|
|
|
def resource_str(self, secgroup):
|
|
return "security group {} (id {})".format(
|
|
secgroup['name'], secgroup['id'])
|
|
|
|
|
|
class NeutronFloatingIps(NeutronResources):
|
|
def list(self):
|
|
return filter(self._owned_resource,
|
|
self.client.list_floatingips()['floatingips'])
|
|
|
|
def delete(self, floating_ip):
|
|
super(NeutronFloatingIps, self).delete(floating_ip)
|
|
self.client.delete_floatingip(floating_ip['id'])
|
|
|
|
def resource_str(self, floating_ip):
|
|
return "floating ip {} (id {})".format(
|
|
floating_ip['floating_ip_address'], floating_ip['id'])
|
|
|
|
|
|
class NovaServers(Resources):
|
|
def __init__(self, session):
|
|
super(NovaServers, self).__init__(session)
|
|
self.client = nova_client.Client(
|
|
session.username, session.password,
|
|
session.project_name, auth_url=session.auth_url,
|
|
endpoint_type=session.endpoint_type)
|
|
self.project_id = session.project_id
|
|
|
|
"""Manage nova resources"""
|
|
def list(self):
|
|
return self.client.servers.list()
|
|
|
|
def delete(self, server):
|
|
super(NovaServers, self).delete(server)
|
|
self.client.servers.delete(server)
|
|
|
|
def resource_str(self, server):
|
|
return "server {} (id {})".format(server.name, server.id)
|
|
|
|
|
|
class GlanceImages(Resources):
|
|
def __init__(self, session):
|
|
self.client = glance_client.Client(
|
|
endpoint=session.get_endpoint("image"),
|
|
token=session.token)
|
|
self.project_id = session.project_id
|
|
|
|
def list(self):
|
|
return filter(self._owned_resource, self.client.images.list())
|
|
|
|
def delete(self, image):
|
|
super(GlanceImages, self).delete(image)
|
|
self.client.images.delete(image.id)
|
|
|
|
def resource_str(self, image):
|
|
return "image {} (id {})".format(image.name, image.id)
|
|
|
|
def _owned_resource(self, res):
|
|
# Only considering resources owned by project
|
|
return res.owner == self.project_id
|
|
|
|
|
|
class CeilometerAlarms(Resources):
|
|
|
|
def __init__(self, session):
|
|
# Ceilometer Client needs a method that returns the token
|
|
def get_token():
|
|
return session.token
|
|
self.client = ceilometer_client.Client(
|
|
endpoint=session.get_endpoint("metering"),
|
|
token=get_token)
|
|
self.project_id = session.project_id
|
|
|
|
def list(self):
|
|
query = [{'field': 'project_id',
|
|
'op': 'eq',
|
|
'value': self.project_id}]
|
|
return self.client.alarms.list(q=query)
|
|
|
|
def delete(self, alarm):
|
|
super(CeilometerAlarms, self).delete(alarm)
|
|
self.client.alarms.delete(alarm.alarm_id)
|
|
|
|
def resource_str(self, alarm):
|
|
return "alarm {}".format(alarm.name)
|
|
|
|
|
|
class KeystoneManager(object):
|
|
"""Manages Keystone queries"""
|
|
|
|
def __init__(self, username, password, project, auth_url):
|
|
self.client = keystone_client.Client(
|
|
username=username, password=password,
|
|
tenant_name=project, auth_url=auth_url)
|
|
|
|
def become_project_admin(self, project_name):
|
|
user_id = self.client.user_id
|
|
logging.info("* Granting role admin to user {} on project {}.".format(
|
|
user_id, project_name))
|
|
|
|
try:
|
|
tenants = self.client.tenants.list()
|
|
except Exception as e:
|
|
raise ConnectionFailed(str(e))
|
|
try:
|
|
project_id = filter(lambda x : x.name==project_name, tenants)[0].id
|
|
except:
|
|
raise NoSuchProject(project_name)
|
|
|
|
roles = self.client.roles.list()
|
|
role_id = filter(lambda x : x.name=="admin", roles)[0].id
|
|
try:
|
|
return self.client.roles.add_user_role(user_id, role_id, project_id)
|
|
except api_exceptions.Conflict:
|
|
# user is already admin on the target project
|
|
pass
|
|
|
|
def delete_project(self, project_name):
|
|
tenants = self.client.tenants.list()
|
|
project_id = filter(lambda x : x.name==project_name, tenants)[0].id
|
|
logging.info("* Deleting project {}.".format(project_name))
|
|
self.client.tenants.delete(project_id)
|
|
|
|
|
|
def purge_project(admin_name, password, project, auth_url,
|
|
endpoint_type='publicURL'):
|
|
"""
|
|
project is the project that will be purged.
|
|
|
|
Warning: admin must have access to the project.
|
|
"""
|
|
session = Session(admin_name, password, project,
|
|
auth_url, endpoint_type)
|
|
for rc in RESOURCES_CLASSES:
|
|
try:
|
|
resources = globals()[rc](session)
|
|
except EndpointNotFound:
|
|
# If service is not in Keystone's services catalog, ignoring it
|
|
pass
|
|
else:
|
|
resources.purge()
|
|
|
|
def list_resources(admin_name, password, project, auth_url,
|
|
endpoint_type='publicURL'):
|
|
"""
|
|
Listing resources of given project.
|
|
"""
|
|
session = Session(admin_name, password, project,
|
|
auth_url, endpoint_type)
|
|
for rc in RESOURCES_CLASSES:
|
|
try:
|
|
resources = globals()[rc](session)
|
|
except EndpointNotFound:
|
|
pass
|
|
else:
|
|
resources.dump()
|
|
|
|
|
|
# From Russell Heilling
|
|
# http://stackoverflow.com/questions/10551117/setting-options-from-environment-variables-when-using-argparse
|
|
class EnvDefault(argparse.Action):
|
|
def __init__(self, envvar, required=True, default=None, **kwargs):
|
|
# Overriding default with environment variable if available
|
|
if envvar in os.environ:
|
|
default = os.environ[envvar]
|
|
if required and default:
|
|
required = False
|
|
super(EnvDefault, self).__init__(default=default, required=required,
|
|
**kwargs)
|
|
|
|
def __call__(self, parser, namespace, values, option_string=None):
|
|
setattr(namespace, self.dest, values)
|
|
|
|
|
|
def parse_args():
|
|
desc = "Purge resources from an Openstack project."
|
|
parser = argparse.ArgumentParser(description=desc)
|
|
parser.add_argument("--verbose", action="store_true",
|
|
dest="verbose",
|
|
help="Makes output verbose")
|
|
parser.add_argument("--dry_run", action="store_true",
|
|
dest="dry_run",
|
|
help="List project's resources")
|
|
parser.add_argument("--endpoint_type", action=EnvDefault,
|
|
envvar='OS_ENDPOINT_TYPE', default="publicURL",
|
|
help="Endpoint type to use. Defaults to " \
|
|
"env[OS_ENDPOINT_TYPE] or publicURL")
|
|
parser.add_argument("--username", action=EnvDefault,
|
|
envvar='OS_USERNAME', required=True,
|
|
help="A user name with access to the " \
|
|
"project being purged. Defaults " \
|
|
"to env[OS_USERNAME]")
|
|
parser.add_argument("--password", action=EnvDefault,
|
|
envvar='OS_PASSWORD', required=True,
|
|
help="The user's password. Defaults "
|
|
"to env[OS_PASSWORD].")
|
|
parser.add_argument("--admin_project", action=EnvDefault,
|
|
envvar='OS_TENANT_NAME', required=True,
|
|
help="Name of a project the user is admin on. "\
|
|
"Defaults to env[OS_TENANT_NAME].")
|
|
parser.add_argument("--auth_url", action=EnvDefault,
|
|
envvar='OS_AUTH_URL', required=True,
|
|
help="Authentication URL. Defaults to " \
|
|
"env[OS_AUTH_URL].")
|
|
parser.add_argument("--cleanup_project", required=True,
|
|
help="Name of project to purge")
|
|
return parser.parse_args()
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
|
|
if args.verbose:
|
|
logging.basicConfig(level=logging.INFO)
|
|
else:
|
|
# Set default log level to Warning
|
|
logging.basicConfig(level=logging.WARNING)
|
|
|
|
try:
|
|
keystone_manager = KeystoneManager(args.username, args.password,
|
|
args.admin_project, args.auth_url)
|
|
except api_exceptions.Unauthorized as exc:
|
|
print "Authentication failed: {}".format(str(exc))
|
|
sys.exit(AUTHENTICATION_FAILED_ERROR_CODE)
|
|
|
|
try:
|
|
keystone_manager.become_project_admin(args.cleanup_project)
|
|
except NoSuchProject as exc:
|
|
print "Project {} doesn't exist".format(str(exc))
|
|
sys.exit(NoSuchProject.ERROR_CODE)
|
|
except ConnectionFailed as exc:
|
|
print "Connection failed: {}".format(str(exc))
|
|
sys.exit(ConnectionFailed.ERROR_CODE)
|
|
|
|
try:
|
|
if args.dry_run:
|
|
list_resources(args.username, args.password, args.cleanup_project,
|
|
args.auth_url, args.endpoint_type)
|
|
else:
|
|
purge_project(args.username, args.password, args.cleanup_project,
|
|
args.auth_url, args.endpoint_type)
|
|
except ConnectionError as exc:
|
|
print "Connection error: {}".format(str(exc))
|
|
sys.exit(CONNECTION_ERROR_CODE)
|
|
except DeletionFailed as exc:
|
|
print "Deletion of {} failed".format(str(exc))
|
|
sys.exit(DeletionFailed.ERROR_CODE)
|
|
|
|
if args.dry_run is False:
|
|
keystone_manager.delete_project(args.cleanup_project)
|
|
sys.exit(0)
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|