OLD | NEW |
(Empty) | |
| 1 # Copyright (c) 2010 Mitch Garnaat http://garnaat.org/ |
| 2 # Copyright (c) 2010, Eucalyptus Systems, Inc. |
| 3 # |
| 4 # Permission is hereby granted, free of charge, to any person obtaining a |
| 5 # copy of this software and associated documentation files (the |
| 6 # "Software"), to deal in the Software without restriction, including |
| 7 # without limitation the rights to use, copy, modify, merge, publish, dis- |
| 8 # tribute, sublicense, and/or sell copies of the Software, and to permit |
| 9 # persons to whom the Software is furnished to do so, subject to the fol- |
| 10 # lowing conditions: |
| 11 # |
| 12 # The above copyright notice and this permission notice shall be included |
| 13 # in all copies or substantial portions of the Software. |
| 14 # |
| 15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS |
| 16 # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- |
| 17 # ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT |
| 18 # SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, |
| 19 # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
| 20 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS |
| 21 # IN THE SOFTWARE. |
| 22 |
| 23 import sys |
| 24 import os |
| 25 import boto |
| 26 import optparse |
| 27 import copy |
| 28 import boto.exception |
| 29 import boto.roboto.awsqueryservice |
| 30 |
| 31 import bdb |
| 32 import traceback |
| 33 try: |
| 34 import epdb as debugger |
| 35 except ImportError: |
| 36 import pdb as debugger |
| 37 |
| 38 def boto_except_hook(debugger_flag, debug_flag): |
| 39 def excepthook(typ, value, tb): |
| 40 if typ is bdb.BdbQuit: |
| 41 sys.exit(1) |
| 42 sys.excepthook = sys.__excepthook__ |
| 43 |
| 44 if debugger_flag and sys.stdout.isatty() and sys.stdin.isatty(): |
| 45 if debugger.__name__ == 'epdb': |
| 46 debugger.post_mortem(tb, typ, value) |
| 47 else: |
| 48 debugger.post_mortem(tb) |
| 49 elif debug_flag: |
| 50 print traceback.print_tb(tb) |
| 51 sys.exit(1) |
| 52 else: |
| 53 print value |
| 54 sys.exit(1) |
| 55 |
| 56 return excepthook |
| 57 |
| 58 class Line(object): |
| 59 |
| 60 def __init__(self, fmt, data, label): |
| 61 self.fmt = fmt |
| 62 self.data = data |
| 63 self.label = label |
| 64 self.line = '%s\t' % label |
| 65 self.printed = False |
| 66 |
| 67 def append(self, datum): |
| 68 self.line += '%s\t' % datum |
| 69 |
| 70 def print_it(self): |
| 71 if not self.printed: |
| 72 print self.line |
| 73 self.printed = True |
| 74 |
| 75 class RequiredParamError(boto.exception.BotoClientError): |
| 76 |
| 77 def __init__(self, required): |
| 78 self.required = required |
| 79 s = 'Required parameters are missing: %s' % self.required |
| 80 boto.exception.BotoClientError.__init__(self, s) |
| 81 |
| 82 class EncoderError(boto.exception.BotoClientError): |
| 83 |
| 84 def __init__(self, error_msg): |
| 85 s = 'Error encoding value (%s)' % error_msg |
| 86 boto.exception.BotoClientError.__init__(self, s) |
| 87 |
| 88 class FilterError(boto.exception.BotoClientError): |
| 89 |
| 90 def __init__(self, filters): |
| 91 self.filters = filters |
| 92 s = 'Unknown filters: %s' % self.filters |
| 93 boto.exception.BotoClientError.__init__(self, s) |
| 94 |
| 95 class Encoder: |
| 96 |
| 97 @classmethod |
| 98 def encode(cls, p, rp, v, label=None): |
| 99 if p.name.startswith('_'): |
| 100 return |
| 101 try: |
| 102 mthd = getattr(cls, 'encode_'+p.ptype) |
| 103 mthd(p, rp, v, label) |
| 104 except AttributeError: |
| 105 raise EncoderError('Unknown type: %s' % p.ptype) |
| 106 |
| 107 @classmethod |
| 108 def encode_string(cls, p, rp, v, l): |
| 109 if l: |
| 110 label = l |
| 111 else: |
| 112 label = p.name |
| 113 rp[label] = v |
| 114 |
| 115 encode_file = encode_string |
| 116 encode_enum = encode_string |
| 117 |
| 118 @classmethod |
| 119 def encode_integer(cls, p, rp, v, l): |
| 120 if l: |
| 121 label = l |
| 122 else: |
| 123 label = p.name |
| 124 rp[label] = '%d' % v |
| 125 |
| 126 @classmethod |
| 127 def encode_boolean(cls, p, rp, v, l): |
| 128 if l: |
| 129 label = l |
| 130 else: |
| 131 label = p.name |
| 132 if v: |
| 133 v = 'true' |
| 134 else: |
| 135 v = 'false' |
| 136 rp[label] = v |
| 137 |
| 138 @classmethod |
| 139 def encode_datetime(cls, p, rp, v, l): |
| 140 if l: |
| 141 label = l |
| 142 else: |
| 143 label = p.name |
| 144 rp[label] = v |
| 145 |
| 146 @classmethod |
| 147 def encode_array(cls, p, rp, v, l): |
| 148 v = boto.utils.mklist(v) |
| 149 if l: |
| 150 label = l |
| 151 else: |
| 152 label = p.name |
| 153 label = label + '.%d' |
| 154 for i, value in enumerate(v): |
| 155 rp[label%(i+1)] = value |
| 156 |
| 157 class AWSQueryRequest(object): |
| 158 |
| 159 ServiceClass = None |
| 160 |
| 161 Description = '' |
| 162 Params = [] |
| 163 Args = [] |
| 164 Filters = [] |
| 165 Response = {} |
| 166 |
| 167 CLITypeMap = {'string' : 'string', |
| 168 'integer' : 'int', |
| 169 'int' : 'int', |
| 170 'enum' : 'choice', |
| 171 'datetime' : 'string', |
| 172 'dateTime' : 'string', |
| 173 'file' : 'string', |
| 174 'boolean' : None} |
| 175 |
| 176 @classmethod |
| 177 def name(cls): |
| 178 return cls.__name__ |
| 179 |
| 180 def __init__(self, **args): |
| 181 self.args = args |
| 182 self.parser = None |
| 183 self.cli_options = None |
| 184 self.cli_args = None |
| 185 self.cli_output_format = None |
| 186 self.connection = None |
| 187 self.list_markers = [] |
| 188 self.item_markers = [] |
| 189 self.request_params = {} |
| 190 self.connection_args = None |
| 191 |
| 192 def __repr__(self): |
| 193 return self.name() |
| 194 |
| 195 def get_connection(self, **args): |
| 196 if self.connection is None: |
| 197 self.connection = self.ServiceClass(**args) |
| 198 return self.connection |
| 199 |
| 200 @property |
| 201 def status(self): |
| 202 retval = None |
| 203 if self.http_response is not None: |
| 204 retval = self.http_response.status |
| 205 return retval |
| 206 |
| 207 @property |
| 208 def reason(self): |
| 209 retval = None |
| 210 if self.http_response is not None: |
| 211 retval = self.http_response.reason |
| 212 return retval |
| 213 |
| 214 @property |
| 215 def request_id(self): |
| 216 retval = None |
| 217 if self.aws_response is not None: |
| 218 retval = getattr(self.aws_response, 'requestId') |
| 219 return retval |
| 220 |
| 221 def process_filters(self): |
| 222 filters = self.args.get('filters', []) |
| 223 filter_names = [f['name'] for f in self.Filters] |
| 224 unknown_filters = [f for f in filters if f not in filter_names] |
| 225 if unknown_filters: |
| 226 raise FilterError('Unknown filters: %s' % unknown_filters) |
| 227 for i, filter in enumerate(self.Filters): |
| 228 name = filter['name'] |
| 229 if name in filters: |
| 230 self.request_params['Filter.%d.Name' % (i+1)] = name |
| 231 for j, value in enumerate(boto.utils.mklist(filters[name])): |
| 232 Encoder.encode(filter, self.request_params, value, |
| 233 'Filter.%d.Value.%d' % (i+1, j+1)) |
| 234 |
| 235 def process_args(self, **args): |
| 236 """ |
| 237 Responsible for walking through Params defined for the request and: |
| 238 |
| 239 * Matching them with keyword parameters passed to the request |
| 240 constructor or via the command line. |
| 241 * Checking to see if all required parameters have been specified |
| 242 and raising an exception, if not. |
| 243 * Encoding each value into the set of request parameters that will |
| 244 be sent in the request to the AWS service. |
| 245 """ |
| 246 self.args.update(args) |
| 247 self.connection_args = copy.copy(self.args) |
| 248 if 'debug' in self.args and self.args['debug'] >= 2: |
| 249 boto.set_stream_logger(self.name()) |
| 250 required = [p.name for p in self.Params+self.Args if not p.optional] |
| 251 for param in self.Params+self.Args: |
| 252 if param.long_name: |
| 253 python_name = param.long_name.replace('-', '_') |
| 254 else: |
| 255 python_name = boto.utils.pythonize_name(param.name, '_') |
| 256 value = None |
| 257 if python_name in self.args: |
| 258 value = self.args[python_name] |
| 259 if value is None: |
| 260 value = param.default |
| 261 if value is not None: |
| 262 if param.name in required: |
| 263 required.remove(param.name) |
| 264 if param.request_param: |
| 265 if param.encoder: |
| 266 param.encoder(param, self.request_params, value) |
| 267 else: |
| 268 Encoder.encode(param, self.request_params, value) |
| 269 if python_name in self.args: |
| 270 del self.connection_args[python_name] |
| 271 if required: |
| 272 l = [] |
| 273 for p in self.Params+self.Args: |
| 274 if p.name in required: |
| 275 if p.short_name and p.long_name: |
| 276 l.append('(%s, %s)' % (p.optparse_short_name, |
| 277 p.optparse_long_name)) |
| 278 elif p.short_name: |
| 279 l.append('(%s)' % p.optparse_short_name) |
| 280 else: |
| 281 l.append('(%s)' % p.optparse_long_name) |
| 282 raise RequiredParamError(','.join(l)) |
| 283 boto.log.debug('request_params: %s' % self.request_params) |
| 284 self.process_markers(self.Response) |
| 285 |
| 286 def process_markers(self, fmt, prev_name=None): |
| 287 if fmt and fmt['type'] == 'object': |
| 288 for prop in fmt['properties']: |
| 289 self.process_markers(prop, fmt['name']) |
| 290 elif fmt and fmt['type'] == 'array': |
| 291 self.list_markers.append(prev_name) |
| 292 self.item_markers.append(fmt['name']) |
| 293 |
| 294 def send(self, verb='GET', **args): |
| 295 self.process_args(**args) |
| 296 self.process_filters() |
| 297 conn = self.get_connection(**self.connection_args) |
| 298 self.http_response = conn.make_request(self.name(), |
| 299 self.request_params, |
| 300 verb=verb) |
| 301 self.body = self.http_response.read() |
| 302 boto.log.debug(self.body) |
| 303 if self.http_response.status == 200: |
| 304 self.aws_response = boto.jsonresponse.Element(list_marker=self.list_
markers, |
| 305 item_marker=self.item_
markers) |
| 306 h = boto.jsonresponse.XmlHandler(self.aws_response, self) |
| 307 h.parse(self.body) |
| 308 return self.aws_response |
| 309 else: |
| 310 boto.log.error('%s %s' % (self.http_response.status, |
| 311 self.http_response.reason)) |
| 312 boto.log.error('%s' % self.body) |
| 313 raise conn.ResponseError(self.http_response.status, |
| 314 self.http_response.reason, |
| 315 self.body) |
| 316 |
| 317 def add_standard_options(self): |
| 318 group = optparse.OptionGroup(self.parser, 'Standard Options') |
| 319 # add standard options that all commands get |
| 320 group.add_option('-D', '--debug', action='store_true', |
| 321 help='Turn on all debugging output') |
| 322 group.add_option('--debugger', action='store_true', |
| 323 default=False, |
| 324 help='Enable interactive debugger on error') |
| 325 group.add_option('-U', '--url', action='store', |
| 326 help='Override service URL with value provided') |
| 327 group.add_option('--region', action='store', |
| 328 help='Name of the region to connect to') |
| 329 group.add_option('-I', '--access-key-id', action='store', |
| 330 help='Override access key value') |
| 331 group.add_option('-S', '--secret-key', action='store', |
| 332 help='Override secret key value') |
| 333 group.add_option('--version', action='store_true', |
| 334 help='Display version string') |
| 335 if self.Filters: |
| 336 self.group.add_option('--help-filters', action='store_true', |
| 337 help='Display list of available filters') |
| 338 self.group.add_option('--filter', action='append', |
| 339 metavar=' name=value', |
| 340 help='A filter for limiting the results') |
| 341 self.parser.add_option_group(group) |
| 342 |
| 343 def process_standard_options(self, options, args, d): |
| 344 if hasattr(options, 'help_filters') and options.help_filters: |
| 345 print 'Available filters:' |
| 346 for filter in self.Filters: |
| 347 print '%s\t%s' % (filter.name, filter.doc) |
| 348 sys.exit(0) |
| 349 if options.debug: |
| 350 self.args['debug'] = 2 |
| 351 if options.url: |
| 352 self.args['url'] = options.url |
| 353 if options.region: |
| 354 self.args['region'] = options.region |
| 355 if options.access_key_id: |
| 356 self.args['aws_access_key_id'] = options.access_key_id |
| 357 if options.secret_key: |
| 358 self.args['aws_secret_access_key'] = options.secret_key |
| 359 if options.version: |
| 360 # TODO - Where should the version # come from? |
| 361 print 'version x.xx' |
| 362 exit(0) |
| 363 sys.excepthook = boto_except_hook(options.debugger, |
| 364 options.debug) |
| 365 |
| 366 def get_usage(self): |
| 367 s = 'usage: %prog [options] ' |
| 368 l = [ a.long_name for a in self.Args ] |
| 369 s += ' '.join(l) |
| 370 for a in self.Args: |
| 371 if a.doc: |
| 372 s += '\n\n\t%s - %s' % (a.long_name, a.doc) |
| 373 return s |
| 374 |
| 375 def build_cli_parser(self): |
| 376 self.parser = optparse.OptionParser(description=self.Description, |
| 377 usage=self.get_usage()) |
| 378 self.add_standard_options() |
| 379 for param in self.Params: |
| 380 ptype = action = choices = None |
| 381 if param.ptype in self.CLITypeMap: |
| 382 ptype = self.CLITypeMap[param.ptype] |
| 383 action = 'store' |
| 384 if param.ptype == 'boolean': |
| 385 action = 'store_true' |
| 386 elif param.ptype == 'array': |
| 387 if len(param.items) == 1: |
| 388 ptype = param.items[0]['type'] |
| 389 action = 'append' |
| 390 elif param.cardinality != 1: |
| 391 action = 'append' |
| 392 if ptype or action == 'store_true': |
| 393 if param.short_name: |
| 394 self.parser.add_option(param.optparse_short_name, |
| 395 param.optparse_long_name, |
| 396 action=action, type=ptype, |
| 397 choices=param.choices, |
| 398 help=param.doc) |
| 399 elif param.long_name: |
| 400 self.parser.add_option(param.optparse_long_name, |
| 401 action=action, type=ptype, |
| 402 choices=param.choices, |
| 403 help=param.doc) |
| 404 |
| 405 def do_cli(self): |
| 406 if not self.parser: |
| 407 self.build_cli_parser() |
| 408 self.cli_options, self.cli_args = self.parser.parse_args() |
| 409 d = {} |
| 410 self.process_standard_options(self.cli_options, self.cli_args, d) |
| 411 for param in self.Params: |
| 412 if param.long_name: |
| 413 p_name = param.long_name.replace('-', '_') |
| 414 else: |
| 415 p_name = boto.utils.pythonize_name(param.name) |
| 416 value = getattr(self.cli_options, p_name) |
| 417 if param.ptype == 'file' and value: |
| 418 if value == '-': |
| 419 value = sys.stdin.read() |
| 420 else: |
| 421 path = os.path.expanduser(value) |
| 422 path = os.path.expandvars(path) |
| 423 if os.path.isfile(path): |
| 424 fp = open(path) |
| 425 value = fp.read() |
| 426 fp.close() |
| 427 else: |
| 428 self.parser.error('Unable to read file: %s' % path) |
| 429 d[p_name] = value |
| 430 for arg in self.Args: |
| 431 if arg.long_name: |
| 432 p_name = arg.long_name.replace('-', '_') |
| 433 else: |
| 434 p_name = boto.utils.pythonize_name(arg.name) |
| 435 value = None |
| 436 if arg.cardinality == 1: |
| 437 if len(self.cli_args) >= 1: |
| 438 value = self.cli_args[0] |
| 439 else: |
| 440 value = self.cli_args |
| 441 d[p_name] = value |
| 442 self.args.update(d) |
| 443 if hasattr(self.cli_options, 'filter') and self.cli_options.filter: |
| 444 d = {} |
| 445 for filter in self.cli_options.filter: |
| 446 name, value = filter.split('=') |
| 447 d[name] = value |
| 448 if 'filters' in self.args: |
| 449 self.args['filters'].update(d) |
| 450 else: |
| 451 self.args['filters'] = d |
| 452 try: |
| 453 response = self.main() |
| 454 self.cli_formatter(response) |
| 455 except RequiredParamError, e: |
| 456 print e |
| 457 sys.exit(1) |
| 458 except self.ServiceClass.ResponseError, err: |
| 459 print 'Error(%s): %s' % (err.error_code, err.error_message) |
| 460 sys.exit(1) |
| 461 except boto.roboto.awsqueryservice.NoCredentialsError, err: |
| 462 print 'Unable to find credentials.' |
| 463 sys.exit(1) |
| 464 except Exception, e: |
| 465 print e |
| 466 sys.exit(1) |
| 467 |
| 468 def _generic_cli_formatter(self, fmt, data, label=''): |
| 469 if fmt['type'] == 'object': |
| 470 for prop in fmt['properties']: |
| 471 if 'name' in fmt: |
| 472 if fmt['name'] in data: |
| 473 data = data[fmt['name']] |
| 474 if fmt['name'] in self.list_markers: |
| 475 label = fmt['name'] |
| 476 if label[-1] == 's': |
| 477 label = label[0:-1] |
| 478 label = label.upper() |
| 479 self._generic_cli_formatter(prop, data, label) |
| 480 elif fmt['type'] == 'array': |
| 481 for item in data: |
| 482 line = Line(fmt, item, label) |
| 483 if isinstance(item, dict): |
| 484 for field_name in item: |
| 485 line.append(item[field_name]) |
| 486 elif isinstance(item, basestring): |
| 487 line.append(item) |
| 488 line.print_it() |
| 489 |
| 490 def cli_formatter(self, data): |
| 491 """ |
| 492 This method is responsible for formatting the output for the |
| 493 command line interface. The default behavior is to call the |
| 494 generic CLI formatter which attempts to print something |
| 495 reasonable. If you want specific formatting, you should |
| 496 override this method and do your own thing. |
| 497 |
| 498 :type data: dict |
| 499 :param data: The data returned by AWS. |
| 500 """ |
| 501 if data: |
| 502 self._generic_cli_formatter(self.Response, data) |
| 503 |
| 504 |
OLD | NEW |