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