1044 lines
41 KiB
Python
Executable File
1044 lines
41 KiB
Python
Executable File
#!/usr/bin/env python
|
|
#
|
|
# Copyright 2014 Catalyst IT Ltd
|
|
#
|
|
# 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 argparse
|
|
import os
|
|
import sys
|
|
import six
|
|
import traceback
|
|
import prettytable
|
|
|
|
from cinderclient.v1 import client as cinder_client
|
|
from glanceclient import client as glance_client
|
|
from oslo_utils import importutils
|
|
from oslo_utils import encodeutils
|
|
from heatclient import client as heat_client
|
|
from keystoneclient.v2_0 import client as keystone_client
|
|
from neutronclient.v2_0 import client as neutron_client
|
|
try:
|
|
from novaclient.v2 import client as nova_client
|
|
except:
|
|
from novaclient.v3 import client as nova_client
|
|
from swiftclient import client as swift_client
|
|
from swiftclient import exceptions as swift_exceptions
|
|
|
|
DNS_NAMESERVERS = ['202.78.240.213', '202.78.240.214', '202.78.240.215']
|
|
|
|
|
|
def arg(*args, **kwargs):
|
|
def _decorator(func):
|
|
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
|
return func
|
|
return _decorator
|
|
|
|
|
|
class TenantShell(object):
|
|
|
|
def get_base_parser(self):
|
|
parser = argparse.ArgumentParser(
|
|
prog='tenant',
|
|
description='Tenant management script for Catalyst Cloud.',
|
|
add_help=False,
|
|
)
|
|
|
|
# Global arguments
|
|
parser.add_argument('-h', '--help',
|
|
action='store_true',
|
|
help=argparse.SUPPRESS,
|
|
)
|
|
|
|
parser.add_argument('-a', '--os-auth-url', metavar='OS_AUTH_URL',
|
|
type=str, required=False, dest='OS_AUTH_URL',
|
|
default=os.environ.get('OS_AUTH_URL', None),
|
|
help='Keystone Authentication URL')
|
|
|
|
parser.add_argument('-u', '--os-username', metavar='OS_USERNAME',
|
|
type=str, required=False, dest='OS_USERNAME',
|
|
default=os.environ.get('OS_USERNAME', None),
|
|
help='Username for authentication')
|
|
|
|
parser.add_argument('-p', '--os-password', metavar='OS_PASSWORD',
|
|
type=str, required=False, dest='OS_PASSWORD',
|
|
default=os.environ.get('OS_PASSWORD', None),
|
|
help='Password for authentication')
|
|
|
|
parser.add_argument('-t', '--os-tenant-name',
|
|
metavar='OS_TENANT_NAME',
|
|
type=str, required=False,
|
|
dest='OS_TENANT_NAME',
|
|
default=os.environ.get('OS_TENANT_NAME', None),
|
|
help='Tenant name for authentication')
|
|
|
|
parser.add_argument('-r', '--os-region-name',
|
|
metavar='OS_REGION_NAME',
|
|
type=str, required=False,
|
|
dest='OS_REGION_NAME',
|
|
default=os.environ.get('OS_REGION_NAME', None),
|
|
help='Region for authentication')
|
|
|
|
parser.add_argument('-c', '--os-cacert', metavar='OS_CACERT',
|
|
dest='OS_CACERT',
|
|
default=os.environ.get('OS_CACERT'),
|
|
help='Path of CA TLS certificate(s) used to '
|
|
'verify the remote server\'s certificate. '
|
|
'Without this option glance looks for the '
|
|
'default system CA certificates.')
|
|
|
|
parser.add_argument('-k', '--insecure',
|
|
default=False,
|
|
action='store_true', dest='OS_INSECURE',
|
|
help='Explicitly allow script to perform '
|
|
'\"insecure SSL\" (https) requests. '
|
|
'The server\'s certificate will not be '
|
|
'verified against any certificate authorities.'
|
|
' This option should be used with caution.')
|
|
return parser
|
|
|
|
def get_subcommand_parser(self):
|
|
parser = self.get_base_parser()
|
|
self.subcommands = {}
|
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
|
submodule = importutils.import_module('tenant')
|
|
self._find_actions(subparsers, submodule)
|
|
self._find_actions(subparsers, self)
|
|
return parser
|
|
|
|
def _find_actions(self, subparsers, actions_module):
|
|
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
|
command = attr[3:].replace('_', '-')
|
|
callback = getattr(actions_module, attr)
|
|
desc = callback.__doc__ or ''
|
|
help = desc.strip().split('\n')[0]
|
|
arguments = getattr(callback, 'arguments', [])
|
|
|
|
subparser = subparsers.add_parser(command,
|
|
help=help,
|
|
description=desc,
|
|
add_help=False,
|
|
formatter_class=HelpFormatter
|
|
)
|
|
subparser.add_argument('-h', '--help',
|
|
action='help',
|
|
help=argparse.SUPPRESS,
|
|
)
|
|
self.subcommands[command] = subparser
|
|
for (args, kwargs) in arguments:
|
|
subparser.add_argument(*args, **kwargs)
|
|
subparser.set_defaults(func=callback)
|
|
|
|
@arg('command', metavar='<subcommand>', nargs='?',
|
|
help='Display help for <subcommand>.')
|
|
def do_help(self, args):
|
|
"""Display help about this program or one of its subcommands.
|
|
|
|
"""
|
|
if getattr(args, 'command', None):
|
|
if args.command in self.subcommands:
|
|
self.subcommands[args.command].print_help()
|
|
else:
|
|
raise Exception("'%s' is not a valid subcommand" %
|
|
args.command)
|
|
else:
|
|
self.parser.print_help()
|
|
|
|
def init_client(self, args):
|
|
try:
|
|
keystone = keystone_client.Client(username=args.OS_USERNAME,
|
|
password=args.OS_PASSWORD,
|
|
tenant_name=args.OS_TENANT_NAME,
|
|
auth_url=args.OS_AUTH_URL,
|
|
region_name=args.OS_REGION_NAME,
|
|
cacert=args.OS_CACERT,
|
|
insecure=args.OS_INSECURE)
|
|
self.keystone = keystone
|
|
except Exception as e:
|
|
# FIXME(flwang): Improve the exception catching
|
|
raise e
|
|
|
|
try:
|
|
neutron = neutron_client.Client(username=args.OS_USERNAME,
|
|
password=args.OS_PASSWORD,
|
|
tenant_name=args.OS_TENANT_NAME,
|
|
auth_url=args.OS_AUTH_URL,
|
|
region_name=args.OS_REGION_NAME,
|
|
insecure=args.OS_INSECURE)
|
|
self.neutron = neutron
|
|
except Exception as e:
|
|
raise e
|
|
|
|
try:
|
|
nova = nova_client.Client(username=args.OS_USERNAME,
|
|
password=args.OS_PASSWORD,
|
|
project_id=args.OS_TENANT_NAME,
|
|
auth_url=args.OS_AUTH_URL,
|
|
region_name=args.OS_REGION_NAME,
|
|
insecure=args.OS_INSECURE)
|
|
self.nova = nova
|
|
except Exception as e:
|
|
raise e
|
|
|
|
try:
|
|
client_kwargs = {
|
|
'token': self.keystone.auth_token,
|
|
'insecure': args.OS_INSECURE
|
|
}
|
|
|
|
endpoint_kwargs = {
|
|
'service_type': 'image',
|
|
'endpoint_type': 'publicURL',
|
|
}
|
|
|
|
if args.OS_REGION_NAME:
|
|
endpoint_kwargs['attr'] = 'region'
|
|
endpoint_kwargs['filter_value'] = args.OS_REGION_NAME
|
|
|
|
endpoint = keystone.service_catalog.url_for(**endpoint_kwargs)
|
|
glance = glance_client.Client('1', endpoint, **client_kwargs)
|
|
self.glance = glance
|
|
except Exception as e:
|
|
raise e
|
|
|
|
try:
|
|
cinder = cinder_client.Client(args.OS_USERNAME,
|
|
args.OS_PASSWORD,
|
|
tenant_id=keystone.auth_tenant_id,
|
|
auth_url=args.OS_AUTH_URL,
|
|
region_name=args.OS_REGION_NAME,
|
|
insecure=args.OS_INSECURE)
|
|
self.cinder = cinder
|
|
except Exception as e:
|
|
raise e
|
|
|
|
def main(self, argv):
|
|
parser = self.get_base_parser()
|
|
(options, args) = parser.parse_known_args(argv)
|
|
|
|
subcommand_parser = self.get_subcommand_parser()
|
|
self.parser = subcommand_parser
|
|
|
|
if options.help or not argv:
|
|
self.do_help(options)
|
|
return 0
|
|
|
|
args = subcommand_parser.parse_args(argv)
|
|
if args.func == self.do_help:
|
|
self.do_help(args)
|
|
return 0
|
|
|
|
try:
|
|
self.init_client(args)
|
|
args.func(self, args)
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
class HelpFormatter(argparse.HelpFormatter):
|
|
def start_section(self, heading):
|
|
# Title-case the headings
|
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
|
super(HelpFormatter, self).start_section(heading)
|
|
|
|
|
|
@arg('--tenant-name', type=str, metavar='TENANT_NAME', dest='TENANT_NAME',
|
|
help='New tenant name (must be unique).')
|
|
@arg('--tenant-description', type=str, default=None, metavar='TENANT_DESC',
|
|
dest='TENANT_DESC', help='Description of new tenant. Default is none.')
|
|
@arg('--meter-label-name', type=str, metavar='METER_LABEL_NAME',
|
|
dest='METER_LABEL_NAME', help='Neutron meter label name which will be '
|
|
'associated with the tenant')
|
|
@arg('--meter-label-description', type=str, default=None,
|
|
metavar='METER_LABEL_DESC', dest='METER_LABEL_DESC',
|
|
help='Description of new meter label. Default is none.')
|
|
@arg('--network-name', type=str, required=False, metavar='NETWORK_NAME',
|
|
dest='NETWORK_NAME', help='New network name for the tenant.')
|
|
@arg('--subnet-name', type=str, required=False, metavar='SUBNET_NAME',
|
|
dest='SUBNET_NAME', help='New subnet name for the new network.')
|
|
@arg('--subnet-cidr', type=str, required=False, metavar='SUBNET_CIDR',
|
|
dest='SUBNET_CIDR', help='Subnet IP range with CIDR format.')
|
|
@arg('--dns-server', type=str, required=False, metavar='DNS_SERVER',
|
|
action='append', dest='DNS_SERVER',
|
|
help='DNS server, following the format like: '
|
|
'--dns-server 10.0.0.1 --dns-server 10.0.0.2')
|
|
@arg('--router-name', type=str, required=False, metavar='ROUTER_NAME',
|
|
dest='ROUTER_NAME', help='Router name associated with the new network.')
|
|
@arg('--public-network', type=str, required=False,
|
|
metavar='PUBLIC_NETWORK', dest='PUBLIC_NETWORK',
|
|
help='Public network id which the new router will use.')
|
|
@arg('--meter-rule-direction', type=str, required=False, default='both',
|
|
choices=('ingress', 'egress', 'both'),
|
|
metavar='METER_RULE_DIRECTION', dest='METER_RULE_DIRECTION',
|
|
help='Direction of meter label rule.')
|
|
def do_init(shell, args):
|
|
"""Add a new tenant and initialize related resources."""
|
|
print('>>> CHECK CAPACITY')
|
|
raw_input('CAUTION: Sort out capacity planning first.')
|
|
|
|
print('>>> CREATE TENANT')
|
|
tenant_id = create_tenant(shell.keystone, args)
|
|
|
|
print('>>> ADD ADMIN TO THE NEW TENANT')
|
|
add_admin_to_tenant(shell.keystone, tenant_id)
|
|
|
|
print('>>> CREATE NETWORK')
|
|
network_id = create_network(shell.neutron, tenant_id, args.NETWORK_NAME)
|
|
|
|
print('>>> CREATE SUBNET')
|
|
subnet = create_subnet(shell.neutron, tenant_id, network_id,
|
|
args.SUBNET_NAME, args.SUBNET_CIDR,
|
|
args.DNS_SERVER)
|
|
|
|
print('>>> CREATE ROUTER')
|
|
router = create_router(shell.neutron, tenant_id, args.PUBLIC_NETWORK,
|
|
args.ROUTER_NAME)
|
|
|
|
print('>>> CREATE INTERFACE')
|
|
create_interface(shell.neutron, router, subnet['subnet']['id'])
|
|
|
|
# Comment out metering label before we fixed the issue when it works with
|
|
# VPNaaS.
|
|
# print('>>> CREATE METER LABEL')
|
|
# meter_label_id = create_meter_label(shell.neutron, args, tenant_id)
|
|
|
|
# print('>>> CREATE METER LABEL RULE')
|
|
# direction = args.METER_RULE_DIRECTION
|
|
# if direction in ('ingress', 'egress'):
|
|
# create_meter_label_rule(shell.neutron, meter_label_id, direction)
|
|
# elif direction == 'both':
|
|
# create_meter_label_rule(shell.neutron, meter_label_id, 'ingress')
|
|
# create_meter_label_rule(shell.neutron, meter_label_id, 'egress')
|
|
|
|
print('>>> COMPLETE SUCCESSFULLY')
|
|
|
|
|
|
@arg('--label-name-template', type=str, metavar='LABEL_NAME_TEMPLATE',
|
|
dest='LABEL_NAME_TEMPLATE', default='meter-label-{0}',
|
|
help='Define a name template to add meter label for existed tenants. '
|
|
'Such as: meter-label-{0}, {0} will be replaced by the tenant name'
|
|
' automatically.')
|
|
@arg('--meter-rule-direction', type=str, required=False, default='both',
|
|
choices=('ingress', 'egress', 'both'),
|
|
metavar='METER_RULE_DIRECTION', dest='METER_RULE_DIRECTION',
|
|
help='Direction of meter label rule.')
|
|
def do_meter(shell, args):
|
|
"""Add meter label for all existed tenants for network traffic billing.
|
|
|
|
"""
|
|
print('>>> TENANT LIST MISSING METER LABEL')
|
|
tenants = shell.keystone.tenants.list()
|
|
meter_labels = shell.neutron.list_metering_labels()
|
|
|
|
dict_tenants = {}
|
|
for tenant in tenants:
|
|
dict_tenants[tenant.id] = tenant
|
|
for label in meter_labels['metering_labels']:
|
|
if label['tenant_id'] in dict_tenants.keys():
|
|
del dict_tenants[label['tenant_id']]
|
|
print_list(dict_tenants.values(), ['id', 'name', 'enabled'])
|
|
if len(dict_tenants.values()) <= 0:
|
|
print('>>> ALL TENANTS HAVE METER LABEL')
|
|
return 0
|
|
answer = raw_input('Create meter label/rules for above tenants(Y/n)?')
|
|
if answer.lower() == 'y':
|
|
for tenant in dict_tenants.values():
|
|
label_name = args.LABEL_NAME_TEMPLATE.format(tenant.name)
|
|
args.METER_LABEL_NAME = label_name
|
|
args.METER_LABEL_DESC = 'Meter label of {0}'.format(tenant.name)
|
|
meter_label_id = create_meter_label(shell.neutron, args, tenant.id)
|
|
if meter_label_id:
|
|
direction = args.METER_RULE_DIRECTION
|
|
if direction in ('ingress', 'egress'):
|
|
create_meter_label_rule(shell.neutron, meter_label_id,
|
|
direction)
|
|
elif direction == 'both':
|
|
create_meter_label_rule(shell.neutron, meter_label_id,
|
|
'ingress')
|
|
create_meter_label_rule(shell.neutron, meter_label_id,
|
|
'egress')
|
|
print('>>> COMPLETE SUCCESSFULLY')
|
|
|
|
|
|
@arg('--tenant-id', type=str, metavar='TENANT_ID',
|
|
dest='TENANT_ID', required=True,
|
|
help='ID of the tenant to be deleted.')
|
|
@arg('--auto-clean', type=bool, metavar='AUTO_CLEAN',
|
|
dest='AUTO_CLEAN', default=False,
|
|
help='Auto clean all resources.')
|
|
def do_delete(shell, args):
|
|
"""Delete tenant and all resources associated with it to avoid leaving
|
|
any legacy stuff
|
|
"""
|
|
|
|
tenant = shell.keystone.tenants.get(args.TENANT_ID)
|
|
shell.tenant = tenant
|
|
|
|
if not prompt_yes_no('Tenant [%s] will be deleted. '
|
|
'Please confirm to continue.' % tenant.name,
|
|
default='yes'):
|
|
return
|
|
|
|
component_list = ['nova', 'neutron', 'cinder', 'glance', 'swift', 'heat',
|
|
'keystone']
|
|
|
|
myself = __import__('tenant')
|
|
for component in component_list:
|
|
callback = getattr(myself, component.lower() + '_delete')
|
|
print('>>> TO DELETE ' + component.upper())
|
|
try:
|
|
callback(shell, args)
|
|
except Exception as e:
|
|
print(e)
|
|
print('\n\nTenant %s has been cleaned up based on above selections.' %
|
|
args.TENANT_ID)
|
|
|
|
|
|
def keystone_delete(shell, args):
|
|
try:
|
|
if(args.AUTO_CLEAN or prompt_yes_no('Please confirm to delete the'
|
|
' tenant from Keystone:')):
|
|
shell.keystone.tenants.delete(args.TENANT_ID)
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def nova_delete(shell, args):
|
|
# NOTE(flwang): Seems there is a bug for nova, it doesn't honour the
|
|
# project id though based on the code it does. Will dig it later.
|
|
print('>>>>>> SERVERS(VM) LIST')
|
|
servers = shell.nova.servers.list(search_opts={'all_tenants': True})
|
|
servers = [s for s in servers if s.tenant_id == args.TENANT_ID]
|
|
print_list(servers, ['id', 'name', 'status', 'tenant_id'])
|
|
|
|
if (len(servers) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for server in servers:
|
|
shell.nova.servers.delete(server.id)
|
|
|
|
|
|
def glance_delete(shell, args):
|
|
images = shell.glance.images.list(owner=args.TENANT_ID)
|
|
# NOTE(flwang): Make sure the images are what we want to delete
|
|
images = [img for img in images if img.owner == args.TENANT_ID]
|
|
print_list(images, ['id', 'name', 'owner'])
|
|
|
|
if (len(images) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for image in images:
|
|
shell.glance.images.delete(image.id)
|
|
|
|
|
|
def cinder_delete(shell, args):
|
|
print('>>>>>> VOLUME SNAPSHOTS')
|
|
# Delete snapshots
|
|
snapshots = shell.cinder.volume_snapshots.list(search_opts={'all_tenants':
|
|
True})
|
|
tenant_attr = 'os-extended-snapshot-attributes:project_id'
|
|
# NOTE(flwang): If the script user is admin then it will get all the
|
|
# snapshot and each snapshot will have the attribute
|
|
# 'os-extended-snapshot-attributes:project_id' to indicate the tenant.
|
|
# For non-admin, there is no that attribute.
|
|
user_roles = shell.keystone.session.auth.auth_ref['user']['roles']
|
|
if {u'name': u'admin'} in user_roles:
|
|
snapshots = [v for v in snapshots
|
|
if getattr(v, tenant_attr) == args.TENANT_ID]
|
|
print_list(snapshots, ['id', 'display_name', 'status', tenant_attr])
|
|
else:
|
|
print_list(snapshots, ['id', 'display_name', 'status'])
|
|
|
|
if (len(snapshots) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for snapshot in snapshots:
|
|
shell.cinder.volume_snapshots.delete(snapshot.id)
|
|
|
|
print('>>>>>> VOLUMES')
|
|
# Delete volumes
|
|
volumes = shell.cinder.volumes.list(search_opts={'all_tenants': True})
|
|
tenant_attr = 'os-vol-tenant-attr:tenant_id'
|
|
if {u'name': u'admin'} in user_roles:
|
|
volumes = [v for v in volumes
|
|
if getattr(v, tenant_attr) == args.TENANT_ID]
|
|
print_list(volumes, ['id', 'display_name', 'status', tenant_attr])
|
|
else:
|
|
print_list(volumes, ['id', 'display_name', 'status'])
|
|
|
|
if (len(volumes) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for volume in volumes:
|
|
shell.cinder.volumes.delete(volume.id)
|
|
|
|
|
|
def swift_delete(shell, args):
|
|
endpoint = shell.keystone.service_catalog.url_for(service_type='object-store') # noqa
|
|
url = endpoint.split('_')[0] + '_' + args.TENANT_ID
|
|
|
|
try:
|
|
# Get a specific token for swift
|
|
ks = keystone_client.Client(username=args.OS_USERNAME,
|
|
password=args.OS_PASSWORD,
|
|
tenant_name=shell.tenant.name,
|
|
auth_url=args.OS_AUTH_URL,
|
|
region_name=args.OS_REGION_NAME,
|
|
cacert=args.OS_CACERT,
|
|
insecure=args.OS_INSECURE)
|
|
account = getattr(swift_client, 'get_account')(url, ks.auth_token)
|
|
print_list(account[1], ['name', 'count', 'bytes'])
|
|
if (len(account[1]) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for co in account[1]:
|
|
co_obj = getattr(swift_client, 'get_container')(url,
|
|
ks.auth_token,
|
|
co['name'])
|
|
# Delete objects firstly
|
|
for obj in co_obj[1]:
|
|
getattr(swift_client,
|
|
'delete_object')(url, ks.auth_token,
|
|
container=co['name'],
|
|
name=obj['name'])
|
|
# Delete container
|
|
getattr(swift_client, 'delete_container')(url,
|
|
ks.auth_token,
|
|
co['name'])
|
|
except swift_exceptions.ClientException as e:
|
|
print('%s %s' % (e.http_status, e.http_reason))
|
|
|
|
|
|
def neutron_delete(shell, args):
|
|
# vpn service
|
|
_delete_targeted_tenant_resource(shell, args, 'ipsec_site_connection')
|
|
_delete_targeted_tenant_resource(shell, args, 'ipsecpolicy')
|
|
_delete_targeted_tenant_resource(shell, args, 'ikepolicy')
|
|
_delete_targeted_tenant_resource(shell, args, 'vpnservice')
|
|
|
|
# meter label and rules
|
|
print('>>>>>> METER LABEL RULE LIST')
|
|
if not hasattr(shell.neutron, 'list_metering_labels'):
|
|
return
|
|
metering_labels = shell.neutron.list_metering_labels()['metering_labels']
|
|
metering_label_rules = shell.neutron.list_metering_label_rules()
|
|
metering_label_rules = metering_label_rules['metering_label_rules']
|
|
|
|
targeted_rules = []
|
|
for rule in metering_label_rules:
|
|
if rule['metering_label_id'] in [m['id'] for m in metering_labels
|
|
if m['tenant_id'] == args.TENANT_ID]:
|
|
targeted_rules.append(rule)
|
|
print_list(targeted_rules, ['id', 'name', 'tenant_id'])
|
|
|
|
if (len(targeted_rules) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for rule in targeted_rules:
|
|
shell.neutron.delete_metering_label_rule(rule['id'])
|
|
|
|
print('>>>>>> METER LABEL LIST')
|
|
metering_labels = [m for m in metering_labels
|
|
if m['tenant_id'] == args.TENANT_ID]
|
|
print_list(metering_labels, ['id', 'name', 'tenant_id'])
|
|
if (len(metering_labels) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for label in metering_labels:
|
|
shell.neutron.delete_metering_label(label['id'])
|
|
|
|
# port
|
|
print('>>>>>> PORT LIST')
|
|
ports = shell.neutron.list_ports()
|
|
targeted_ports = []
|
|
for port in ports['ports']:
|
|
if port['tenant_id'] == args.TENANT_ID:
|
|
targeted_ports.append(port)
|
|
print_list(targeted_ports, ['id', 'name', 'tenant_id'])
|
|
|
|
if (len(targeted_ports) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for port in targeted_ports:
|
|
try:
|
|
if port['device_owner'] == 'network:router_gateway':
|
|
shell.neutron.remove_gateway_router(port['device_id'])
|
|
elif port['device_owner'] == 'network:router_interface':
|
|
for subnet in port['fixed_ips']:
|
|
body = {'subnet_id': subnet['subnet_id']}
|
|
shell.neutron.remove_interface_router(
|
|
port['device_id'], body)
|
|
else:
|
|
shell.neutron.delete_port(port['id'])
|
|
except Exception as e:
|
|
print('Failed to delete port:{0}, see: {1}'.
|
|
format(port['id'], str(e)))
|
|
continue
|
|
|
|
# security group
|
|
_delete_targeted_tenant_resource(shell, args, 'security_group')
|
|
|
|
# floating IP
|
|
_delete_targeted_tenant_resource(shell, args, 'floatingip')
|
|
|
|
# sub net
|
|
_delete_targeted_tenant_resource(shell, args, 'subnet')
|
|
|
|
# router
|
|
_delete_targeted_tenant_resource(shell, args, 'router')
|
|
|
|
# network
|
|
_delete_targeted_tenant_resource(shell, args, 'network')
|
|
|
|
|
|
def heat_delete(shell, args):
|
|
print('>>>>>> STACKS LIST')
|
|
try:
|
|
heat_srv = shell.keystone.services.find(type='orchestration')
|
|
heat_endpoint = shell.keystone.endpoints.find(service_id=heat_srv.id)
|
|
heat_url = heat_endpoint.publicurl.replace('$(tenant_id)s',
|
|
args.TENANT_ID)
|
|
heat = heat_client.Client('1', endpoint=heat_url,
|
|
token=shell.keystone.auth_token)
|
|
except Exception as e:
|
|
raise e
|
|
|
|
stacks = heat.stacks.list()
|
|
stacks = [s for s in stacks]
|
|
print_list(stacks, ['id', 'stack_name', 'stack_status'])
|
|
|
|
if (len(stacks) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
for stack in stacks:
|
|
heat.stacks.delete(stack.id)
|
|
|
|
|
|
def _delete_targeted_tenant_resource(shell, args, resource):
|
|
print('>>>>>> %s LIST' % resource.upper())
|
|
list_command = 'list_{0}s'.format(resource)
|
|
if resource in ('ipsecpolicy', 'ikepolicy'):
|
|
list_command = list_command.replace('policy', 'policie')
|
|
resources = getattr(shell.neutron, list_command)()
|
|
|
|
resp_key = resource + 's'
|
|
if resource in ('ipsecpolicy', 'ikepolicy'):
|
|
resp_key = resp_key.replace('policy', 'policie')
|
|
delete_resources = []
|
|
for res in resources[resp_key]:
|
|
if res['tenant_id'] == args.TENANT_ID:
|
|
delete_resources.append(res)
|
|
print_list(delete_resources, ['id', 'name', 'tenant_id'])
|
|
|
|
if (len(delete_resources) and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Please confirm:'))):
|
|
delete_function = getattr(shell.neutron,
|
|
'delete_{0}'.format(resource))
|
|
for res in delete_resources:
|
|
delete_function(res['id'])
|
|
|
|
|
|
@arg('--auto-clean', type=bool, metavar='AUTO_CLEAN',
|
|
dest='AUTO_CLEAN', default=False,
|
|
help='Auto clean legacy resources.')
|
|
@arg('--component', type=str, metavar='COMPONENT',
|
|
dest='COMPONENT',
|
|
help='Specific component to audit.')
|
|
def do_audit(shell, args):
|
|
"""Audit all existed tenants to make sure everything is OK.
|
|
|
|
Tenant deletion related blueprints:
|
|
[1] https://blueprints.launchpad.net/keystone/+spec/notifications
|
|
[2] https://blueprints.launchpad.net/neutron/+spec/tenant-delete
|
|
"""
|
|
user_roles = shell.keystone.session.auth.auth_ref['user']['roles']
|
|
if {u'name': u'admin'} not in user_roles:
|
|
print('Admin permission is required.')
|
|
return
|
|
|
|
component_list = ['nova', 'neutron', 'cinder', 'glance']
|
|
|
|
tenants = shell.keystone.tenants.list()
|
|
tenant_ids = [t.id for t in tenants]
|
|
|
|
myself = __import__('tenant')
|
|
if args.COMPONENT:
|
|
callback = getattr(myself, args.COMPONENT.lower() + '_audit')
|
|
print('>>> AUDITING ' + args.COMPONENT.upper())
|
|
callback(shell, args, tenant_ids)
|
|
else:
|
|
for component in component_list:
|
|
callback = getattr(myself, component.lower() + '_audit')
|
|
print('>>> AUDITING ' + component.upper())
|
|
callback(shell, args, tenant_ids)
|
|
|
|
|
|
def nova_audit(shell, args, tenant_ids):
|
|
# instance
|
|
print('>>>>>> ZOMBIE INSTANCE LIST')
|
|
servers = shell.nova.servers.list(search_opts={'all_tenants': True})
|
|
zombie_servers = [s for s in servers if s.tenant_id not in tenant_ids]
|
|
print_list(zombie_servers, ['id', 'name', 'tenant_id'])
|
|
|
|
if ((len(zombie_servers) and
|
|
(args.AUTO_CLEAN or prompt_yes_no('Confirm to delete:')))):
|
|
for server in zombie_servers:
|
|
shell.nova.servers.delete(server.id)
|
|
|
|
|
|
def neutron_audit(shell, args, tenant_ids):
|
|
# Clean up services, FWaaS, LBaaS, etc
|
|
_clean_up_resource(shell, args, tenant_ids, 'ipsec_site_connection')
|
|
_clean_up_resource(shell, args, tenant_ids, 'ipsecpolicy')
|
|
_clean_up_resource(shell, args, tenant_ids, 'ikepolicy')
|
|
_clean_up_resource(shell, args, tenant_ids, 'vpnservice')
|
|
|
|
# metering label
|
|
_clean_up_resource(shell, args, tenant_ids, 'metering_label')
|
|
|
|
# meter label rule
|
|
print('>>>>>> ZOMBIE METER LABEL RULE LIST')
|
|
metering_labels = shell.neutron.list_metering_labels()
|
|
metering_label_ids = [m['id'] for m in metering_labels['metering_labels']]
|
|
metering_label_rules = shell.neutron.list_metering_label_rules()
|
|
zombie_metering_label_rules = []
|
|
for metering_label_rule in metering_label_rules['metering_label_rules']:
|
|
if metering_label_rule['metering_label_id'] not in metering_label_ids:
|
|
zombie_metering_label_rules.append(metering_label_rule)
|
|
print_list(zombie_metering_label_rules, ['id', 'name', 'tenant_id'])
|
|
|
|
if ((len(zombie_metering_label_rules) > 0 and
|
|
(args.AUTO_CLEAN or prompt_yes_no('Confirm to delete:')))):
|
|
for metering_label_rule in zombie_metering_label_rules:
|
|
shell.neutron.delete_metering_label_rule(metering_label_rule['id'])
|
|
|
|
# floating ip
|
|
_clean_up_resource(shell, args, tenant_ids, 'floatingip')
|
|
|
|
# port
|
|
print('>>>>>> ZOMBIE PORT LIST')
|
|
ports = shell.neutron.list_ports()
|
|
zombie_ports = []
|
|
for port in ports['ports']:
|
|
if port['tenant_id'] not in tenant_ids:
|
|
zombie_ports.append(port)
|
|
print_list(zombie_ports, ['id', 'name', 'tenant_id'])
|
|
|
|
if (len(zombie_ports) > 0 and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Confirm to delete:'))):
|
|
for port in zombie_ports:
|
|
try:
|
|
if port['device_owner'] == 'network:router_gateway':
|
|
shell.neutron.remove_gateway_router(port['device_id'])
|
|
elif port['device_owner'] == 'network:router_interface':
|
|
for subnet in port['fixed_ips']:
|
|
body = {'subnet_id': subnet['subnet_id']}
|
|
shell.neutron.remove_interface_router(
|
|
port['device_id'], body)
|
|
else:
|
|
shell.neutron.delete_port(port['id'])
|
|
except Exception as e:
|
|
print('Failed to delete port:{0}, see: {1}'.format(port['id'],
|
|
str(e)))
|
|
continue
|
|
|
|
# security group
|
|
_clean_up_resource(shell, args, tenant_ids, 'security_group')
|
|
|
|
# subnet
|
|
_clean_up_resource(shell, args, tenant_ids, 'subnet')
|
|
|
|
# router
|
|
_clean_up_resource(shell, args, tenant_ids, 'router')
|
|
|
|
# network
|
|
_clean_up_resource(shell, args, tenant_ids, 'network')
|
|
|
|
|
|
def _clean_up_resource(shell, args, tenant_ids, resource):
|
|
print('>>>>>> ZOMBIE %s LIST' % resource.upper())
|
|
resources = getattr(shell.neutron, 'list_{0}s'.format(resource))()
|
|
zombie_resources = [r for r in resources[resource + 's']
|
|
if r['tenant_id'] not in tenant_ids]
|
|
print_list(zombie_resources, ['id', 'name', 'tenant_id'])
|
|
|
|
if (len(zombie_resources) > 0 and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Confirm to delete:'))):
|
|
delete_function = getattr(shell.neutron, 'delete_{0}'.format(resource))
|
|
for res in zombie_resources:
|
|
delete_function(res['id'])
|
|
|
|
|
|
def cinder_audit(shell, args, tenant_ids):
|
|
print('>>>>>> ZOMBIE VOLUME SNAPSHOTS LIST')
|
|
# snapshots
|
|
snapshots = shell.cinder.volume_snapshots.list(search_opts={'all_tenants':
|
|
True})
|
|
tenant_attr = 'os-extended-snapshot-attributes:project_id'
|
|
zombie_snapshots = [s for s in snapshots
|
|
if getattr(s, tenant_attr) not in tenant_ids]
|
|
print_list(zombie_snapshots, ['id', 'display_name', 'status', tenant_attr])
|
|
|
|
if (len(zombie_snapshots) > 0 and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Confirm to delete:'))):
|
|
for snapshot in zombie_snapshots:
|
|
shell.cinder.volume_snapshots.delete(snapshot.id)
|
|
|
|
# volume
|
|
print('>>>>>> ZOMBIE VOLUME LIST')
|
|
volumes = shell.cinder.volumes.list(search_opts={'all_tenants': True})
|
|
tenant_attr = 'os-vol-tenant-attr:tenant_id'
|
|
zombie_volumes = [v for v in volumes
|
|
if getattr(v, tenant_attr) not in tenant_ids]
|
|
print_list(zombie_volumes, ['id', 'display_name',
|
|
'os-vol-tenant-attr:tenant_id'])
|
|
|
|
if (len(zombie_volumes) > 0 and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Confirm to delete:'))):
|
|
for volume in zombie_volumes:
|
|
shell.cinder.volumes.delete(volume.id)
|
|
|
|
|
|
def glance_audit(shell, args, tenant_ids):
|
|
# image
|
|
print('>>>>>> ZOMBIE IMAGE LIST')
|
|
images = shell.glance.images.list(filters={"is_public": None})
|
|
zombie_images = [i for i in images if (i.owner not in
|
|
tenant_ids) and (not i.is_public)]
|
|
print_list(zombie_images, ['id', 'name', 'owner'])
|
|
|
|
if (len(zombie_images) > 0 and (args.AUTO_CLEAN or
|
|
prompt_yes_no('Confirm to delete:'))):
|
|
for image in zombie_images:
|
|
shell.glance.images.delete(image.id)
|
|
|
|
|
|
def swift_audit(shell, args, tenant_ids):
|
|
# TODO(flwang): Seems Swift can't get all the resources with admin
|
|
# because all its resources will be associated with a tenant. That means
|
|
# without tenant id, you can't list them. So it's hard to detect the
|
|
# zombie resources.
|
|
pass
|
|
|
|
|
|
def create_tenant(keystone, args):
|
|
try:
|
|
tenant = keystone.tenants.create(tenant_name=args.TENANT_NAME,
|
|
description=args.TENANT_DESC,
|
|
enabled=True)
|
|
print_dict(tenant._info)
|
|
except Exception as e:
|
|
raise e
|
|
return tenant.id
|
|
|
|
|
|
def add_admin_to_tenant(keystone, tenant_id):
|
|
# Add admin user into the new tenant since it is required for monitoring
|
|
try:
|
|
admin_user_id, admin_role_id = get_admin_user_role(keystone)
|
|
keystone.roles.add_user_role(admin_user_id, admin_role_id, tenant_id)
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def get_admin_user_role(keystone):
|
|
# NOTE(flwang): Here is assuming that both the admin name and role name
|
|
# using 'admin' as the name.
|
|
try:
|
|
admin_user_id = [r.id for r in keystone.users.list()
|
|
if r.name == 'admin']
|
|
admin_role_id = [r.id for r in keystone.roles.list()
|
|
if r.name == 'admin']
|
|
return (admin_user_id[0], admin_role_id[0])
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def create_network(neutron, tenant_id, network_name):
|
|
if not network_name:
|
|
network_name = raw_input('Please enter the network name:')
|
|
try:
|
|
network_body = {
|
|
"network": {
|
|
"name": network_name,
|
|
'tenant_id': tenant_id,
|
|
"admin_state_up": True
|
|
}
|
|
}
|
|
network = neutron.create_network(body=network_body)
|
|
print_dict(network['network'])
|
|
return network['network']['id']
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def create_subnet(neutron, tenant_id, network_id, subnet_name, subnet_cidr,
|
|
dns_servers):
|
|
if not subnet_cidr:
|
|
subnet_cidr = raw_input('Please enter the IP range(CIDR format):')
|
|
try:
|
|
dns_servers = dns_servers if dns_servers else DNS_NAMESERVERS
|
|
subnet_body = {
|
|
"subnet": {
|
|
"network_id": network_id,
|
|
"ip_version": 4,
|
|
'tenant_id': tenant_id,
|
|
'dns_nameservers': dns_servers,
|
|
"cidr": subnet_cidr
|
|
}
|
|
}
|
|
subnet = neutron.create_subnet(body=subnet_body)
|
|
print_dict(subnet['subnet'])
|
|
return subnet
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def create_router(neutron, tenant_id, public_network, router_name):
|
|
if not router_name:
|
|
router_name = raw_input('Please enter the router name:')
|
|
if not public_network:
|
|
public_network = raw_input('Please enter the public network id:')
|
|
try:
|
|
router_body = {
|
|
"router": {
|
|
"name": router_name,
|
|
"external_gateway_info": {
|
|
"network_id": public_network
|
|
},
|
|
'tenant_id': tenant_id,
|
|
"admin_state_up": True
|
|
}
|
|
}
|
|
router = neutron.create_router(body=router_body)
|
|
print_dict(router['router'])
|
|
return router
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def create_interface(neutron, router, subnet_id):
|
|
try:
|
|
interface_body = {
|
|
"subnet_id": subnet_id
|
|
}
|
|
interface = neutron.add_interface_router(router['router']['id'],
|
|
body=interface_body)
|
|
print_dict(interface)
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def create_meter_label(neutron, args, tenant_id):
|
|
try:
|
|
meter_label_name = args.METER_LABEL_NAME
|
|
if not meter_label_name:
|
|
meter_label_name = 'meter-label-' + args.TENANT_NAME
|
|
|
|
meter_label_body = {
|
|
'metering_label': {
|
|
'name': meter_label_name,
|
|
'tenant_id': tenant_id,
|
|
'description': args.METER_LABEL_DESC,
|
|
}
|
|
}
|
|
meter_label = neutron.create_metering_label(body=meter_label_body)
|
|
print_dict(meter_label['metering_label'])
|
|
return meter_label['metering_label']['id']
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def create_meter_label_rule(neutron, meter_label_id, direction):
|
|
try:
|
|
meter_rule_body = {"metering_label_rule":
|
|
{"remote_ip_prefix": "0.0.0.0/0",
|
|
"direction": direction,
|
|
"metering_label_id": meter_label_id
|
|
}
|
|
}
|
|
meter_rule = neutron.create_metering_label_rule(body=meter_rule_body)
|
|
print_dict(meter_rule['metering_label_rule'])
|
|
except Exception as e:
|
|
raise e
|
|
|
|
|
|
def rollback(args, **parms):
|
|
# TODO(flwang): Remove the created resources if there is any failure.
|
|
pass
|
|
|
|
|
|
def print_list(objs, fields, formatters={}):
|
|
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
|
pt.align = 'l'
|
|
|
|
for o in objs:
|
|
row = []
|
|
for field in fields:
|
|
if field in formatters:
|
|
row.append(formatters[field](o))
|
|
else:
|
|
field_name = field.lower().replace(' ', '_')
|
|
if type(o) == dict and field in o:
|
|
data = o[field_name]
|
|
else:
|
|
data = getattr(o, field_name, None) or ''
|
|
row.append(data)
|
|
pt.add_row(row)
|
|
|
|
print(encodeutils.safe_encode(pt.get_string()))
|
|
|
|
|
|
def prompt_yes_no(question, default="no"):
|
|
"""Ask a yes/no question via raw_input() and return their answer.
|
|
|
|
"question" is a string that is presented to the user.
|
|
"default" is the presumed answer if the user just hits <Enter>.
|
|
It must be "yes" (the default), "no" or None (meaning
|
|
an answer is required of the user).
|
|
|
|
The "answer" return value is one of "yes" or "no".
|
|
"""
|
|
valid = {"yes": True, "y": True, "ye": True,
|
|
"no": False, "n": False}
|
|
if default is None:
|
|
prompt = " [y/n] "
|
|
elif default == "yes":
|
|
prompt = " [Y/n] "
|
|
elif default == "no":
|
|
prompt = " [y/N] "
|
|
else:
|
|
raise ValueError("invalid default answer: '%s'" % default)
|
|
|
|
while True:
|
|
sys.stdout.write(question + prompt)
|
|
choice = raw_input().lower()
|
|
if default is not None and choice == '':
|
|
return valid[default]
|
|
elif choice in valid:
|
|
return valid[choice]
|
|
else:
|
|
sys.stdout.write("Please respond with 'yes' or 'no' "
|
|
"(or 'y' or 'n').\n")
|
|
|
|
|
|
def print_dict(d, max_column_width=80):
|
|
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
|
pt.align = 'l'
|
|
pt.max_width = max_column_width
|
|
[pt.add_row(list(r)) for r in six.iteritems(d)]
|
|
print(encodeutils.safe_encode(pt.get_string(sortby='Property')))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
TenantShell().main(sys.argv[1:])
|
|
except KeyboardInterrupt:
|
|
print("Terminating...")
|
|
sys.exit(1)
|
|
except Exception as e:
|
|
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
traceback.print_exception(exc_type, exc_value, exc_traceback,
|
|
limit=2, file=sys.stdout)
|