def _key_pair(api_level='user'): """ Returns key pair(key id and secret key) for specified API scope. """ if api_level in ('global', ): api_key_id = settings.GLOBAL_SCOPE_API_KEY_ID secret_key = settings.GLOBAL_SCOPE_API_SECRET_KEY no_key_msg = "API {} for global scope is not configured. " \ "Please run 'scalr-ctl configure --with-global-scope' to change default " \ "authentication settings." assert api_key_id, no_key_msg.format("Key ID") assert secret_key, no_key_msg.format("Secret Key") elif api_level in ('user', 'account'): api_key_id = settings.API_KEY_ID secret_key = settings.API_SECRET_KEY no_key_msg = "API {} is not configured. Please specify option " \ "{} or run 'scalr-ctl configure' to change default " \ "authentication settings." assert api_key_id, no_key_msg.format("Key ID", "--key_id") assert secret_key, no_key_msg.format("Secret Key", "--secret_key") else: raise click.ClickException('Invalid Scalr API level') return api_key_id, secret_key
def _get_param(parent, child, key): head, _, tail = key.partition('.') if head == 'child': return reduce(dict.__getitem__, tail.split('.'), child) elif head == 'parent': return reduce(dict.__getitem__, tail.split('.'), parent) else: raise click.ClickException("Invalid key: \"{}\"".format(key))
def run(self, *args, **kwargs): hide_output = kwargs.pop('hide_output', False) kv = kwargs.copy() kv['hide_output'] = True response = super(Export, self).run(*args, **kv) kwargs['envId'] = settings.envId kv = copy.deepcopy(kwargs) for item in ('debug', 'nocolor', 'transformation'): if item in kv: del kv[item] uri = self._request_template.format(**kwargs) scalrctl_meta = { 'API_VERSION': settings.API_VERSION, 'envId': settings.envId, 'DATE': datetime.datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S'), 'API_HOST': settings.API_HOST, 'API_LEVEL': self.api_level, 'METHOD': self.http_method, 'ROUTE': self.route, 'URI': uri, 'ACTION': self.name, 'ARGUMENTS': (args, kv), 'SCALRCTL_VERSION': defaults.VERSION, } try: response_json = json.loads(response) except ValueError as e: if settings.debug_mode: raise raise click.ClickException(str(e)) response_json['meta']['scalrctl'] = scalrctl_meta result = [ response_json, ] if self.relations: relations = self._get_relations(response_json['data']) result.extend(relations) def _order(arg): action_name = arg['meta']['scalrctl']['ACTION'] return self.relations.get(action_name, {}).get('order', 0) result = sorted(result, key=_order) if not hide_output: dump = yaml.safe_dump(result, encoding='utf-8', allow_unicode=True, default_flow_style=False) click.echo(dump) return result
def reraise(message): import sys exc_info = sys.exc_info() if isinstance(exc_info[1], click.ClickException): exc_class = exc_info[0] else: exc_class = click.ClickException debug(traceback.format_exc()) message = str(message) if not settings.debug_mode: message = "{}, use '--debug' option for details.".format(message) raise click.ClickException(message)
def generate_post_data(spec_data, endpoint): """ Generates POST data for specified API endpoint. """ if endpoint in spec_data['paths']: params_spec = spec_data['paths'].get(endpoint) else: raise click.ClickException( 'API endpoint {} does not found'.format(endpoint)) if 'post' in params_spec: if 'parameters' in params_spec['post']: schema = params_spec['post']['parameters'][0]['schema'] post_data = _generate_params(spec_data, schema) else: post_data = {} else: raise click.ClickException( 'POST method for endpoint {} does not exist'.format(endpoint)) return post_data
def _check_arguments(self, **kwargs): route_data = self.raw_spec['paths'][self.route] if 'parameters' not in route_data: return for param in route_data['parameters']: pattern = param.get('pattern') param_name = param.get('name') if pattern and param_name and param_name in kwargs: value = str(kwargs[param_name]) matches = re.match(pattern, value.strip()) if not matches or len(matches.group()) != len(value): raise click.ClickException( "Invalid value for {}".format(param_name))
def get_definition(spec_data, endpoint): """ Returns object name by endpoint. """ if endpoint in spec_data['paths']: endpoint_spec = spec_data['paths'].get(endpoint) else: raise click.ClickException( 'API endpoint {} does not found'.format(endpoint)) for param in endpoint_spec['post'].get('parameters', ''): if '$ref' in param.get('schema', ''): return param.get('schema')['$ref'].split('/')[-1]
def _import_object(self, obj_data, env_id, update_mode, dry_run=False): args, kwargs = obj_data['meta']['scalrctl']['ARGUMENTS'] route = obj_data['meta']['scalrctl']['ROUTE'] http_method = 'patch' if update_mode else 'post' obj_name = obj_data['data'].get('name') action = None for action_name, section in self.scheme['export'].items(): if 'http-method' in section and 'route' in section and \ section['http-method'] == 'get' and \ section['route'] == route: scheme = section['{}-params'.format(http_method)] cls = pydoc.locate( scheme['class']) if 'class' in scheme else commands.Action action = cls(name=action_name, route=scheme['route'], http_method=http_method, api_level=self.api_level) break if not action: msg = "Cannot import Scalr object: API method '{}: {}' not found" \ .format('GET', route) raise click.ClickException(msg) obj_type = action._get_body_type_params()[0]['name'] if action.name not in ('role-image', ): kwargs['import-data'] = {obj_type: obj_data['data']} kwargs['dryrun'] = dry_run if env_id: kwargs['envId'] = env_id click.secho("{} {} {} {}...".format( "Updating" if update_mode else "Creating", self._get_object_alias(obj_type), '\"%s\"' % obj_name if obj_name else '', "ID" if update_mode else ""), bold=True) result = action.run(*args, **kwargs) result_json = json.loads(result) alias = self._get_object_alias(obj_type) click.secho("{} created.\n".format(alias), bold=True) return result_json
def run(self, *args, **kwargs): if 'debug' in kwargs: settings.debug_mode = kwargs.pop('debug') env_id = kwargs.pop('env_id', None) or settings.envId dry_run = kwargs.pop('dryrun', False) update_mode = kwargs.pop('update', False) raw_objects = kwargs.pop('raw', None) or click.get_text_stream('stdin') import_objects = self._validate_object(raw_objects) for obj in import_objects: action_name = obj['meta']['scalrctl']['ACTION'] obj_name = obj['data'].get('name') result = None try: if action_name == 'role-categories': # finds objects with the same names in new environment result = self._find_existing_object( action_name, obj_name, env_id) if result: msg = "Warning: \"{}\" already exists\n".format(obj_name) click.secho(msg, bold=True, fg='yellow') else: obj = self._modify_object( obj) # updates object body with new ID's result = self._import_object(obj, env_id, update_mode, dry_run) # save ID's of objects in new environment self._save_imported(obj, result['data']) except Exception as e: error_code = getattr(e, 'code', None) ignored = ('role-categories', 'role-global-variables') if error_code == 'UnicityViolation' and action_name in ignored: click.secho("Warning: {}\n".format(str(e)), bold=True, fg='yellow') else: # TODO: delete imported objects raise click.ClickException(str(e))
def _format_response(self, response, hidden=False, **kwargs): text = None if response: try: response_json = json.loads(response) except ValueError: utils.debug("Server response: {}".format(str(response))) utils.reraise("Invalid server response") if response_json.get('errors'): error_data = response_json['errors'][0] error = click.ClickException(error_data['message']) error.code = error_data.get('code') raise error if not hidden: utils.debug(response_json.get('meta')) if hidden: pass elif settings.view in ('raw', 'json'): click.echo(response) elif settings.view == 'xml': click.echo(dicttoxml.dicttoxml(response_json)) elif settings.view == 'tree': data = json.dumps(response_json.get('data')) click.echo(view.build_tree(data)) elif settings.view == 'table': columns = self._table_columns or self._get_column_names() rows, current_page, last_page = view.calc_table( response_json, columns) pre = "Page: {} of {}".format(current_page, last_page) click.echo(view.build_table(columns, rows, pre=pre)) # XXX elif self.http_method.upper() == 'DELETE': deleted_id = '' for param, value in kwargs.items(): if 'Id' in param and param != 'envId': deleted_id = value break text = "Deleted {}".format(deleted_id) return text
def read_spec(api_level, ext='json'): """ Reads Scalr specification file, json or yaml. """ spec_path = os.path.join(defaults.CONFIG_DIRECTORY, '{}.{}'.format(api_level, ext)) if os.path.exists(spec_path): with open(spec_path, 'r') as fp: spec_data = fp.read() if ext == 'json': return json.loads(spec_data) elif ext == 'yaml': return yaml.safe_load(spec_data) else: msg = "Scalr specification file '{}' does not exist, " \ "try to run 'scalr-ctl update'.".format(spec_path) raise click.ClickException(msg)
def cli(ctx, key_id, secret_key, config, *args, **kvargs): """Scalr-ctl is a command-line interface to your Scalr account""" service_cmd = any(arg in ('configure', 'update') for arg in sys.argv) if key_id: settings.API_KEY_ID = str(key_id) if secret_key: settings.API_SECRET_KEY = str(secret_key) elif (settings.API_KEY_ID and settings.API_KEY_ID.strip() and not settings.API_SECRET_KEY and not service_cmd): # [ST-21] settings.API_SECRET_KEY = str(click.prompt(text='API SECRET KEY', hide_input=True)) if config: if os.path.exists(config): config_data = yaml.load(open(config, 'r')) configure.apply_settings(config_data) else: msg = 'Configuration file not found: {}'.format(config) raise click.ClickException(msg)
def create_post_example(api_level, endpoint): """ Returns example for POST request. """ if endpoint in EXCLUDES: raise click.ClickException('Invalid API endpoint') spec_data = json.loads(_read_spec(api_level)) post_data = generate_post_data(spec_data, endpoint) object_name = get_definition(spec_data, endpoint) doc_url = get_doc_url(api_level, endpoint) example = ("The body must be a valid {name} object. " "Example value:\n{post_data}\n" "See more at {doc_url}\n\n" "Type your {name} object below this line. " "The above text will not be sent to the API server.").format( name=object_name, post_data=json.dumps(post_data, indent=2), doc_url=doc_url, ) example = '\n'.join(['# {}'.format(line) for line in example.split('\n')]) return example
def _filter_json_object(self, data, filter_createonly=False, schema=None, reference=None): """ Removes immutable parts from JSON object before sending it in POST or PATCH. """ filtered = {} # load `schema` if schema is None: for param in self._get_body_type_params(): if 'schema' in param: schema = param['schema'] if '$ref' in schema: reference = schema['$ref'] schema = self._lookup(schema['$ref']) break # load child object as `schema` if 'discriminator' in schema: disc_key = schema['discriminator'] disc_path = '{}/{}'.format(reference, disc_key) disc_value = data.get(disc_key) or self._discriminators.get( disc_path) if not disc_value: raise click.ClickException(( "Provided JSON object is incorrect: missing required param '{}'." ).format(disc_key)) elif disc_value not in self._list_concrete_types(schema): raise click.ClickException( ("Provided JSON object is incorrect: required " "param '{}' has invalid value '{}', must be one of: {}." ).format(disc_key, disc_value, self._list_concrete_types(schema))) else: # save discriminator for current reference/key self._discriminators[disc_path] = disc_value reference = '#/definitions/{}'.format(disc_value) schema = self._lookup(reference) # filter input data by properties of `schema` if schema and 'properties' in schema: create_only_props = schema.get('x-createOnly', '') for p_key, p_value in schema['properties'].items(): if reference: key_path = '.'.join([reference.split('/')[-1], p_key]) else: key_path = p_key if p_key not in data: utils.debug("Ignore {}, unknown key.".format(key_path)) continue if p_value.get('readOnly'): utils.debug("Ignore {}, read-only key.".format(key_path)) continue if filter_createonly and p_key in create_only_props: utils.debug("Ignore {}, create-only key.".format(key_path)) continue if '$ref' in p_value and isinstance(data[p_key], dict): # recursive filter sub-object utils.debug("Filter sub-object: {}.".format( p_value['$ref'])) filtered[p_key] = self._filter_json_object( data[p_key], filter_createonly=filter_createonly, reference=p_value['$ref'], schema=self._lookup(p_value['$ref']), ) else: # add valid key-value filtered[p_key] = data[p_key] return filtered
def dummy_run(): raise click.ClickException("Not implemented in current API version")
def request(method, api_level, request_uri, payload=None, data=None): """ Makes request to Scalr API. """ time_iso8601 = time.strftime('%Y-%m-%dT%H:%M:%S.000Z', time.gmtime()) try: api_key_id, secret_key = _key_pair(api_level=api_level) query_string = urlencode( sorted(payload.items()), quote_via=quote ) if payload else '' body = json.dumps(yaml.safe_load(data)) if data else '' # XXX string_to_sign = '\n'.join(( method.upper(), time_iso8601, request_uri, query_string, body )) digest = hmac.new( secret_key.encode('UTF-8'), string_to_sign.encode('UTF-8'), hashlib.sha256 ).digest() signature = '{} {}'.format( settings.SIGNATURE_VERSION, binascii.b2a_base64(digest).strip().decode('UTF-8') ) headers = dict() headers['Content-Type'] = 'application/json; charset=utf-8' headers['X-Scalr-Key-Id'] = api_key_id headers['X-Scalr-Date'] = time_iso8601 headers['X-Scalr-Signature'] = signature # if hasattr(settings, "API_DEBUG") and settings.API_DEBUG: # headers['X-Scalr-Debug'] = 1 url = urlunsplit(( settings.API_SCHEME, settings.API_HOST, request_uri, '', '' )) if settings.debug_mode: click.echo('API HOST: {}\n' 'stringToSign: {}\n' 'Headers: {}\n'.format( settings.API_HOST, string_to_sign, json.dumps(headers, indent=2)) ) resp = requests.request( method.lower(), url, data=body, params=payload, headers=headers, verify=settings.SSL_VERIFY_PEER ) result = resp.text except (Exception, BaseException) as e: if settings.debug_mode: raise raise click.ClickException(str(e)) return result
def reraise(message): debug(traceback.format_exc()) message = str(message) if not settings.debug_mode: message = "{}, use '--debug' option for details.".format(message) raise click.ClickException(message)