Initial commit
This commit is contained in:
commit
69c6710c2e
|
@ -0,0 +1,10 @@
|
|||
.project
|
||||
.nfs*
|
||||
*~
|
||||
.sw*
|
||||
.*.sw*
|
||||
.DS_Store
|
||||
core
|
||||
core.*
|
||||
*.pyc
|
||||
*.pyo
|
|
@ -0,0 +1,13 @@
|
|||
Software license (ISC license):
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for
|
||||
any purpose with or without fee is hereby granted, provided that the
|
||||
above copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@ -0,0 +1,104 @@
|
|||
# Copyright (c) 2012, Eucalyptus Systems, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for
|
||||
# any purpose with or without fee is hereby granted, provided that the
|
||||
# above copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import argparse
|
||||
|
||||
__version__ = '0.0'
|
||||
|
||||
class Arg(object):
|
||||
"""
|
||||
A command line argument. Positional and keyword arguments to __init__
|
||||
are the same as those to argparse.ArgumentParser.add_argument.
|
||||
|
||||
The value specified by the 'dest' argument (or the one inferred if
|
||||
none is specified) is used as the name of the parameter to server
|
||||
queries unless send=False is also supplied.
|
||||
"""
|
||||
|
||||
def __init__(self, *pargs, **kwargs):
|
||||
self.route = kwargs.pop('route_to', PARAMS)
|
||||
self.pargs = pargs
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Arg):
|
||||
return sorted(self.pargs) == sorted(other.pargs)
|
||||
return False
|
||||
|
||||
|
||||
class MutuallyExclusiveArgList(list):
|
||||
"""
|
||||
Pass Args as positional arguments to __init__ to create a set of
|
||||
command line arguments that are mutually exclusive. If the first
|
||||
argument passed to __init__ is True then the user must specify
|
||||
exactly one of them.
|
||||
|
||||
Example: MutuallyExclusiveArgList(Arg('--one'), Arg('--two'))
|
||||
"""
|
||||
|
||||
def __init__(self, *args):
|
||||
if len(args) > 0 and isinstance(args[0], bool):
|
||||
self.required = args[0]
|
||||
list.__init__(self, args[1:])
|
||||
else:
|
||||
self.required = False
|
||||
list.__init__(self, args)
|
||||
|
||||
|
||||
class Filter(object):
|
||||
"""
|
||||
An AWS API filter. For APIs that support filtering by key/value
|
||||
pairs, adding a Filter to a request's list of filters will allow a
|
||||
user to send an output filter to the server with '--filter key=value'
|
||||
at the command line.
|
||||
|
||||
The value specified by the 'dest' argument (or the 'name' argument,
|
||||
if none is given) is used as the name of a filter in queries.
|
||||
"""
|
||||
def __init__(self, name, dest=None, type=str, choices=None, help=None):
|
||||
self.name = name # the name as shown on the command line
|
||||
self.dest = dest or name # the name as sent to the server
|
||||
self.type = type
|
||||
self.choices = choices
|
||||
self.help = help
|
||||
|
||||
def convert(self, str_value):
|
||||
"""
|
||||
Convert a value to the data type expected by this filter by calling
|
||||
self.type on the value. If this doesn't work then an ArgumentTypeError
|
||||
will result. If the conversion succeeds but does not appear in
|
||||
self.choices when it exists, an ArgumentTypeError will result as well.
|
||||
"""
|
||||
try:
|
||||
value = self.type(str_value)
|
||||
except ValueError:
|
||||
raise argparse.ArgumentTypeError('%s does not have type %s' %
|
||||
(str_value, self.type.__name__))
|
||||
if self.choices and value not in self.choices:
|
||||
msg = 'filter value %s does not match any of %s' % (value,
|
||||
', '.join([str(choice) for choice in self.choices]))
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
return value
|
||||
|
||||
|
||||
# Constants (enums?) used for arg routing
|
||||
CONNECTION = '==CONNECTION=='
|
||||
PARAMS = '==PARAMS=='
|
||||
|
||||
|
||||
STD_AUTH_ARGS = [
|
||||
Arg('-I', '--access-key-id', dest='aws_access_key_id',
|
||||
metavar='KEY_ID', route_to=CONNECTION),
|
||||
Arg('-S', '--secret-key', dest='aws_secret_access_key', metavar='KEY',
|
||||
route_to=CONNECTION)]
|
|
@ -0,0 +1,477 @@
|
|||
# Copyright (c) 2012, Eucalyptus Systems, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for
|
||||
# any purpose with or without fee is hereby granted, provided that the
|
||||
# above copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import argparse
|
||||
import bdb
|
||||
import boto.jsonresponse
|
||||
from functools import partial
|
||||
import json
|
||||
import pprint
|
||||
import sys
|
||||
import textwrap
|
||||
import traceback
|
||||
import types
|
||||
|
||||
try:
|
||||
import epdb
|
||||
except ImportError:
|
||||
import pdb
|
||||
|
||||
from . import __version__, CONNECTION, PARAMS, Arg, MutuallyExclusiveArgList
|
||||
from .service import BaseService, MissingCredentialsError
|
||||
|
||||
class InheritableRequestClass(type):
|
||||
'''
|
||||
Classes of this type allow one to specify 'Args', 'Filters', 'ListMarkers',
|
||||
and 'ItemMarkers' lists as attributes of classes and have them be appended
|
||||
to their superclasses' rather than clobbering them.
|
||||
|
||||
Additionally, all method calls are decorated with a function that renames
|
||||
logs to that of the the object's name() return value.
|
||||
'''
|
||||
def __new__(mcs, name, bases, attrs):
|
||||
for attrname in ('Args', 'Filters', 'ListMarkers', 'ItemMarkers'):
|
||||
if attrname in attrs:
|
||||
for base in bases:
|
||||
for attr in getattr(base, attrname, []):
|
||||
if attr not in attrs[attrname]:
|
||||
attrs[attrname].append(attr)
|
||||
return type.__new__(mcs, name, bases, attrs)
|
||||
|
||||
class BaseRequest(object):
|
||||
'''
|
||||
The basis for a command line tool that represents a request. To invoke
|
||||
this as a command line tool, call the do_cli() method on an instance of the
|
||||
class; arguments will be parsed from the command line. To invoke this in
|
||||
another context, pass keyword args to __init__ with names that match those
|
||||
stored by the argument parser and then call main().
|
||||
|
||||
Important methods in this class include:
|
||||
- do_cli: command line entry point
|
||||
- main: pre/post-request processing and request sending
|
||||
- send: actually send a request to the server and return a
|
||||
response (called by the main() method)
|
||||
- print_result: format data from the main method and print it to stdout
|
||||
|
||||
To be useful a tool should inherit from this class and implement the main()
|
||||
and print_result() methods. The do_cli() method functions as the entry
|
||||
point for the command line, populating self.args from the command line and
|
||||
then calling mein() and print_result() in sequence. Other tools may
|
||||
instead supply arguments via __init__() and then call main() alone.
|
||||
|
||||
Important members of this class include:
|
||||
- ServiceClass: a class corresponding to the web service in use
|
||||
- APIVersion: the API version to send along with the request. This is
|
||||
only necessary to override the service class's API version
|
||||
for a specific request.
|
||||
- Action: a string containing the Action query parameter. This
|
||||
defaults to the class's name.
|
||||
- Description: a string describing the tool. This becomes part of the
|
||||
command line help string.
|
||||
- Args: a list of Arg and/or MutuallyExclusiveArgGroup objects
|
||||
are used to generate command line arguments. Inheriting
|
||||
classes needing to add command line arguments should
|
||||
contain their own Args lists, which are *prepended* to
|
||||
those of their parent classes.
|
||||
- Filters: a list of Filter objects that are used to generate filter
|
||||
options at the command line. Inheriting classes needing
|
||||
to add filters should contain their own Filters lists,
|
||||
which are *prepended* to those of their parent classes.
|
||||
'''
|
||||
|
||||
__metaclass__ = InheritableRequestClass
|
||||
Version = 'requestbuilder ' + __version__
|
||||
|
||||
ServiceClass = BaseService
|
||||
APIVersion = None
|
||||
Action = None
|
||||
Description = ''
|
||||
|
||||
ListMarkers = []
|
||||
ItemMarkers = []
|
||||
|
||||
Args = [Arg('-D', '--debug', action='store_true', route_to=None,
|
||||
help='show debugging output'),
|
||||
Arg('--debugger', action='store_true', route_to=None,
|
||||
help='enable interactive debugger on error')]
|
||||
Filters = []
|
||||
|
||||
def name(self):
|
||||
'''
|
||||
The name of this action. Used when choosing what to supply for the
|
||||
Action query parameter.
|
||||
'''
|
||||
return self.Action or self.__class__.__name__
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Arguments corresponding to those in self.Args. This may be used in
|
||||
# lieu of (and will take priority over) arguments given at the CLI.
|
||||
self.args = kwargs
|
||||
|
||||
# Parts of the HTTP request to be sent to the server.
|
||||
# Note that self.flatten_params will update self.params for each entry
|
||||
# in self.args that routes to PARAMS.
|
||||
self.headers = None
|
||||
self.params = None
|
||||
self.post_data = None
|
||||
self.verb = 'GET'
|
||||
|
||||
# HTTP response obtained from the server
|
||||
self.http_response = None
|
||||
|
||||
self._arg_routes = {}
|
||||
self._cli_parser = None
|
||||
self._connection = None
|
||||
|
||||
self._parse_arg_lists()
|
||||
|
||||
@property
|
||||
def connection(self):
|
||||
if self._connection is None:
|
||||
conn_args = {}
|
||||
for (key, val) in self.args.iteritems():
|
||||
if key in self._arg_routes.get(CONNECTION):
|
||||
conn_args[key] = val
|
||||
self._connection = self.ServiceClass(**conn_args)
|
||||
return self._connection
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
if self.http_response is not None:
|
||||
return self.http_response.status
|
||||
else:
|
||||
return None
|
||||
|
||||
@property
|
||||
def reason(self):
|
||||
if self.http_response is not None:
|
||||
return self.http_response.reason
|
||||
else:
|
||||
return None
|
||||
|
||||
def print_result(self, data):
|
||||
'''
|
||||
Format data for printing at the command line and print it to standard
|
||||
out. The default formatter attempts to print JSON or something else
|
||||
reasonable. Override this method if you want specific formatting.
|
||||
'''
|
||||
if data:
|
||||
if isinstance(data, dict):
|
||||
for (key, val) in data.iteritems():
|
||||
if key not in ['ResponseMetadata', 'requestId']:
|
||||
# Will there ever be more than one of these?
|
||||
print json.dumps(val, indent=4)
|
||||
elif isinstance(data, list):
|
||||
print '\n'.join([str(item) for item in data])
|
||||
elif isinstance(data, basestring):
|
||||
print data
|
||||
else:
|
||||
pprint.pprint(data)
|
||||
|
||||
def flatten_params(self, args, route, prefix=None):
|
||||
'''
|
||||
Given a possibly-nested dict of args and an arg routing destination,
|
||||
transform each element in the dict that matches the corresponding
|
||||
arg routing table into a simple dict containing key-value pairs
|
||||
suitable for use as query parameters. This implementation flattens
|
||||
dicts and lists into the format given by the EC2 query API, which uses
|
||||
dotted lists of dict keys and list indices to indicate nested
|
||||
structures.
|
||||
|
||||
Keys with nonzero values that evaluate as false are ignored. If a
|
||||
collection of keys is supplied with ignore then keys that do not
|
||||
appear in that collection are also ignored.
|
||||
|
||||
Examples:
|
||||
in: {'InstanceId': 'i-12345678', 'PublicIp': '1.2.3.4'}
|
||||
out: {'InstanceId': 'i-12345678', 'PublicIp': '1.2.3.4'}
|
||||
|
||||
in: {'RegionName': ['us-east-1', 'us-west-1']}
|
||||
out: {'RegionName.1': 'us-east-1',
|
||||
'RegionName.2': 'us-west-1'}
|
||||
|
||||
in: {'Filter': [{'Name': 'image-id',
|
||||
'Value': ['ami-12345678']},
|
||||
{'Name': 'instance-type',
|
||||
'Value': ['m1.small', 't1.micro']}],
|
||||
'InstanceId': ['i-24680135']}
|
||||
out: {'Filter.1.Name': 'image-id',
|
||||
'Filter.1.Value.1': 'ami-12345678',
|
||||
'Filter.2.Name': 'instance-type',
|
||||
'Filter.2.Value.1': 'm1.small',
|
||||
'Filter.2.Value.2': 't1.micro',
|
||||
'InstanceId.1': 'i-24680135'}
|
||||
'''
|
||||
flattened = {}
|
||||
if args is None:
|
||||
return {}
|
||||
elif isinstance(args, dict):
|
||||
for (key, val) in args.iteritems():
|
||||
# Prefix.Key1, Prefix.Key2, ...
|
||||
if key in self._arg_routes.get(route, []) or route is _ALWAYS:
|
||||
if prefix:
|
||||
prefixed_key = prefix + '.' + str(key)
|
||||
else:
|
||||
prefixed_key = str(key)
|
||||
|
||||
if isinstance(val, dict) or isinstance(val, list):
|
||||
flattened.update(self.flatten_params(val, route,
|
||||
prefixed_key))
|
||||
elif isinstance(val, file):
|
||||
flattened[prefixed_key] = val.read()
|
||||
elif val or val is 0:
|
||||
flattened[prefixed_key] = str(val)
|
||||
elif isinstance(args, list):
|
||||
for (i_item, item) in enumerate(args, 1):
|
||||
# Prefix.1, Prefix.2, ...
|
||||
if prefix:
|
||||
prefixed_key = prefix + '.' + str(i_item)
|
||||
else:
|
||||
prefixed_key = str(i_item)
|
||||
|
||||
if isinstance(item, dict) or isinstance(item, list):
|
||||
flattened.update(self.flatten_params(item, route,
|
||||
prefixed_key))
|
||||
elif isinstance(item, file):
|
||||
flattened[prefixed_key] = item.read()
|
||||
elif item or item == 0:
|
||||
flattened[prefixed_key] = str(item)
|
||||
else:
|
||||
raise TypeError('non-flattenable type: ' + args.__class__.__name__)
|
||||
return flattened
|
||||
|
||||
def _parse_arg_lists(self):
|
||||
'''
|
||||
Use self.Args and self.Filters to build a command line argument parser
|
||||
that is stored to self._cli_parser and to populate the argument routing
|
||||
table.
|
||||
'''
|
||||
self._cli_parser = argparse.ArgumentParser(
|
||||
description='\n'.join(textwrap.wrap(self.Description)),
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
for arg_obj in self.Args:
|
||||
self.__add_arg_to_cli_parser(arg_obj, self._cli_parser)
|
||||
if self.Filters:
|
||||
self._cli_parser.add_argument('--filter', metavar='key=value',
|
||||
action='append', dest='_filters', help='filter output',
|
||||
type=partial(_parse_filter, filter_objs=self.Filters))
|
||||
self._arg_routes[None].append('_filters')
|
||||
self._cli_parser.epilog = self.__build_filter_help()
|
||||
self._cli_parser.add_argument('--version', action='version',
|
||||
version=self.Version)
|
||||
|
||||
def process_cli_args(self):
|
||||
'''
|
||||
Process CLI args to fill in missing parts of self.args and enable
|
||||
debugging if necessary.
|
||||
'''
|
||||
cli_args = self._cli_parser.parse_args().__dict__
|
||||
for (key, val) in cli_args.iteritems():
|
||||
self.args.setdefault(key, val)
|
||||
|
||||
if self.args.get('debug'):
|
||||
boto.set_stream_logger(self.name())
|
||||
if self.args.get('debugger'):
|
||||
sys.excepthook = _requestbuilder_except_hook(
|
||||
self.args.get('debugger', False),
|
||||
self.args.get('debug', False))
|
||||
|
||||
if '_filters' in self.args:
|
||||
self.params['Filter'] = _process_filters(cli_args.pop('_filters'))
|
||||
|
||||
def send(self):
|
||||
'''
|
||||
Send a request to the server and return its response. More precisely:
|
||||
|
||||
1. Build a flattened dict of params suitable for submission as HTTP
|
||||
request parameters, based first upon the content of self.params,
|
||||
and second upon everything in self.args that routes to PARAMS.
|
||||
2. Send an HTTP request via self.connection with the HTTP verb given
|
||||
in self.verb using query parameters from the aforementioned
|
||||
flattened dict, headers based on self.headers, and POST data based
|
||||
on self.post_data.
|
||||
3. If the response's status code indicates success, parse the
|
||||
response's body with self.parse_http_response and return the
|
||||
result.
|
||||
4. If the response's status code does not indicate success, log an
|
||||
error and raise a ResponseError.
|
||||
'''
|
||||
params = self.flatten_params(self.args, PARAMS)
|
||||
params.update(self.flatten_params(self.params, _ALWAYS))
|
||||
if self.headers:
|
||||
boto.log.debug('Request headers: {0}'.format(self.headers))
|
||||
if params:
|
||||
boto.log.debug('Request params: {0}'.format(params))
|
||||
self.http_response = self.connection.make_request(self.name(),
|
||||
verb=self.verb, headers=self.headers, params=params,
|
||||
data=self.post_data, api_version=self.APIVersion)
|
||||
response_body = self.http_response.read()
|
||||
boto.log.debug(response_body)
|
||||
if 200 <= self.http_response.status < 300:
|
||||
return self.parse_http_response(response_body)
|
||||
else:
|
||||
boto.log.error('{0} {1}'.format(self.http_response.status,
|
||||
self.http_response.reason))
|
||||
boto.log.error(response_body)
|
||||
raise self.connection.ResponseError(self.http_response.status,
|
||||
self.http_response.reason,
|
||||
response_body)
|
||||
|
||||
def parse_http_response(self, response_body):
|
||||
response = boto.jsonresponse.Element(list_marker=self.ListMarkers,
|
||||
item_marker=self.ItemMarkers)
|
||||
handler = boto.jsonresponse.XmlHandler(response, self)
|
||||
handler.parse(response_body)
|
||||
return response[response.keys()[0]] # Strip off the root element
|
||||
|
||||
def main(self):
|
||||
'''
|
||||
The main processing method for this type of request. In this method,
|
||||
inheriting classes generally populate self.headers, self.params, and
|
||||
self.post_data with information gathered from self.args or elsewhere,
|
||||
call self.send, and return the response.
|
||||
'''
|
||||
pass
|
||||
|
||||
def do_cli(self):
|
||||
'''
|
||||
The entry point for the command line. This method parses command line
|
||||
arguments using the class's Args and Filters lists to populate
|
||||
self.args, obtains a response from the main method, then passes the
|
||||
result to print_result.
|
||||
'''
|
||||
try:
|
||||
self.process_cli_args() # self.args is populated
|
||||
response = self.main()
|
||||
self.print_result(response)
|
||||
except self.ServiceClass.ResponseError as err:
|
||||
sys.exit('error ({code}) {msg}'.format(code=err.error_code,
|
||||
msg=err.error_message))
|
||||
except MissingCredentialsError as err:
|
||||
sys.exit('error: unable to find credentials')
|
||||
except Exception as err:
|
||||
if self.args.get('debug'):
|
||||
raise
|
||||
if '--debug' in sys.argv or '-D' in sys.argv:
|
||||
# In case an error occurs during argument parsing
|
||||
raise
|
||||
sys.exit('error: {0}'.format(err))
|
||||
|
||||
def __add_arg_to_cli_parser(self, arglike_obj, parser):
|
||||
if isinstance(arglike_obj, Arg):
|
||||
arg = parser.add_argument(*arglike_obj.pargs, **arglike_obj.kwargs)
|
||||
self._arg_routes.setdefault(arglike_obj.route, [])
|
||||
self._arg_routes[arglike_obj.route].append(arg.dest)
|
||||
elif isinstance(arglike_obj, MutuallyExclusiveArgList):
|
||||
exgroup = parser.add_mutually_exclusive_group(
|
||||
required=arglike_obj.required)
|
||||
for group_arg in arglike_obj:
|
||||
self.__add_arg_to_cli_parser(group_arg, exgroup)
|
||||
else:
|
||||
raise TypeError('Unknown argument type ' +
|
||||
arglike_obj.__class__.__name__)
|
||||
|
||||
def __build_filter_help(self):
|
||||
"""
|
||||
Return a pre-formatted help string for all of the filters defined in
|
||||
self.Filters. The result is meant to be used as command line help
|
||||
output.
|
||||
"""
|
||||
max_len = 24
|
||||
col_len = max([len(filter_obj.name) for filter_obj in self.Filters
|
||||
if len(filter_obj.name) < max_len]) + 1
|
||||
helplines = ['available filters:']
|
||||
for filter_obj in self.Filters:
|
||||
if filter_obj.help:
|
||||
if len(filter_obj.name) <= col_len:
|
||||
# filter-name Description of the filter that
|
||||
# continues on the next line
|
||||
right_space = ' ' * (max_len - len(filter_obj.name) - 2)
|
||||
wrapper = textwrap.TextWrapper(fix_sentence_endings=True,
|
||||
initial_indent=(' ' + filter_obj.name + right_space),
|
||||
subsequent_indent=(' ' * max_len))
|
||||
else:
|
||||
# really-long-filter-name
|
||||
# Description that begins on the next line
|
||||
helplines.append(' ' + filter_obj.name)
|
||||
wrapper = textwrap.TextWrapper(fix_sentence_endings=True,
|
||||
initial_indent=( ' ' * max_len),
|
||||
subsequent_indent=(' ' * max_len))
|
||||
helplines.extend(wrapper.wrap(filter_obj.help))
|
||||
else:
|
||||
helplines.append(' ' + filter_obj.name)
|
||||
return '\n'.join(helplines)
|
||||
|
||||
def _parse_filter(filter_str, filter_objs=None):
|
||||
"""
|
||||
Given a "key=value" string given as a command line parameter, return a pair
|
||||
with the matching filter's dest member and the given value after converting
|
||||
it to the type expected by the filter. If this is impossible, an
|
||||
ArgumentTypeError will result instead.
|
||||
"""
|
||||
if '=' not in filter_str:
|
||||
msg = 'filter %s must have format "key=value"' % filter_str
|
||||
raise argparse.ArgumentTypeError(msg)
|
||||
(key, val_as_str) = filter_str.split('=', 1)
|
||||
# Find the appropriate filter object
|
||||
try:
|
||||
filter_obj = [obj for obj in (filter_objs or []) if obj.name == key][0]
|
||||
val = filter_obj.convert(val_as_str)
|
||||
except IndexError:
|
||||
raise argparse.ArgumentTypeError('unknown filter: %s' % key)
|
||||
return (filter_obj.dest, val)
|
||||
|
||||
def _process_filters(cli_filters):
|
||||
"""
|
||||
Change filters from the [(key, value), ...] format given at the command
|
||||
line to [{'Name': key, 'Value': [value, ...]}, ...] format, which
|
||||
flattens to the form the server expects.
|
||||
"""
|
||||
filter_args = {}
|
||||
# Compile [(key, value), ...] pairs into {key: [value, ...], ...}
|
||||
for (key, val) in cli_filters or {}:
|
||||
filter_args.setdefault(key, [])
|
||||
filter_args[key].append(val)
|
||||
# Build the flattenable [{'Name': key, 'Value': [value, ...]}, ...]
|
||||
filters = [{'Name': name, 'Value': values} for (name, values)
|
||||
in filter_args.iteritems()]
|
||||
return filters
|
||||
|
||||
def _requestbuilder_except_hook(debugger_enabled, debug_enabled):
|
||||
"""
|
||||
Wrapper for the debugger-launching except hook
|
||||
"""
|
||||
def excepthook(typ, value, tracebk):
|
||||
"""
|
||||
If the debugger option is enabled, launch epdb (or pdb if epdb is
|
||||
unavailable) when an uncaught exception occurs.
|
||||
"""
|
||||
if typ is bdb.BdbQuit:
|
||||
sys.exit(1)
|
||||
sys.excepthook = sys.__excepthook__
|
||||
|
||||
if debugger_enabled and sys.stdout.isatty() and sys.stdin.isatty():
|
||||
if 'epdb' in sys.modules:
|
||||
epdb.post_mortem(tracebk, typ, value)
|
||||
else:
|
||||
pdb.post_mortem(tracebk)
|
||||
elif debug_enabled:
|
||||
traceback.print_tb(tracebk)
|
||||
sys.exit(1)
|
||||
else:
|
||||
print value
|
||||
sys.exit(1)
|
||||
return excepthook
|
||||
|
||||
_ALWAYS = '==ALWAYS=='
|
|
@ -0,0 +1,113 @@
|
|||
# Copyright (c) 2012, Eucalyptus Systems, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for
|
||||
# any purpose with or without fee is hereby granted, provided that the
|
||||
# above copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import boto.connection
|
||||
import boto.exception
|
||||
import os.path
|
||||
import urlparse
|
||||
|
||||
class BaseService(boto.connection.AWSAuthConnection):
|
||||
Description = ''
|
||||
APIVersion = ''
|
||||
|
||||
Authentication = 'sign-v2'
|
||||
Path = '/'
|
||||
Port = 443
|
||||
Provider = 'aws'
|
||||
EnvURL = 'AWS_URL'
|
||||
ResponseError = boto.exception.BotoServerError
|
||||
|
||||
# Endpoint names (i.e. 'us-east-1') and their URLs
|
||||
Endpoints = {}
|
||||
|
||||
def __init__(self, url=None, **kwargs):
|
||||
self._init_args = kwargs
|
||||
|
||||
self.find_credentials()
|
||||
self._read_url_info(url or os.getenv(self.EnvURL))
|
||||
if self.Endpoints:
|
||||
if 'region_name' in self._init_args:
|
||||
endpoint = self.Endpoints.get(self._init_args['region_name'])
|
||||
self._read_url_info(endpoint)
|
||||
else:
|
||||
self._read_url_info(self.Endpoints.values()[0])
|
||||
self._init_args.setdefault('path', self.Path)
|
||||
self._init_args.setdefault('port', self.Port)
|
||||
self._init_args.setdefault('provider', self.Provider)
|
||||
try:
|
||||
boto.connection.AWSAuthConnection.__init__(self, **self._init_args)
|
||||
except boto.exception.NoAuthHandlerFound:
|
||||
raise MissingCredentialsError()
|
||||
|
||||
def find_credentials(self):
|
||||
'''
|
||||
If the 'AWS_CREDENTIAL_FILE' environment variable exists, parse that
|
||||
file for access keys and use them if keys were not already supplied to
|
||||
__init__.
|
||||
'''
|
||||
if 'AWS_CREDENTIAL_FILE' in os.environ:
|
||||
path = os.getenv('AWS_CREDENTIAL_FILE')
|
||||
path = os.path.expandvars(path)
|
||||
path = os.path.expanduser(path)
|
||||
with open(path) as credfile:
|
||||
for line in credfile:
|
||||
line = line.split('#', 1)[0]
|
||||
if '=' in line:
|
||||
(key, val) = line.split('=', 1)
|
||||
if key.strip() == 'AWSAccessKeyId':
|
||||
self._init_args.setdefault('aws_access_key_id',
|
||||
val.strip())
|
||||
elif key.strip() == 'AWSSecretKey':
|
||||
self._init_args.setdefault('aws_secret_access_key',
|
||||
val.strip())
|
||||
|
||||
def make_request(self, action, verb='GET', path='/', params=None,
|
||||
headers=None, data='', api_version=None):
|
||||
request = self.build_base_http_request(verb, path, None, params,
|
||||
headers or {}, data)
|
||||
if action:
|
||||
request.params['Action'] = action
|
||||
if api_version:
|
||||
request.params['Version'] = api_version
|
||||
elif self.APIVersion:
|
||||
request.params['Version'] = self.APIVersion
|
||||
return self._mexe(request)
|
||||
|
||||
def _read_url_info(self, url):
|
||||
"""
|
||||
Parse a URL and use it to fill in is_secure, host, port, and path if
|
||||
any are missing.
|
||||
"""
|
||||
if url:
|
||||
parse_result = urlparse.urlparse(url)
|
||||
if parse_result[0] == 'https':
|
||||
self._init_args.setdefault('is_secure', True)
|
||||
else:
|
||||
self._init_args.setdefault('is_secure', False)
|
||||
if ':' in parse_result[1]:
|
||||
(host, port) = parse_result[1].rsplit(':', 1)
|
||||
self._init_args.setdefault('host', host)
|
||||
self._init_args.setdefault('port', int(port))
|
||||
else:
|
||||
self._init_args.setdefault('host', parse_result[1])
|
||||
if parse_result[2]:
|
||||
self._init_args.setdefault('path', parse_result[2])
|
||||
|
||||
def _required_auth_capability(self):
|
||||
return [self.Authentication]
|
||||
|
||||
class MissingCredentialsError(boto.exception.BotoClientError):
|
||||
def __init__(self):
|
||||
boto.exception.BotoClientError.__init__(self,
|
||||
'Failed to find credentials')
|
|
@ -0,0 +1,36 @@
|
|||
# Copyright (c) 2012, Eucalyptus Systems, Inc.
|
||||
#
|
||||
# Permission to use, copy, modify, and/or distribute this software for
|
||||
# any purpose with or without fee is hereby granted, provided that the
|
||||
# above copyright notice and this permission notice appear in all copies.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
|
||||
# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
from distutils.core import setup
|
||||
|
||||
from requestbuilder import __version__
|
||||
|
||||
setup(name = 'requestbuilder',
|
||||
version = __version__,
|
||||
description = 'Command line-driven HTTP request builder',
|
||||
author = 'Garrett Holmstrom (gholms)',
|
||||
author_email = 'gholms@fedoraproject.org',
|
||||
packages = ['requestbuilder'],
|
||||
license = 'ISC',
|
||||
platforms = 'Posix; MacOS X',
|
||||
classifiers = ['Development Status :: 2 - Pre-Alpha',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: ISC License (ISCL)',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.6',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Topic :: Internet'],
|
||||
requires = ['boto (>= 2.2)'],
|
||||
provides = ['requestbuilder'])
|
Loading…
Reference in New Issue