Home | History | Annotate | Download | only in roboto
      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