Commit f724cd35 authored by Stavros Sachtouris's avatar Stavros Sachtouris
Browse files

Get endpoint urls for all CLI operations

Refs: #3874

Use kamaki.clients.astakos.AstakosClient as a cached astakos client to get
user information and, most importantly, endpoints. Allow users to authenticate
multiple tokens on the same session. In every session there must be at most
one authentication per user/token.

Major change: top kamaki.cli.commands class now contains a base_auth field
with the cached authenticating client. All urls are drained from this field.
parent 05e144e2
......@@ -208,20 +208,26 @@ def _init_session(arguments):
_silent = arguments['silent'].value
_setup_logging(_silent, _debug, _verbose, _include)
global_url = arguments['config'].get('global', 'url')
global_token = arguments['config'].get('global', 'token')
from kamaki.clients.astakos import AstakosClient as AuthCachedClient
return AuthCachedClient(global_url, global_token)
def _load_spec_module(spec, arguments, module):
spec_name = arguments['config'].get(spec, 'cli')
if spec_name is None:
#spec_name = arguments['config'].get('cli', spec)
if not spec:
return None
pkg = None
for location in cmd_spec_locations:
location += spec_name if location == '' else '.%s' % spec_name
location += spec if location == '' else '.%s' % spec
pkg = __import__(location, fromlist=[module])
return pkg
except ImportError:
except ImportError as ie:
if not pkg:
kloger.debug('Loading cmd grp %s failed: %s' % (spec, ie))
return pkg
......@@ -229,43 +235,44 @@ def _groups_help(arguments):
global _debug
global kloger
descriptions = {}
for spec in arguments['config'].get_groups():
for cmd_group, spec in arguments['config'].get_cli_specs():
pkg = _load_spec_module(spec, arguments, '_commands')
if pkg:
cmds = None
_cnf = arguments['config']
cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(, 'cli')]
except AttributeError:
if _debug:
kloger.warning('No description for %s' % spec)
cmds = getattr(pkg, '_commands')
# #_cnf = arguments['config']
# #cmds = [cmd for cmd in getattr(pkg, '_commands') if _cnf.get(
# # 'cli',]
#except AttributeError:
# if _debug:
# kloger.warning('No description for %s' % cmd_group)
for cmd in cmds:
descriptions[] = cmd.description
except TypeError:
if _debug:
kloger.warning('no cmd specs in module %s' % spec)
'No cmd description for module %s' % cmd_group)
elif _debug:
kloger.warning('Loading of %s cmd spec failed' % spec)
kloger.warning('Loading of %s cmd spec failed' % cmd_group)
print('\nOptions:\n - - - -')
def _load_all_commands(cmd_tree, arguments):
_cnf = arguments['config']
specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
for spec in specs:
#specs = [spec for spec in _cnf.get_groups() if _cnf.get(spec, 'cli')]
for cmd_group, spec in _cnf.get_cli_specs():
spec_module = _load_spec_module(spec, arguments, '_commands')
spec_commands = getattr(spec_module, '_commands')
except AttributeError:
if _debug:
global kloger
kloger.warning('No valid description for %s' % spec)
kloger.warning('No valid description for %s' % cmd_group)
for spec_tree in spec_commands:
if == spec:
if == cmd_group:
......@@ -314,7 +321,7 @@ def print_error_message(cli_err):
errmsg = red(errmsg)
for errmsg in cli_err.details:
print('| %s' % errmsg)
print('| %s' % errmsg)
def exec_cmd(instance, cmd_args, help_method):
......@@ -358,20 +365,20 @@ def set_command_params(parameters):
# CLI Choice:
def run_one_cmd(exe_string, parser):
def run_one_cmd(exe_string, parser, auth_base):
global _history
_history = History(
parser.arguments['config'].get('history', 'file'))
_history.add(' '.join([exe_string] + argv[1:]))
from kamaki.cli import one_command, _help), parser, _help)
def run_shell(exe_string, parser):
def run_shell(exe_string, parser, auth_base):
from command_shell import _init_shell
shell = _init_shell(exe_string, parser)
_load_all_commands(shell.cmd_tree, parser.arguments), parser)
def main():
......@@ -389,18 +396,18 @@ def main():
filelog = logger.add_file_logger(__name__.split('.')[0])'* Initial Call *\n%s\n- - -' % ' '.join(argv))
auth_base = _init_session(parser.arguments)
from kamaki.cli.utils import suggest_missing
if parser.unparsed:
run_one_cmd(exe, parser)
run_one_cmd(exe, parser, auth_base)
elif _help:
run_shell(exe, parser)
run_shell(exe, parser, auth_base)
except CLIError as err:
if _debug:
......@@ -167,7 +167,10 @@ class ConfigArgument(Argument):
return self.value.get(group, term)
def get_groups(self):
return self.value.apis()
return self.value.keys('cli')
def get_cli_specs(self):
return self.value.items('cli')
_config_arg = ConfigArgument(
1, 'Path to configuration file',
......@@ -68,6 +68,7 @@ class Shell(Cmd):
_context_stack = []
_prompt_stack = []
_parser = None
auth_base = None
undoc_header = 'interactive shell commands:'
......@@ -197,11 +198,11 @@ class Shell(Cmd):
if subcmd.path == 'history_run':
instance = cls(
instance = cls(dict(cmd_parser.arguments))
instance = cls(
dict(cmd_parser.arguments), self.auth_base)
cmd_parser.arguments = instance.arguments
cmd_parser.syntax = '%s %s' % (
subcmd.path.replace('_', ' '), cls.syntax)
......@@ -296,7 +297,8 @@ class Shell(Cmd):
hdr = tmp_partition[0].strip()
return '%s commands:' % hdr
def run(self, parser, path=''):
def run(self, auth_base, parser, path=''):
self.auth_base = auth_base
self._parser = parser
self._history = History(
parser.arguments['config'].get('history', 'file'))
......@@ -40,7 +40,7 @@ log = get_logger(__name__)
class _command_init(object):
def __init__(self, arguments={}):
def __init__(self, arguments={}, auth_base=None):
if hasattr(self, 'arguments'):
if isinstance(self, _optional_output_cmd):
......@@ -52,6 +52,7 @@ class _command_init(object):
self.config = self['config']
except KeyError:
self.auth_base = auth_base or getattr(self, 'auth_base', None)
def _set_log_params(self):
......@@ -32,7 +32,7 @@
# or implied, of GRNET S.A.command
from kamaki.cli import command
from kamaki.clients.astakos import AstakosClient
#from kamaki.clients.astakos import AstakosClient
from kamaki.cli.commands import _command_init, errors, _optional_json
from kamaki.cli.command_tree import CommandTree
......@@ -45,11 +45,11 @@ class _user_init(_command_init):
def _run(self):
token = self.config.get('user', 'token')\
or self.config.get('global', 'token')
base_url = self.config.get('user', 'url')\
or self.config.get('global', 'url')
self.client = AstakosClient(base_url=base_url, token=token)
#token = self.config.get('user', 'token')\
# or self.config.get('global', 'token')
#base_url = self.config.get('global', 'url')
#self.client = AstakosClient(base_url=base_url, token=token)
self.client = self.auth_base
......@@ -71,9 +71,8 @@ class user_authenticate(_user_init, _optional_json):
def _run(self, custom_token=None):
super(self.__class__, self)._run()
title=('uuid', 'name',), with_redundancy=True)
r = self.auth_base.authenticate(custom_token)
self._print([r], title=('uuid', 'name',), with_redundancy=True)
def main(self, custom_token=None):
......@@ -41,7 +41,7 @@ _commands = [config_cmds]
about_options = '\nAbout options:\
\n. syntax: [group.]option\
\n. example: file.account\
\n. example: file.uuid\
\n. special case: <option> is equivalent to global.<option>\
\n. configuration file syntax:\
\n. [group]\
......@@ -70,8 +70,10 @@ class _init_cyclades(_command_init):
def _run(self, service='compute'):
token = self.config.get(service, 'token')\
or self.config.get('global', 'token')
base_url = self.config.get(service, 'url')\
or self.config.get('global', 'url')
cyclades_endpoints = self.auth_base.get_service_endpoints(
self.config.get('cyclades', 'type'),
self.config.get('cyclades', 'version'))
base_url = cyclades_endpoints['publicURL']
self.client = CycladesClient(base_url=base_url, token=token)
......@@ -54,7 +54,7 @@ class generic(object):
return _raise
def _connection(this, foo, base_url):
def _connection(this, foo):
def _raise(self, *args, **kwargs):
foo(self, *args, **kwargs)
......@@ -68,9 +68,7 @@ class generic(object):
' to get current token: /config get [server.]token'])
elif ce.status in range(-12, 200) + [302, 401, 403, 500]:
raiseCLIError(ce, importance=3, details=[
'Check if service is up or set to url %s' % base_url,
' to get url: /config get %s' % base_url,
' to set url: /config set %s <URL>' % base_url])
'Check if serviceis up'])
elif ce.status == 404 and 'kamakihttpresponse' in ce_msg:
client = getattr(self, 'client', None)
if not client:
......@@ -78,9 +76,9 @@ class generic(object):
url = getattr(client, 'base_url', '<empty>')
msg = 'Invalid service url %s' % url
raiseCLIError(ce, msg, details=[
'Please, check if service url is correctly set',
'* to get current url: /config get compute.url',
'* to set url: /config set compute.url <URL>'])
'Check if authentication url is correct',
' check current url: /config get url',
' set new auth. url: /config set url'])
return _raise
......@@ -90,8 +88,8 @@ class user(object):
_token_details = [
'To check default token: /config get token',
'If set/update a token:',
'* (permanent): /config set token <token>',
'* (temporary): re-run with <token> parameter']
'* (permanent): /config set token <token>',
'* (temporary): re-run with <token> parameter']
def load(this, foo):
......@@ -105,11 +103,11 @@ class user(object):
'No permanent token (try: kamaki config set token <tkn>)')
if not getattr(client, 'base_url', False):
msg = 'Missing astakos server URL'
msg = 'Missing synnefo URL'
raise CLIError(msg, importance=3, details=[
'Check if user.url is set correctly',
'To get astakos url: /config get user.url',
'To set astakos url: /config set user.url <URL>'])
'Check if authentication url is correct',
' check current url: /config get url',
' set new auth. url: /config set url'])
return r
return _raise
......@@ -164,7 +162,7 @@ class cyclades(object):
def connection(this, foo):
return generic._connection(foo, 'compute.url')
return generic._connection(foo)
def date(this, foo):
......@@ -360,7 +358,7 @@ class plankton(object):
def connection(this, foo):
return generic._connection(foo, 'image.url')
return generic._connection(foo)
def id(this, foo):
......@@ -408,7 +406,7 @@ class pithos(object):
def connection(this, foo):
return generic._connection(foo, 'file.url')
return generic._connection(foo)
def account(this, foo):
......@@ -160,7 +160,7 @@ class history_run(_init_history):
_cmd_tree = None
def __init__(self, arguments={}, cmd_tree=None):
def __init__(self, arguments={}, auth_base=None, cmd_tree=None):
super(self.__class__, self).__init__(arguments)
self._cmd_tree = cmd_tree
......@@ -77,6 +77,10 @@ class _init_image(_command_init):
token = self.config.get('image', 'token')\
or self.config.get('compute', 'token')\
or self.config.get('global', 'token')
plankton_endpoints = self.auth_base.get_service_endpoints(
self.config.get('plankton', 'type'),
self.config.get('plankton', 'version'))
base_url = plankton_endpoints['publicURL']
base_url = self.config.get('image', 'url')\
or self.config.get('compute', 'url')\
or self.config.get('global', 'url')
......@@ -305,7 +309,10 @@ class image_register(_init_image, _optional_json):
def _get_pithos_client(self, container):
if self['no_metafile_upload']:
return None
purl = self.config.get('file', 'url')
pithos_endpoints = self.auth_base.get_service_endpoints(
self.config.get('pithos', 'type'),
self.config.get('pithos', 'version'))
purl = pithos_endpoints['publicURL']
ptoken = self.client.token
return PithosClient(purl, ptoken, self._get_uuid(), container)
......@@ -154,8 +154,10 @@ class _pithos_init(_command_init):
def _run(self):
self.token = self.config.get('file', 'token')\
or self.config.get('global', 'token')
self.base_url = self.config.get('file', 'url')\
or self.config.get('global', 'url')
pithos_endpoints = self.auth_base.get_service_endpoints(
self.config.get('pithos', 'type'),
self.config.get('pithos', 'version'))
self.base_url = pithos_endpoints['publicURL']
self.container = self.config.get('file', 'container')\
or self.config.get('global', 'container')
......@@ -171,20 +173,14 @@ class _pithos_init(_command_init):
def _set_account(self):
user = AstakosClient(self.config.get('user', 'url'), self.token)
self.account = self['account'] or user.term('uuid')
"""Backwards compatibility"""
self.account = self.account\
or self.config.get('file', 'account')\
or self.config.get('global', 'account')
self.account = self.auth_base.user_term('uuid', self.token)
class _file_account_command(_pithos_init):
"""Base class for account level storage commands"""
def __init__(self, arguments={}):
super(_file_account_command, self).__init__(arguments)
def __init__(self, arguments={}, auth_base=None):
super(_file_account_command, self).__init__(arguments, auth_base)
self['account'] = ValueArgument(
'Set user account (not permanent)',
('-A', '--account'))
......@@ -207,8 +203,8 @@ class _file_container_command(_file_account_command):
container = None
path = None
def __init__(self, arguments={}):
super(_file_container_command, self).__init__(arguments)
def __init__(self, arguments={}, auth_base=None):
super(_file_container_command, self).__init__(arguments, auth_base)
self['container'] = ValueArgument(
'Set container to work with (temporary)',
('-C', '--container'))
......@@ -525,9 +521,10 @@ class _source_destination_command(_file_container_command):
suffix_replace=ValueArgument('', '--suffix-to-replace', default=''),
def __init__(self, arguments={}):
def __init__(self, arguments={}, auth_base=None):
super(_source_destination_command, self).__init__(self.arguments)
super(_source_destination_command, self).__init__(
self.arguments, auth_base)
def _run(self, source_container___path, path_is_optional=False):
super(_source_destination_command, self)._run(
......@@ -1480,8 +1477,8 @@ class file_delete(_file_container_command, _optional_output_cmd):
('-R', '--recursive'))
def __init__(self, arguments={}):
super(self.__class__, self).__init__(arguments)
def __init__(self, arguments={}, auth_base=None):
super(self.__class__, self).__init__(arguments, auth_base)
self['delimiter'] = DelimiterArgument(
......@@ -50,8 +50,8 @@ log = add_stream_logger(__name__)
class _astakos_init(_command_init):
def __init__(self, arguments=dict()):
super(_astakos_init, self).__init__(arguments)
def __init__(self, arguments=dict(), auth_base=None):
super(_astakos_init, self).__init__(arguments, auth_base)
self['token'] = ValueArgument('Custom token', '--token')
......@@ -61,9 +61,10 @@ class _astakos_init(_command_init):
or self.config.get('astakos', 'token')\
or self.config.get('user', 'token')\
or self.config.get('global', 'token')
base_url = self.config.get('astakos', 'url')\
or self.config.get('user', 'url')\
or self.config.get('global', 'url')
astakos_endpoints = self.auth_base.get_service_endpoints(
self.config.get('astakos', 'type'),
self.config.get('astakos', 'version'))
base_url = astakos_endpoints['publicURL']
self.client = AstakosClient(base_url, logger=log)
......@@ -108,12 +108,20 @@ class Config(RawConfigParser):
for option, val in options.items():
self.set(section, option, val)
def _get_dict(self, section, include_defaults=True):
d = dict(DEFAULTS[section]) if include_defaults else {}
except KeyError:
d = {}
d.update(RawConfigParser.items(self, section))
except NoSectionError:
return d
def reload(self):
self = self.__init__(self.path)
def apis(self):
return [api for api in self.sections() if api != 'global']
def get(self, section, option):
value = self._overrides.get(section, {}).get(option)
if value is not None:
......@@ -137,15 +145,12 @@ class Config(RawConfigParser):
except NoSectionError:
def keys(self, section, include_defaults=True):
d = self._get_dict(section, include_defaults)
return d.keys()
def items(self, section, include_defaults=True):
d = dict(DEFAULTS[section]) if include_defaults else {}
except KeyError:
d = {}
d.update(RawConfigParser.items(self, section))
except NoSectionError:
d = self._get_dict(section, include_defaults)
return d.items()
def override(self, section, option, value):
......@@ -55,7 +55,7 @@ def _get_best_match_from_cmd_tree(cmd_tree, unparsed):
return None
def run(parser, _help):
def run(auth_base, parser, _help):
group = get_command_group(list(parser.unparsed), parser.arguments)
if not group:
......@@ -68,7 +68,8 @@ def run(parser, _help):
global _best_match
_best_match = []
spec_module = _load_spec_module(group, parser.arguments, '_commands')
group_spec = parser.arguments['config'].get('cli', group)
spec_module = _load_spec_module(group_spec, parser.arguments, '_commands')
if spec_module is None:
raise CLIUnknownCommand(
'Could not find specs for %s commands' % group,
......@@ -95,7 +96,7 @@ def run(parser, _help):
cls = cmd.get_class()
executable = cls(parser.arguments)
executable = cls(parser.arguments, auth_base)
#parsed, unparsed = parse_known_args(parser, executable.arguments)
for term in _best_match:
......@@ -35,8 +35,6 @@ from kamaki.clients import Client, ClientError
from logging import getLogger
class AstakosClient(Client):
"""Synnefo Astakos API client"""
......@@ -138,5 +136,9 @@ class AstakosClient(Client):
return r['user']
def term(self, key, token=None):
"""Get (cached) term, from user credentials"""
return self.user_term(key, token)
def user_term(self, key, token=None):
"""Get (cached) term, from user credentials"""
return self.user_info(token).get(key, None)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment