def command(api=None, group=None, name=None, syntax=None):
"""Class decorator that registers a class as a CLI command."""
-
+
def decorator(cls):
grp, sep, cmd = cls.__name__.partition('_')
if not sep:
grp, cmd = None, cls.__name__
-
+
cls.api = api
cls.group = group or grp
cls.name = name or cmd
-
+
short_description, sep, long_description = cls.__doc__.partition('\n')
cls.description = short_description
cls.long_description = long_description or short_description
-
+
cls.syntax = syntax
if cls.syntax is None:
# Generate a syntax string based on main's arguments
cls.syntax = ' '.join(x for x in [required, optional] if x)
if spec.varargs:
cls.syntax += ' <%s ...>' % spec.varargs
-
+
if cls.group not in _commands:
_commands[cls.group] = OrderedDict()
_commands[cls.group][cls.name] = cls
@command(api='config')
class config_list(object):
"""List configuration options"""
-
+
def update_parser(self, parser):
parser.add_argument('-a', dest='all', action='store_true',
default=False, help='include default values')
@command(api='config')
class config_get(object):
"""Show a configuration option"""
-
+
def main(self, option):
section, sep, key = option.rpartition('.')
section = section or 'global'
@command(api='config')
class config_set(object):
"""Set a configuration option"""
-
+
def main(self, option, value):
section, sep, key = option.rpartition('.')
section = section or 'global'
@command(api='config')
class config_delete(object):
"""Delete a configuration option (and use the default value)"""
-
+
def main(self, option):
section, sep, key = option.rpartition('.')
section = section or 'global'
@command(api='compute')
class server_list(object):
"""List servers"""
-
+
def update_parser(self, parser):
parser.add_argument('-l', dest='detail', action='store_true',
default=False, help='show detailed output')
@command(api='compute')
class server_info(object):
"""Get server details"""
-
+
def main(self, server_id):
server = self.client.get_server_details(int(server_id))
print_dict(server)
@command(api='compute')
class server_create(object):
"""Create a server"""
-
+
def update_parser(self, parser):
parser.add_argument('--personality', dest='personalities',
action='append', default=[],
for personality in self.args.personalities:
p = personality.split(',')
p.extend([None] * (5 - len(p))) # Fill missing fields with None
-
+
path = p[0]
-
+
if not path:
print("Invalid personality argument '%s'" % p)
return 1
if not exists(path):
print("File %s does not exist" % path)
return 1
-
+
with open(path) as f:
contents = b64encode(f.read())
@command(api='compute')
class server_rename(object):
"""Update a server's name"""
-
+
def main(self, server_id, new_name):
self.client.update_server_name(int(server_id), new_name)
@command(api='compute')
class server_delete(object):
"""Delete a server"""
-
+
def main(self, server_id):
self.client.delete_server(int(server_id))
@command(api='compute')
class server_reboot(object):
"""Reboot a server"""
-
+
def update_parser(self, parser):
parser.add_argument('-f', dest='hard', action='store_true',
default=False, help='perform a hard reboot')
@command(api='cyclades')
class server_start(object):
"""Start a server"""
-
+
def main(self, server_id):
self.client.start_server(int(server_id))
@command(api='cyclades')
class server_shutdown(object):
"""Shutdown a server"""
-
+
def main(self, server_id):
self.client.shutdown_server(int(server_id))
@command(api='cyclades')
class server_console(object):
"""Get a VNC console"""
-
+
def main(self, server_id):
reply = self.client.get_server_console(int(server_id))
print_dict(reply)
@command(api='cyclades')
class server_firewall(object):
"""Set the server's firewall profile"""
-
+
def main(self, server_id, profile):
self.client.set_firewall_profile(int(server_id), profile)
@command(api='cyclades')
class server_addr(object):
"""List a server's addresses"""
-
+
def main(self, server_id, network=None):
reply = self.client.list_server_addresses(int(server_id), network)
margin = max(len(x['name']) for x in reply)
@command(api='compute')
class server_meta(object):
"""Get a server's metadata"""
-
+
def main(self, server_id, key=None):
reply = self.client.get_server_metadata(int(server_id), key)
print_dict(reply)
@command(api='compute')
class server_addmeta(object):
"""Add server metadata"""
-
+
def main(self, server_id, key, val):
reply = self.client.create_server_metadata(int(server_id), key, val)
print_dict(reply)
@command(api='compute')
class server_setmeta(object):
"""Update server's metadata"""
-
+
def main(self, server_id, key, val):
metadata = {key: val}
reply = self.client.update_server_metadata(int(server_id), **metadata)
@command(api='compute')
class server_delmeta(object):
"""Delete server metadata"""
-
+
def main(self, server_id, key):
self.client.delete_server_metadata(int(server_id), key)
@command(api='cyclades')
class server_stats(object):
"""Get server statistics"""
-
+
def main(self, server_id):
reply = self.client.get_server_stats(int(server_id))
print_dict(reply, exclude=('serverRef',))
@command(api='compute')
class flavor_list(object):
"""List flavors"""
-
+
def update_parser(self, parser):
parser.add_argument('-l', dest='detail', action='store_true',
default=False, help='show detailed output')
@command(api='compute')
class flavor_info(object):
"""Get flavor details"""
-
+
def main(self, flavor_id):
flavor = self.client.get_flavor_details(int(flavor_id))
print_dict(flavor)
@command(api='compute')
class image_list(object):
"""List images"""
-
+
def update_parser(self, parser):
parser.add_argument('-l', dest='detail', action='store_true',
default=False, help='show detailed output')
@command(api='compute')
class image_info(object):
"""Get image details"""
-
+
def main(self, image_id):
image = self.client.get_image_details(image_id)
print_dict(image)
@command(api='compute')
class image_delete(object):
"""Delete image"""
-
+
def main(self, image_id):
self.client.delete_image(image_id)
@command(api='compute')
class image_properties(object):
"""Get image properties"""
-
+
def main(self, image_id, key=None):
reply = self.client.get_image_metadata(image_id, key)
print_dict(reply)
@command(api='compute')
class image_addproperty(object):
"""Add an image property"""
-
+
def main(self, image_id, key, val):
reply = self.client.create_image_metadata(image_id, key, val)
print_dict(reply)
@command(api='compute')
class image_setproperty(object):
"""Update an image property"""
-
+
def main(self, image_id, key, val):
metadata = {key: val}
reply = self.client.update_image_metadata(image_id, **metadata)
@command(api='compute')
class image_delproperty(object):
"""Delete an image property"""
-
+
def main(self, image_id, key):
self.client.delete_image_metadata(image_id, key)
@command(api='cyclades')
class network_list(object):
"""List networks"""
-
+
def update_parser(self, parser):
parser.add_argument('-l', dest='detail', action='store_true',
default=False, help='show detailed output')
@command(api='cyclades')
class network_create(object):
"""Create a network"""
-
+
def main(self, name):
reply = self.client.create_network(name)
print_dict(reply)
@command(api='cyclades')
class network_info(object):
"""Get network details"""
-
+
def main(self, network_id):
network = self.client.get_network_details(network_id)
print_dict(network)
@command(api='cyclades')
class network_rename(object):
"""Update network name"""
-
+
def main(self, network_id, new_name):
self.client.update_network_name(network_id, new_name)
@command(api='cyclades')
class network_delete(object):
"""Delete a network"""
-
+
def main(self, network_id):
self.client.delete_network(network_id)
@command(api='cyclades')
class network_connect(object):
"""Connect a server to a network"""
-
+
def main(self, server_id, network_id):
self.client.connect_server(server_id, network_id)
@command(api='cyclades')
class network_disconnect(object):
"""Disconnect a server from a network"""
-
+
def main(self, server_id, network_id):
self.client.disconnect_server(server_id, network_id)
@command(api='image')
class image_public(object):
"""List public images"""
-
+
def update_parser(self, parser):
parser.add_argument('-l', dest='detail', action='store_true',
default=False, help='show detailed output')
val = getattr(self.args, filter, None)
if val is not None:
filters[filter] = val
-
+
order = self.args.order or ''
images = self.client.list_public(self.args.detail, filters=filters,
order=order)
@command(api='image')
class image_meta(object):
"""Get image metadata"""
-
+
def main(self, image_id):
image = self.client.get_meta(image_id)
print_dict(image)
@command(api='image')
class image_register(object):
"""Register an image"""
-
+
def update_parser(self, parser):
parser.add_argument('--checksum', dest='checksum', metavar='CHECKSUM',
help='set image checksum')
account = self.config.get('storage', 'account')
container = self.config.get('storage', 'container')
location = 'pithos://%s/%s/%s' % (account, container, location)
-
+
params = {}
for key in ('checksum', 'container_format', 'disk_format', 'id',
'owner', 'size'):
val = getattr(self.args, key)
if val is not None:
params[key] = val
-
+
if self.args.is_public:
params['is_public'] = 'true'
-
+
properties = {}
for property in self.args.properties or []:
key, sep, val = property.partition('=')
print("Invalid property '%s'" % property)
return 1
properties[key.strip()] = val.strip()
-
+
self.client.register(name, location, params, properties)
@command(api='image')
class image_members(object):
"""Get image members"""
-
+
def main(self, image_id):
members = self.client.list_members(image_id)
for member in members:
@command(api='image')
class image_shared(object):
"""List shared images"""
-
+
def main(self, member):
images = self.client.list_shared(member)
for image in images:
@command(api='image')
class image_addmember(object):
"""Add a member to an image"""
-
+
def main(self, image_id, member):
self.client.add_member(image_id, member)
@command(api='image')
class image_delmember(object):
"""Remove a member from an image"""
-
+
def main(self, image_id, member):
self.client.remove_member(image_id, member)
@command(api='image')
class image_setmembers(object):
"""Set the members of an image"""
-
+
def main(self, image_id, *member):
self.client.set_members(image_id, member)
class _store_account_command(object):
"""Base class for account level storage commands"""
-
+
def update_parser(self, parser):
parser.add_argument('--account', dest='account', metavar='NAME',
help="Specify an account to use")
def progress(self, message):
"""Return a generator function to be used for progress tracking"""
-
+
MESSAGE_LENGTH = 25
-
+
def progress_gen(n):
msg = message.ljust(MESSAGE_LENGTH)
for i in ProgressBar(msg).iter(range(n)):
yield
yield
-
+
return progress_gen
-
+
def main(self):
if self.args.account is not None:
self.client.account = self.args.account
class _store_container_command(_store_account_command):
"""Base class for container level storage commands"""
-
+
def update_parser(self, parser):
super(_store_container_command, self).update_parser(parser)
parser.add_argument('--container', dest='container', metavar='NAME',
@command(api='storage')
class store_create(_store_account_command):
"""Create a container"""
-
+
def main(self, container):
super(store_create, self).main()
self.client.create_container(container)
@command(api='storage')
class store_container(_store_account_command):
"""Get container info"""
-
+
def main(self, container):
super(store_container, self).main()
reply = self.client.get_container_meta(container)
@command(api='storage')
class store_list(_store_container_command):
"""List objects"""
-
+
def format_size(self, size):
units = ('B', 'K', 'M', 'G', 'T')
size = float(size)
size /= 1024
s = ('%.1f' % size).rstrip('.0')
return s + unit
-
-
+
def main(self, path=''):
super(store_list, self).main()
for object in self.client.list_objects():
size = self.format_size(object['bytes'])
print('%6s %s' % (size, object['name']))
-
+
@command(api='storage')
class store_upload(_store_container_command):
"""Upload a file"""
-
+
def main(self, path, remote_path=None):
super(store_upload, self).main()
-
+
if remote_path is None:
remote_path = basename(path)
with open(path) as f:
@command(api='storage')
class store_download(_store_container_command):
"""Download a file"""
-
+
def main(self, remote_path, local_path='-'):
super(store_download, self).main()
-
+
f, size = self.client.get_object(remote_path)
out = open(local_path, 'w') if local_path != '-' else stdout
-
- blocksize = 4 * 1024**2
+
+ blocksize = 4 * 1024 ** 2
nblocks = 1 + (size - 1) // blocksize
-
+
cb = self.progress('Downloading blocks') if local_path != '-' else None
if cb:
gen = cb(nblocks)
gen.next()
-
+
data = f.read(blocksize)
while data:
out.write(data)
@command(api='storage')
class store_delete(_store_container_command):
"""Delete a file"""
-
+
def main(self, path):
super(store_delete, self).main()
self.client.delete_object(path)
@command(api='storage')
class store_purge(_store_account_command):
"""Purge a container"""
-
+
def main(self, container):
super(store_purge, self).main()
self.client.purge_container(container)
@command(api='astakos')
class astakos_authenticate(object):
"""Authenticate a user"""
-
+
def main(self):
reply = self.client.authenticate()
print_dict(reply)
description = GROUPS.get(group, '')
if description:
print('\n' + description)
-
+
print('\nCommands:')
for name, cls in _commands[group].items():
print(' ', name.ljust(14), cls.description)
print("Invalid option '%s'" % option)
exit(1)
config.override(section.strip(), key.strip(), val.strip())
-
+
apis = set(['config'])
for api in ('compute', 'image', 'storage', 'astakos'):
if config.getboolean(api, 'enable'):
apis.add('cyclades')
if config.getboolean('storage', 'pithos_extensions'):
apis.add('pithos')
-
+
# Remove commands that belong to APIs that are not included
for group, group_commands in _commands.items():
for name, cls in group_commands.items():
parser.print_help()
print_commands(group)
exit(1)
-
+
cmd = _commands[group][command]()
parser.prog = '%s %s %s' % (exe, group, command)
parser.epilog = ''
if hasattr(cmd, 'update_parser'):
cmd.update_parser(parser)
-
+
args, argv = parser.parse_known_args()
-
+
if args.help:
parser.print_help()
exit(0)
-
+
if args.silent:
add_handler('', logging.CRITICAL)
elif args.debug:
add_handler('clients.recv', logging.INFO)
else:
add_handler('', logging.WARNING)
-
+
api = cmd.api
if api in ('compute', 'cyclades'):
url = config.get('compute', 'url')
url = config.get('astakos', 'url')
token = config.get('astakos', 'token') or config.get('global', 'token')
cmd.client = clients.astakos(url, token)
-
+
cmd.args = args
cmd.config = config
-
+
try:
ret = cmd.main(*argv[2:])
exit(ret)
message = magenta(err.message)
else:
message = red(err.message)
-
+
print(message, file=stderr)
if err.details and (args.verbose or args.debug):
print(err.details, file=stderr)
class ComputeClient(Client):
"""OpenStack Compute API 1.1 client"""
-
+
def raise_for_status(self, r):
d = r.json
if not d:
message = '%s: %s' % (key, val.get('message', ''))
details = val.get('details', '')
raise ClientError(message, r.status_code, details)
-
+
def list_servers(self, detail=False):
"""List servers, returned detailed output if detailed is True"""
-
+
path = '/servers/detail' if detail else '/servers'
r = self.get(path, success=200)
return r.json['servers']['values']
-
+
def get_server_details(self, server_id):
"""Return detailed output on a server specified by its id"""
-
+
path = '/servers/%s' % (server_id,)
r = self.get(path, success=200)
return r.json['server']
-
+
def create_server(self, name, flavor_id, image_id, personality=None):
"""Submit request to create a new server
'imageRef': image_id}}
if personality:
req['server']['personality'] = personality
-
+
r = self.post('/servers', json=req, success=202)
return r.json['server']
-
+
def update_server_name(self, server_id, new_name):
"""Update the name of the server as reported by the API.
path = '/servers/%s' % (server_id,)
req = {'server': {'name': new_name}}
self.put(path, json=req, success=204)
-
+
def delete_server(self, server_id):
"""Submit a deletion request for a server specified by id"""
-
+
path = '/servers/%s' % (server_id,)
self.delete(path, success=204)
-
+
def reboot_server(self, server_id, hard=False):
"""Submit a reboot request for a server specified by id"""
-
+
path = '/servers/%s/action' % (server_id,)
type = 'HARD' if hard else 'SOFT'
req = {'reboot': {'type': type}}
self.post(path, json=req, success=202)
-
+
def get_server_metadata(self, server_id, key=None):
path = '/servers/%s/meta' % (server_id,)
if key:
path += '/%s' % key
r = self.get(path, success=200)
return r.json['meta'] if key else r.json['metadata']['values']
-
+
def create_server_metadata(self, server_id, key, val):
path = '/servers/%d/meta/%s' % (server_id, key)
req = {'meta': {key: val}}
r = self.put(path, json=req, success=201)
return r.json['meta']
-
+
def update_server_metadata(self, server_id, **metadata):
path = '/servers/%d/meta' % (server_id,)
req = {'metadata': metadata}
r = self.post(path, json=req, success=201)
return r.json['metadata']
-
+
def delete_server_metadata(self, server_id, key):
path = '/servers/%d/meta/%s' % (server_id, key)
self.delete(path, success=204)
-
-
+
def list_flavors(self, detail=False):
path = '/flavors/detail' if detail else '/flavors'
r = self.get(path, success=200)
path = '/flavors/%d' % flavor_id
r = self.get(path, success=200)
return r.json['flavor']
-
-
+
def list_images(self, detail=False):
path = '/images/detail' if detail else '/images'
r = self.get(path, success=200)
return r.json['images']['values']
-
+
def get_image_details(self, image_id):
path = '/images/%s' % (image_id,)
r = self.get(path, success=200)
return r.json['image']
-
+
def delete_image(self, image_id):
path = '/images/%s' % (image_id,)
self.delete(path, success=204)
path += '/%s' % key
r = self.get(path, success=200)
return r.json['meta'] if key else r.json['metadata']['values']
-
+
def create_image_metadata(self, image_id, key, val):
path = '/images/%s/meta/%s' % (image_id, key)
req = {'meta': {key: val}}