/usr/share/pyshared/cloudservers/shell.py is in python-rackspace-cloudservers 1.0~a5-0ubuntu3.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 | """
Command-line interface to the Cloud Servers API.
"""
import argparse
import cloudservers
import getpass
import httplib2
import os
import prettytable
import sys
import textwrap
# Choices for flags.
DAY_CHOICES = [getattr(cloudservers, i).lower()
for i in dir(cloudservers)
if i.startswith('BACKUP_WEEKLY_')]
HOUR_CHOICES = [getattr(cloudservers, i).lower()
for i in dir(cloudservers)
if i.startswith('BACKUP_DAILY_')]
def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l)
# Decorator for args
def arg(*args, **kwargs):
def _decorator(func):
# Because of the sematics of decorator composition if we just append
# to the options list positional options will appear to be backwards.
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
return func
return _decorator
class CommandError(Exception):
pass
def env(e):
return os.environ.get(e, '')
class CloudserversShell(object):
# Hook for the test suite to inject a fake server.
_api_class = cloudservers.CloudServers
def __init__(self):
self.parser = argparse.ArgumentParser(
prog = 'cloudservers',
description = __doc__.strip(),
epilog = 'See "cloudservers help COMMAND" for help on a specific command.',
add_help = False,
formatter_class = CloudserversHelpFormatter,
)
# Global arguments
self.parser.add_argument('-h', '--help',
action = 'help',
help = argparse.SUPPRESS,
)
self.parser.add_argument('--debug',
default = False,
action = 'store_true',
help = argparse.SUPPRESS)
self.parser.add_argument('--username',
default = env('CLOUD_SERVERS_USERNAME'),
help = 'Defaults to env[CLOUD_SERVERS_USERNAME].')
self.parser.add_argument('--apikey',
default = env('CLOUD_SERVERS_API_KEY'),
help='Defaults to env[CLOUD_SERVERS_API_KEY].')
# Subcommands
subparsers = self.parser.add_subparsers(metavar='<subcommand>')
self.subcommands = {}
# Everything that's do_* is a subcommand.
for attr in (a for a in dir(self) if a.startswith('do_')):
# I prefer to be hypen-separated instead of underscores.
command = attr[3:].replace('_', '-')
callback = getattr(self, 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 = CloudserversHelpFormatter
)
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)
def main(self, argv):
# Parse args and call whatever callback was selected
args = self.parser.parse_args(argv)
# Short-circuit and deal with help right away.
if args.func == self.do_help:
self.do_help(args)
return 0
# Deal with global arguments
if args.debug:
httplib2.debuglevel = 1
user, apikey = args.username, args.apikey
if not user:
raise CommandError("You must provide a username, either via "
"--username or via env[CLOUD_SERVERS_USERNAME]")
if not apikey:
raise CommandError("You must provide an API key, either via "
"--apikey or via env[CLOUD_SERVERS_API_KEY]")
self.cs = self._api_class(user, apikey)
try:
self.cs.authenticate()
except cloudservers.Unauthorized:
raise CommandError("Invalid Cloud Servers credentials.")
args.func(args)
@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 args.command:
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
else:
raise CommandError("'%s' is not a valid subcommand." % args.command)
else:
self.parser.print_help()
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('--enable', dest='enabled', default=None, action='store_true', help='Enable backups.')
@arg('--disable', dest='enabled', action='store_false', help='Disable backups.')
@arg('--weekly', metavar='<day>', choices=DAY_CHOICES,
help='Schedule a weekly backup for <day> (one of: %s).' % pretty_choice_list(DAY_CHOICES))
@arg('--daily', metavar='<time-window>', choices=HOUR_CHOICES,
help='Schedule a daily backup during <time-window> (one of: %s).' % pretty_choice_list(HOUR_CHOICES))
def do_backup_schedule(self, args):
"""
Show or edit the backup schedule for a server.
With no flags, the backup schedule will be shown. If flags are given,
the backup schedule will be modified accordingly.
"""
server = self._find_server(args.server)
# If we have some flags, update the backup
backup = {}
if args.daily:
backup['daily'] = getattr(cloudservers, 'BACKUP_DAILY_%s' % args.daily.upper())
if args.weekly:
backup['weekly'] = getattr(cloudservers, 'BACKUP_WEEKLY_%s' % args.weekly.upper())
if args.enabled is not None:
backup['enabled'] = args.enabled
if backup:
server.backup_schedule.update(**backup)
else:
print_dict(server.backup_schedule._info)
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_backup_schedule_delete(self, args):
"""
Delete the backup schedule for a server.
"""
server = self._find_server(args.server)
server.backup_schedule.delete()
@arg('--flavor',
default = None,
metavar = '<flavor>',
help = "Flavor ID (see 'cloudservers flavors'). Defaults to 256MB RAM instance.")
@arg('--image',
default = None,
metavar = '<image>',
help = "Image ID (see 'cloudservers images'). Defaults to Ubuntu 9.10.")
@arg('--ipgroup',
default = None,
metavar = '<group>',
help = "IP group ID (see 'cloudservers ipgroups').")
@arg('--meta',
metavar = "<key=value>",
action = 'append',
help = "Record arbitrary key/value metadata.")
@arg('name', metavar='<name>', help='Name for the new server')
def do_boot(self, args):
"""Boot a new server."""
flavor = args.flavor or self.cs.flavors.find(ram=256)
image = args.image or self.cs.images.find(name="Ubuntu 9.10 (karmic)")
if args.meta:
metadata = dict(v.split('=') for v in args.meta)
else:
metadata = None
server = self.cs.servers.create(args.name, image, flavor, args.ipgroup, metadata)
print_dict(server._info)
def do_flavor_list(self, args):
"""Print a list of available 'flavors' (sizes of servers)."""
print_list(self.cs.flavors.list(), ['ID', 'Name', 'RAM', 'Disk'])
def do_image_list(self, args):
"""Print a list of available images to boot from."""
print_list(self.cs.images.list(), ['ID', 'Name', 'Status'])
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('name', metavar='<name>', help='Name for the new image.')
def do_image_create(self, args):
"""Create a new image by taking a snapshot of a running server."""
server = self._find_server(args.server)
image = self.cs.images.create(args.name, server)
print_dict(image._info)
@arg('image', metavar='<image>', help='Name or ID of image.')
def do_image_delete(self, args):
"""
Delete an image.
It should go without saying, but you cn only delete images you
created.
"""
image = self._find_image(args.image)
image.delete()
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('group', metavar='<group>', help='Name or ID of group.')
@arg('address', metavar='<address>', help='IP address to share.')
def do_ip_share(self, args):
"""Share an IP address from the given IP group onto a server."""
server = self._find_server(args.server)
group = self._find_ipgroup(args.group)
server.share_ip(group, args.address)
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('address', metavar='<address>', help='Shared IP address to remove from the server.')
def do_ip_unshare(self, args):
"""Stop sharing an given address with a server."""
server = self._find_server(args.server)
server.unshare_ip(args.address)
def do_ipgroup_list(self, args):
"""Show IP groups."""
print_list(self.cs.ipgroups.list(), ['ID', 'Name', 'Servers'])
@arg('group', metavar='<group>', help='Name or ID of group.')
def do_ipgroup_show(self, args):
"""Show details about a particular IP group."""
group = self._find_ipgroup(args.group)
print_dict(group._info)
@arg('name', metavar='<name>', help='What to name this new group.')
@arg('server', metavar='<server>', nargs='?',
help='Server (name or ID) to make a member of this new group.')
def do_ipgroup_create(self, args):
"""Create a new IP group."""
if args.server:
server = self._find_server(args.server)
else:
server = None
group = self.cs.ipgroups.create(args.name, server)
print_dict(group._info)
@arg('group', metavar='<group>', help='Name or ID of group.')
def do_ipgroup_delete(self, args):
"""Delete an IP group."""
self._find_ipgroup(args.group).delete()
def do_list(self, args):
"""List active servers."""
print_list(self.cs.servers.list(), ['ID', 'Name', 'Status', 'Public IP', 'Private IP'])
@arg('--hard',
dest = 'reboot_type',
action = 'store_const',
const = cloudservers.REBOOT_HARD,
default = cloudservers.REBOOT_SOFT,
help = 'Perform a hard reboot (instead of a soft one).')
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_reboot(self, args):
"""Reboot a server."""
self._find_server(args.server).reboot(args.reboot_type)
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('image', metavar='<image>', help="Name or ID of new image.")
def do_rebuild(self, args):
"""Shutdown, re-image, and re-boot a server."""
server = self._find_server(args.server)
image = self._find_image(args.image)
server.rebuild(image)
@arg('server', metavar='<server>', help='Name (old name) or ID of server.')
@arg('name', metavar='<name>', help='New name for the server.')
def do_rename(self, args):
"""Rename a server."""
self._find_server(args.server).update(name=args.name)
@arg('server', metavar='<server>', help='Name or ID of server.')
@arg('flavor', metavar='<flavor>', help = "Name or ID of new flavor.")
def do_resize(self, args):
"""Resize a server."""
server = self._find_server(args.server)
flavor = self._find_flavor(args.flavor)
server.resize(flavor)
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_resize_confirm(self, args):
"""Confirm a previous resize."""
self._find_server(args.server).confirm_resize()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_resize_revert(self, args):
"""Revert a previous resize (and return to the previous VM)."""
self._find_server(args.server).revert_resize()
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_root_password(self, args):
"""
Change the root password for a server.
"""
server = self._find_server(args.server)
p1 = getpass.getpass('New password: ')
p2 = getpass.getpass('Again: ')
if p1 != p2:
raise CommandError("Passwords do not match.")
server.update(password=p1)
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_show(self, args):
"""Show details about the given server."""
s = self.cs.servers.get(self._find_server(args.server))
info = s._info.copy()
addresses = info.pop('addresses')
for addrtype in addresses:
info['%s ip' % addrtype] = ', '.join(addresses[addrtype])
info['flavor'] = self._find_flavor(info.pop('flavorId')).name
info['image'] = self._find_image(info.pop('imageId')).name
print_dict(info)
@arg('server', metavar='<server>', help='Name or ID of server.')
def do_delete(self, args):
"""Immediately shut down and delete a server."""
self._find_server(args.server).delete()
def _find_server(self, server):
"""Get a server by name or ID."""
return self._find_resource(self.cs.servers, server)
def _find_ipgroup(self, group):
"""Get an IP group by name or ID."""
return self._find_resource(self.cs.ipgroups, group)
def _find_image(self, image):
"""Get an image by name or ID."""
return self._find_resource(self.cs.images, image)
def _find_flavor(self, flavor):
"""Get a flavor by name, ID, or RAM size."""
try:
return self._find_resource(self.cs.flavors, flavor)
except cloudservers.NotFound:
return self.cs.flavors.find(ram=flavor)
def _find_resource(self, manager, name_or_id):
"""Helper for the _find_* methods."""
try:
if isinstance(name_or_id, int) or name_or_id.isdigit():
return manager.get(int(name_or_id))
else:
return manager.find(name=name_or_id)
except cloudservers.NotFound:
raise CommandError("No %s with a name or ID of '%s' exists."
% (manager.resource_class.__name__.lower(), name_or_id))
# I'm picky about my shell help.
class CloudserversHelpFormatter(argparse.HelpFormatter):
def start_section(self, heading):
# Title-case the headings
heading = '%s%s' % (heading[0].upper(), heading[1:])
super(CloudserversHelpFormatter, self).start_section(heading)
# Helpers
def print_list(objs, fields):
pt = prettytable.PrettyTable([f for f in fields], caching=False)
pt.aligns = ['l' for f in fields]
for o in objs:
pt.add_row([getattr(o, f.lower().replace(' ', '_'), '') for f in fields])
pt.printt(sortby=fields[0])
def print_dict(d):
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
pt.aligns = ['l', 'l']
[pt.add_row(list(r)) for r in d.iteritems()]
pt.printt(sortby='Property')
def main():
try:
CloudserversShell().main(sys.argv[1:])
except CommandError, e:
print >> sys.stderr, e
sys.exit(1)
|