Implement user session commands for kamaki
authorStavros Sachtouris <saxtouri@admin.grnet.gr>
Wed, 30 Oct 2013 15:36:01 +0000 (17:36 +0200)
committerStavros Sachtouris <saxtouri@admin.grnet.gr>
Wed, 30 Oct 2013 15:36:01 +0000 (17:36 +0200)
Refs: #4340

kamaki/cli/__init__.py
kamaki/cli/commands/astakos.py
kamaki/cli/errors.py
kamaki/cli/one_command.py
kamaki/clients/astakos/__init__.py

index bd40f97..db58c84 100644 (file)
@@ -296,31 +296,46 @@ def _init_session(arguments, is_non_API=False):
     return cloud
 
 
-def init_cached_authenticator(url, tokens, config_module, logger):
+def init_cached_authenticator(config_argument, cloud, logger):
     try:
-        auth_base = None
-        for token in reversed(tokens):
+        _cnf = config_argument.value
+        url = _cnf.get_cloud(cloud, 'url')
+        tokens = _cnf.get_cloud(cloud, 'token').split()
+        auth_base, failed = None, []
+        for token in tokens:
             try:
                 if auth_base:
                     auth_base.authenticate(token)
                 else:
-                    auth_base = AuthCachedClient(url, token)
+                    tmp_base = AuthCachedClient(url, token)
                     from kamaki.cli.commands import _command_init
-                    fake_cmd = _command_init(dict(config=config_module))
+                    fake_cmd = _command_init(dict(config=config_argument))
                     fake_cmd.client = auth_base
                     fake_cmd._set_log_params()
                     fake_cmd._update_max_threads()
-                    auth_base.authenticate(token)
+                    tmp_base.authenticate(token)
+                    auth_base = tmp_base
             except ClientError as ce:
                 if ce.status in (401, ):
                     logger.warning(
                         'WARNING: Failed to authenticate token %s' % token)
+                    failed.append(token)
                 else:
                     raise
-        return auth_base
+        for token in failed:
+            r = raw_input(
+                'Token %s failed to authenticate. Remove it? [y/N]: ' % token)
+            if r in ('y', 'Y'):
+                tokens.remove(token)
+        if set(failed).difference(tokens):
+            _cnf.set_cloud(cloud, 'token', ' '.join(tokens))
+            _cnf.write()
+        if tokens:
+            return auth_base
+        logger.warning('WARNING: cloud.%s.token is now empty' % cloud)
     except AssertionError as ae:
         logger.warning('WARNING: Failed to load authenticator [%s]' % ae)
-        return None
+    return None
 
 
 def _load_spec_module(spec, arguments, module):
@@ -484,9 +499,7 @@ def run_shell(exe_string, parser, cloud):
     from command_shell import _init_shell
     global kloger
     _cnf = parser.arguments['config']
-    auth_base = init_cached_authenticator(
-        _cnf.get_cloud(cloud, 'url'), _cnf.get_cloud(cloud, 'token').split(),
-        _cnf, kloger)
+    auth_base = init_cached_authenticator(_cnf, cloud, kloger)
     try:
         username, userid = (
             auth_base.user_term('name'), auth_base.user_term('id'))
index 4426c07..37cd624 100644 (file)
@@ -39,7 +39,7 @@ from kamaki.clients.astakos import SynnefoAstakosClient
 from kamaki.cli.commands import (
     _command_init, errors, _optional_json, addLogSettings)
 from kamaki.cli.command_tree import CommandTree
-from kamaki.cli.errors import CLIBaseUrlError, CLISyntaxError
+from kamaki.cli.errors import CLIBaseUrlError, CLISyntaxError, CLIError
 from kamaki.cli.argument import (
     FlagArgument, ValueArgument, IntArgument, CommaSeparatedListArgument)
 from kamaki.cli.utils import format_size
@@ -59,7 +59,7 @@ def with_temp_token(foo):
             raise CLISyntaxError('A token is needed for %s' % foo)
         token_bu = self.client.token
         try:
-            self.client.token = token
+            self.client.token = token or token_bu
             return foo(self, *args, **kwargs)
         finally:
             self.client.token = token_bu
@@ -94,15 +94,15 @@ class _init_synnefo_astakosclient(_command_init):
 
 
 @command(user_commands)
-class user_info(_init_synnefo_astakosclient, _optional_json):
-    """Authenticate a user and get info"""
+class user_authenticate(_init_synnefo_astakosclient, _optional_json):
+    """Authenticate a user and get all authentication information"""
 
     @errors.generic.all
     @errors.user.authenticate
     @errors.user.astakosclient
     @with_temp_token
     def _run(self):
-        self._print(self.client.get_user_info(), self.print_dict)
+        self._print(self.client.authenticate(), self.print_dict)
 
     def main(self, token=None):
         super(self.__class__, self)._run()
@@ -173,6 +173,164 @@ class user_quotas(_init_synnefo_astakosclient, _optional_json):
         self._run()
 
 
+#  command user session
+
+
+@command(user_commands)
+class user_session(_init_synnefo_astakosclient):
+    """User session commands (cached identity calls for kamaki sessions)"""
+
+
+@command(user_commands)
+class user_session_info(_init_synnefo_astakosclient, _optional_json):
+    """Get info for (current) session user"""
+
+    arguments = dict(
+        uuid=ValueArgument('Query user with uuid', '--uuid'),
+        name=ValueArgument('Query user with username/email', '--username')
+    )
+
+    @errors.generic.all
+    @errors.user.astakosclient
+    def _run(self):
+        if self['uuid'] and self['name']:
+            raise CLISyntaxError(
+                'Arguments uuid and username are mutually exclusive',
+                details=['Use either uuid OR username OR none, not both'])
+        uuid = self['uuid'] or (self._username2uuid(self['name']) if (
+            self['name']) else None)
+        try:
+            token = self.auth_base.get_token(uuid) if uuid else None
+        except KeyError:
+            msg = ('id %s' % self['uuid']) if (
+                self['uuid']) else 'username %s' % self['name']
+            raise CLIError(
+                'No user with %s in the cached session list' % msg, details=[
+                    'To see all cached session users',
+                    '  /user session list',
+                    'To authenticate and add a new user in the session list',
+                    '  /user session authenticate <new token>'])
+        self._print(self.auth_base.user_info(token), self.print_dict)
+
+
+@command(user_commands)
+class user_session_authenticate(_init_synnefo_astakosclient, _optional_json):
+    """Authenticate a user by token and update cache"""
+
+    @errors.generic.all
+    @errors.user.astakosclient
+    def _run(self, token=None):
+        ask = token and token not in self.auth_base._uuids
+        self._print(self.auth_base.authenticate(token), self.print_dict)
+        if ask and self.ask_user(
+                'Token is temporarily stored in memory. If it is stored in'
+                ' kamaki configuration file, it will be available in later'
+                ' sessions. Do you want to permanently store this token?'):
+            tokens = self.auth_base._uuids.keys()
+            tokens.remove(self.auth_base.token)
+            self['config'].set_cloud(
+                self.cloud, 'token', ' '.join([self.auth_base.token] + tokens))
+            self['config'].write()
+
+    def main(self, new_token=None):
+        super(self.__class__, self)._run()
+        self._run(token=new_token)
+
+
+@command(user_commands)
+class user_session_list(_init_synnefo_astakosclient, _optional_json):
+    """List cached users"""
+
+    arguments = dict(
+        detail=FlagArgument('Detailed listing', ('-l', '--detail'))
+    )
+
+    @errors.generic.all
+    @errors.user.astakosclient
+    def _run(self):
+        self._print([u if self['detail'] else (dict(
+            id=u['id'], name=u['name'])) for u in self.auth_base.list_users()])
+
+    def main(self):
+        super(self.__class__, self)._run()
+        self._run()
+
+
+@command(user_commands)
+class user_session_select(_init_synnefo_astakosclient):
+    """Pick a user from the cached list to be the current session user"""
+
+    @errors.generic.all
+    @errors.user.astakosclient
+    def _run(self, uuid):
+        try:
+            first_token = self.auth_base.get_token(uuid)
+        except KeyError:
+            raise CLIError(
+                'No user with uuid %s in the cached session list' % uuid,
+                details=[
+                    'To see all cached session users',
+                    '  /user session list',
+                    'To authenticate and add a new user in the session list',
+                    '  /user session authenticate <new token>'])
+        if self.auth_base.token != first_token:
+            self.auth_base.token = first_token
+            msg = 'User with id %s is now the current session user.\n' % uuid
+            msg += 'Do you want future sessions to also start with this user?'
+            if self.ask_user(msg):
+                tokens = self.auth_base._uuids.keys()
+                tokens.remove(self.auth_base.token)
+                tokens.insert(0, self.auth_base.token)
+                self['config'].set_cloud(
+                    self.cloud, 'token',  ' '.join(tokens))
+                self['config'].write()
+                self.error('User is selected for next sessions')
+            else:
+                self.error('User is not permanently selected')
+        else:
+            self.error('User was already the selected session user')
+
+    def main(self, user_uuid):
+        super(self.__class__, self)._run()
+        self._run(uuid=user_uuid)
+
+
+@command(user_commands)
+class user_session_remove(_init_synnefo_astakosclient):
+    """Delete a user (token) from the cached list of session users"""
+
+    @errors.generic.all
+    @errors.user.astakosclient
+    def _run(self, uuid):
+        if uuid == self.auth_base.user_term('id'):
+            raise CLIError('Cannot remove current session user', details=[
+                'To see all cached session users',
+                '  /user session list',
+                'To see current session user',
+                '  /user session info',
+                'To select a different session user',
+                '  /user session select <user uuid>'])
+        try:
+            self.auth_base.remove_user(uuid)
+        except KeyError:
+            raise CLIError('No user with uuid %s in session list' % uuid,
+                details=[
+                    'To see all cached session users',
+                    '  /user session list',
+                    'To authenticate and add a new user in the session list',
+                    '  /user session authenticate <new token>'])
+        if self.ask_user(
+                'User is removed from current session, but will be restored in'
+                ' the next session. Remove the user from future sessions?'):
+            self['config'].set_cloud(
+                self.cloud, 'token', ' '.join(self.auth_base._uuids.keys()))
+            self['config'].write()
+
+    def main(self, user_uuid):
+        super(self.__class__, self)._run()
+        self._run(uuid=user_uuid)
+
+
 #  command admin
 
 @command(admin_commands)
@@ -405,14 +563,10 @@ class admin_feedback(_init_synnefo_astakosclient):
 class admin_endpoints(_init_synnefo_astakosclient, _optional_json):
     """Get endpoints service endpoints"""
 
-    arguments = dict(uuid=ValueArgument('User uuid', '--uuid'))
-
     @errors.generic.all
     @errors.user.astakosclient
     def _run(self):
-        self._print(
-            self.client.get_endpoints(self['uuid']),
-            self.print_dict)
+        self._print(self.client.get_endpoints(), self.print_dict)
 
     def main(self):
         super(self.__class__, self)._run()
index 16f22d3..3024957 100644 (file)
@@ -137,10 +137,10 @@ def raiseCLIError(err, message='', importance=0, details=[]):
         isinstance(details, list) or isinstance(details, tuple)) else [
             '%s' % details]
     err_details = getattr(err, 'details', [])
-    if not isinstance(details, list) or isinstance(details, tuple):
-        details.append('%s' % err_details)
-    else:
+    if isinstance(err_details, list) or isinstance(err_details, tuple):
         details += list(err_details)
+    else:
+        details.append('%s' % err_details)
 
     origerr = (('%s' % err) or '%s' % type(err)) if err else stack[0]
     message = '%s' % message or origerr
index 97eea62..426ef9d 100644 (file)
@@ -99,9 +99,8 @@ def run(cloud, parser, _help):
         exit(0)
 
     cls = cmd.cmd_class
-    auth_base = init_cached_authenticator(
-        _cnf.get_cloud(cloud, 'url'), _cnf.get_cloud(cloud, 'token').split(),
-        _cnf, kloger) if cloud else None
+    auth_base = init_cached_authenticator(_cnf, cloud, kloger) if (
+        cloud) else None
     executable = cls(parser.arguments, auth_base, cloud)
     parser.update_arguments(executable.arguments)
     for term in _best_match:
index c6a72e4..12ebdff 100644 (file)
 
 from logging import getLogger
 from astakosclient import AstakosClient as SynnefoAstakosClient
+from astakosclient import AstakosClientException as SynnefoAstakosClientError
 
 from kamaki.clients import Client, ClientError
 
 
+def _astakos_error(foo):
+    def wrap(self, *args, **kwargs):
+        try:
+            return foo(self, *args, **kwargs)
+        except SynnefoAstakosClientError as sace:
+            self._raise_for_status(sace)
+    return wrap
+
+
 class AstakosClient(Client):
     """Synnefo Astakos cached client wraper"""
 
+    @_astakos_error
     def __init__(self, base_url, token=None):
         super(AstakosClient, self).__init__(base_url, token)
         self._astakos = dict()
@@ -65,6 +76,7 @@ class AstakosClient(Client):
         self._validate_token(token)
         return self._astakos[self._uuids[token]]
 
+    @_astakos_error
     def authenticate(self, token=None):
         """Get authentication information and store it in this client
         As long as the AstakosClient instance is alive, the latest
@@ -75,7 +87,7 @@ class AstakosClient(Client):
         token = self._resolve_token(token)
         astakos = SynnefoAstakosClient(
             token, self.base_url, logger=getLogger('astakosclient'))
-        r = astakos.get_endpoints()
+        r = astakos.authenticate()
         uuid = r['access']['user']['id']
         self._uuids[token] = uuid
         self._cache[uuid] = r
@@ -84,6 +96,13 @@ class AstakosClient(Client):
         self._usernames2uuids[uuid] = dict()
         return self._cache[uuid]
 
+    def remove_user(self, uuid):
+        self._uuids.pop(self.get_token(uuid))
+        self._cache.pop(uuid)
+        self._astakos.pop(uuid)
+        self._uuids2usernames.pop(uuid)
+        self._usernames2uuids.pop(uuid)
+
     def get_token(self, uuid):
         return self._cache[uuid]['access']['token']['id']
 
@@ -181,8 +200,9 @@ class AstakosClient(Client):
         :returns: (dict) {uuid1: name1, uuid2: name2, ...} or oposite
         """
         return self.uuids2usernames(uuids, token) if (
-            uuids) else self.usernnames2uuids(displaynames, token)
+            uuids) else self.usernames2uuids(displaynames, token)
 
+    @_astakos_error
     def uuids2usernames(self, uuids, token=None):
         token = self._resolve_token(token)
         self._validate_token(token)
@@ -192,6 +212,7 @@ class AstakosClient(Client):
             self._uuids2usernames[uuid].update(astakos.get_usernames(uuids))
         return self._uuids2usernames[uuid]
 
+    @_astakos_error
     def usernames2uuids(self, usernames, token=None):
         token = self._resolve_token(token)
         self._validate_token(token)