From f29a849ffcc203e7038fd2a026e0f755dcf2c1fc Mon Sep 17 00:00:00 2001
From: Steve Martinelli <stevemar@ca.ibm.com>
Date: Thu, 18 Apr 2013 17:49:42 -0500
Subject: [PATCH] Finish up v3 role commands

* Add remove role
* Add --role to group list
* Add --role to user list
* Fix groups in AddRole()
* Remove the tweaks to utils.find_resource for domains; will address
  that across domains, projects, users and groups in another patch.
  I want to nail down the structure of these commands and get that into place

Change-Id: I8673dd8221ef88978dada5a2833c187026bdb31a
---
 openstackclient/common/utils.py      |  11 ++-
 openstackclient/identity/v3/group.py |  95 +++++++++++++++++--
 openstackclient/identity/v3/role.py  | 135 +++++++++++++++++++++------
 openstackclient/identity/v3/user.py  |  92 ++++++++++++++++--
 openstackclient/shell.py             |  15 ++-
 setup.cfg                            |   1 +
 6 files changed, 304 insertions(+), 45 deletions(-)

diff --git a/openstackclient/common/utils.py b/openstackclient/common/utils.py
index 56f9cd17d5..06542887e2 100644
--- a/openstackclient/common/utils.py
+++ b/openstackclient/common/utils.py
@@ -24,20 +24,27 @@ from openstackclient.common import exceptions
 
 def find_resource(manager, name_or_id):
     """Helper for the _find_* methods."""
-    # first try to get entity as integer id
+
+    # Try to get entity as integer id
     try:
         if isinstance(name_or_id, int) or name_or_id.isdigit():
             return manager.get(int(name_or_id))
     except exceptions.NotFound:
         pass
 
-    # now try to get entity as uuid
+    # Try to get entity as uuid
     try:
         uuid.UUID(str(name_or_id))
         return manager.get(name_or_id)
     except (ValueError, exceptions.NotFound):
         pass
 
+    # Try directly using the passed value
+    try:
+        return manager.get(name_or_id)
+    except Exception:
+        pass
+
     kwargs = {}
     if 'NAME_ATTR' in manager.resource_class.__dict__:
         # novaclient does this for oddball resources
diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py
index 0562b76630..3e7368f156 100644
--- a/openstackclient/identity/v3/group.py
+++ b/openstackclient/identity/v3/group.py
@@ -89,27 +89,108 @@ class DeleteGroup(command.Command):
 
 
 class ListGroup(lister.Lister):
-    """List group command"""
+    """List groups and optionally roles assigned to groups"""
 
     api = 'identity'
     log = logging.getLogger(__name__ + '.ListGroup')
 
     def get_parser(self, prog_name):
         parser = super(ListGroup, self).get_parser(prog_name)
+        parser.add_argument(
+            'group',
+            metavar='<group>',
+            nargs='?',
+            help='Name or ID of group to list [required with --role]',
+        )
+        parser.add_argument(
+            '--role',
+            action='store_true',
+            default=False,
+            help='List the roles assigned to <group>',
+        )
+        domain_or_project = parser.add_mutually_exclusive_group()
+        domain_or_project.add_argument(
+            '--domain',
+            metavar='<domain>',
+            help='Filter list by <domain> [Only valid with --role]',
+        )
+        domain_or_project.add_argument(
+            '--project',
+            metavar='<project>',
+            help='Filter list by <project> [Only valid with --role]',
+        )
         parser.add_argument(
             '--long',
             action='store_true',
             default=False,
-            help='Additional fields are listed in output')
+            help='Additional fields are listed in output',
+        )
         return parser
 
     def take_action(self, parsed_args):
         self.log.debug('take_action(%s)' % parsed_args)
-        if parsed_args.long:
-            columns = ('ID', 'Name', 'Domain ID', 'Description')
+        identity_client = self.app.client_manager.identity
+
+        if parsed_args.role:
+            # List roles belonging to group
+
+            # Group is required here, bail if it is not supplied
+            if not parsed_args.group:
+                sys.stderr.write('Error: Group must be specified')
+                # TODO(dtroyer): This lists the commands...I want it to
+                # show the help for _this_ command.
+                self.app.DeferredHelpAction(
+                    self.app.parser,
+                    self.app.parser,
+                    None,
+                    None,
+                )
+                return ([], [])
+
+            group = utils.find_resource(
+                identity_client.groups,
+                parsed_args.group,
+            )
+
+            if parsed_args.domain:
+                columns = ('ID', 'Name', 'Domain', 'Group')
+                domain = utils.find_resource(
+                    identity_client.domains,
+                    parsed_args.domain,
+                )
+                data = identity_client.roles.list(
+                    group=group,
+                    domain=domain,
+                )
+                for group_role in data:
+                    group_role.group = group.name
+                    group_role.domain = domain.name
+            elif parsed_args.project:
+                columns = ('ID', 'Name', 'Project', 'Group')
+                project = utils.find_resource(
+                    identity_client.projects,
+                    parsed_args.project,
+                )
+                data = identity_client.roles.list(
+                    group=group,
+                    project=project,
+                )
+                for group_role in data:
+                    group_role.group = group.name
+                    group_role.project = project.name
+            else:
+                # TODO(dtroyer): raise exception here, this really is an error
+                sys.stderr.write("Error: Must specify --domain or --project "
+                                 "with --role\n")
+                return ([], [])
         else:
-            columns = ('ID', 'Name')
-        data = self.app.client_manager.identity.groups.list()
+            # List groups
+            if parsed_args.long:
+                columns = ('ID', 'Name', 'Domain ID', 'Description')
+            else:
+                columns = ('ID', 'Name')
+            data = identity_client.groups.list()
+
         return (columns,
                 (utils.get_item_properties(
                     s, columns,
@@ -158,7 +239,7 @@ class SetGroup(command.Command):
             kwargs['domain'] = domain
 
         if not len(kwargs):
-            sys.stdout.write("Group not updated, no arguments present")
+            sys.stderr.write("Group not updated, no arguments present")
             return
         identity_client.groups.update(group.id, **kwargs)
         return
diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py
index faff906293..7387509ae2 100644
--- a/openstackclient/identity/v3/role.py
+++ b/openstackclient/identity/v3/role.py
@@ -26,7 +26,7 @@ from openstackclient.common import utils
 
 
 class AddRole(command.Command):
-    """Add role command"""
+    """Adds a role to a user or group on a domain or project"""
 
     api = 'identity'
     log = logging.getLogger(__name__ + '.AddRole')
@@ -42,23 +42,24 @@ class AddRole(command.Command):
         user_or_group.add_argument(
             '--user',
             metavar='<user>',
-            help='Name or ID of user to assign a role',
+            help='Name or ID of user to add a role',
         )
         user_or_group.add_argument(
             '--group',
             metavar='<group>',
-            help='Name or ID of group to assign a role',
+            help='Name or ID of group to add a role',
         )
         domain_or_project = parser.add_mutually_exclusive_group()
         domain_or_project.add_argument(
             '--domain',
             metavar='<domain>',
-            help='Name or ID of domain where user or group resides',
+            default='default',
+            help='Name or ID of domain associated with user or group',
         )
         domain_or_project.add_argument(
             '--project',
             metavar='<project>',
-            help='Name or ID of project where user or group resides',
+            help='Name or ID of project associated with user or group',
         )
         return parser
 
@@ -68,42 +69,40 @@ class AddRole(command.Command):
 
         if (not parsed_args.user and not parsed_args.domain
                 and not parsed_args.group and not parsed_args.project):
-            sys.stdout.write("Role not updated, no arguments present \n")
+            sys.stderr.write("Role not added, no arguments present\n")
             return
 
         role_id = utils.find_resource(identity_client.roles,
                                       parsed_args.role).id
 
-        if (parsed_args.user and parsed_args.domain):
+        if parsed_args.user and parsed_args.domain:
             user = utils.find_resource(identity_client.users,
                                        parsed_args.user)
             domain = utils.find_resource(identity_client.domains,
                                          parsed_args.domain)
             identity_client.roles.grant(role_id, user=user, domain=domain)
-            return
-        elif (parsed_args.user and parsed_args.project):
+        elif parsed_args.user and parsed_args.project:
             user = utils.find_resource(identity_client.users,
                                        parsed_args.user)
             project = utils.find_resource(identity_client.projects,
                                           parsed_args.project)
             identity_client.roles.grant(role_id, user=user, project=project)
-            return
-        elif (parsed_args.group and parsed_args.project):
+        elif parsed_args.group and parsed_args.domain:
+            group = utils.find_resource(identity_client.groups,
+                                        parsed_args.group)
+            domain = utils.find_resource(identity_client.domains,
+                                         parsed_args.domain)
+            identity_client.roles.grant(role_id, group=group, domain=domain)
+        elif parsed_args.group and parsed_args.project:
             group = utils.find_resource(identity_client.group,
                                         parsed_args.group)
             project = utils.find_resource(identity_client.projects,
                                           parsed_args.project)
             identity_client.roles.grant(role_id, group=group, project=project)
-            return
-        elif (parsed_args.group and parsed_args.domain):
-            group = utils.find_resource(identity_client.group,
-                                        parsed_args.group)
-            domain = utils.find_resource(identity_client.domains,
-                                         parsed_args.domain)
-            identity_client.roles.grant(role_id, group=group, domain=domain)
-            return
         else:
-            return
+            sys.stderr.write("Role not added, incorrect set of arguments \
+            provided. See openstack --help for more details\n")
+        return
 
 
 class CreateRole(show.ShowOne):
@@ -115,15 +114,16 @@ class CreateRole(show.ShowOne):
     def get_parser(self, prog_name):
         parser = super(CreateRole, self).get_parser(prog_name)
         parser.add_argument(
-            'role-name',
+            'name',
             metavar='<role-name>',
-            help='New role name')
+            help='New role name',
+        )
         return parser
 
     def take_action(self, parsed_args):
         self.log.debug('take_action(%s)' % parsed_args)
         identity_client = self.app.client_manager.identity
-        role = identity_client.roles.create(parsed_args.role_name)
+        role = identity_client.roles.create(parsed_args.name)
 
         return zip(*sorted(role._info.iteritems()))
 
@@ -139,7 +139,8 @@ class DeleteRole(command.Command):
         parser.add_argument(
             'role',
             metavar='<role>',
-            help='Name or ID of role to delete')
+            help='Name or ID of role to delete',
+        )
         return parser
 
     def take_action(self, parsed_args):
@@ -168,6 +169,85 @@ class ListRole(lister.Lister):
                 ) for s in data))
 
 
+class RemoveRole(command.Command):
+    """Remove role command"""
+
+    api = 'identity'
+    log = logging.getLogger(__name__ + '.RemoveRole')
+
+    def get_parser(self, prog_name):
+        parser = super(RemoveRole, self).get_parser(prog_name)
+        parser.add_argument(
+            'role',
+            metavar='<role>',
+            help='Name or ID of role to remove',
+        )
+        user_or_group = parser.add_mutually_exclusive_group()
+        user_or_group.add_argument(
+            '--user',
+            metavar='<user>',
+            help='Name or ID of user to remove a role',
+        )
+        user_or_group.add_argument(
+            '--group',
+            metavar='<group>',
+            help='Name or ID of group to remove a role',
+        )
+        domain_or_project = parser.add_mutually_exclusive_group()
+        domain_or_project.add_argument(
+            '--domain',
+            metavar='<domain>',
+            help='Name or ID of domain associated with user or group',
+        )
+        domain_or_project.add_argument(
+            '--project',
+            metavar='<project>',
+            help='Name or ID of project associated with user or group',
+        )
+        return parser
+
+    def take_action(self, parsed_args):
+        self.log.debug('take_action(%s)' % parsed_args)
+        identity_client = self.app.client_manager.identity
+
+        if (not parsed_args.user and not parsed_args.domain
+                and not parsed_args.group and not parsed_args.project):
+            sys.stdout.write("Role not updated, no arguments present\n")
+            return
+
+        role_id = utils.find_resource(identity_client.roles,
+                                      parsed_args.role).id
+
+        if parsed_args.user and parsed_args.domain:
+            user = utils.find_resource(identity_client.users,
+                                       parsed_args.user)
+            domain = utils.find_resource(identity_client.domains,
+                                         parsed_args.domain)
+            identity_client.roles.revoke(role_id, user=user, domain=domain)
+        elif parsed_args.user and parsed_args.project:
+            user = utils.find_resource(identity_client.users,
+                                       parsed_args.user)
+            project = utils.find_resource(identity_client.projects,
+                                          parsed_args.project)
+            identity_client.roles.revoke(role_id, user=user, project=project)
+        elif parsed_args.group and parsed_args.project:
+            group = utils.find_resource(identity_client.group,
+                                        parsed_args.group)
+            project = utils.find_resource(identity_client.projects,
+                                          parsed_args.project)
+            identity_client.roles.revoke(role_id, group=group, project=project)
+        elif parsed_args.group and parsed_args.domain:
+            group = utils.find_resource(identity_client.group,
+                                        parsed_args.group)
+            domain = utils.find_resource(identity_client.domains,
+                                         parsed_args.domain)
+            identity_client.roles.revoke(role_id, group=group, domain=domain)
+        else:
+            sys.stderr.write("Role not removed, incorrect set of arguments \
+            provided. See openstack --help for more details\n")
+        return
+
+
 class SetRole(command.Command):
     """Set role command"""
 
@@ -179,7 +259,7 @@ class SetRole(command.Command):
         parser.add_argument(
             'role',
             metavar='<role>',
-            help='Name or ID of role to change',
+            help='Name or ID of role to update',
         )
         parser.add_argument(
             '--name',
@@ -195,7 +275,7 @@ class SetRole(command.Command):
                                       parsed_args.role)
 
         if not parsed_args.name:
-            sys.stdout.write("Role not updated, no arguments present")
+            sys.stderr.write("Role not updated, no arguments present")
             return
 
         identity_client.roles.update(role_id, parsed_args.name)
@@ -213,7 +293,8 @@ class ShowRole(show.ShowOne):
         parser.add_argument(
             'role',
             metavar='<role>',
-            help='Name or ID of role to display')
+            help='Name or ID of role to display',
+        )
         return parser
 
     def take_action(self, parsed_args):
diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py
index 5e6282eb8c..53550cda56 100644
--- a/openstackclient/identity/v3/user.py
+++ b/openstackclient/identity/v3/user.py
@@ -135,7 +135,7 @@ class DeleteUser(command.Command):
 
 
 class ListUser(lister.Lister):
-    """List user command"""
+    """List users and optionally roles assigned to users"""
 
     api = 'identity'
     log = logging.getLogger(__name__ + '.ListUser')
@@ -143,9 +143,27 @@ class ListUser(lister.Lister):
     def get_parser(self, prog_name):
         parser = super(ListUser, self).get_parser(prog_name)
         parser.add_argument(
+            'user',
+            metavar='<user>',
+            nargs='?',
+            help='Name or ID of user to list [required with --role]',
+        )
+        parser.add_argument(
+            '--role',
+            action='store_true',
+            default=False,
+            help='List the roles assigned to <user>',
+        )
+        domain_or_project = parser.add_mutually_exclusive_group()
+        domain_or_project.add_argument(
+            '--domain',
+            metavar='<domain>',
+            help='Filter list by <domain> [Only valid with --role]',
+        )
+        domain_or_project.add_argument(
             '--project',
             metavar='<project>',
-            help='Name or ID of project to filter users',
+            help='Filter list by <project> [Only valid with --role]',
         )
         parser.add_argument(
             '--long',
@@ -157,12 +175,70 @@ class ListUser(lister.Lister):
 
     def take_action(self, parsed_args):
         self.log.debug('take_action(%s)' % parsed_args)
-        if parsed_args.long:
-            columns = ('ID', 'Name', 'Project Id', 'Domain Id',
-                       'Description', 'Email', 'Enabled')
+        identity_client = self.app.client_manager.identity
+
+        if parsed_args.role:
+            # List roles belonging to user
+
+            # User is required here, bail if it is not supplied
+            if not parsed_args.user:
+                sys.stderr.write('Error: User must be specified')
+                return ([], [])
+
+            user = utils.find_resource(
+                identity_client.users,
+                parsed_args.user,
+            )
+
+            # List a user's roles
+            if not parsed_args.domain and not parsed_args.project:
+                columns = ('ID', 'Name')
+                data = identity_client.roles.list(
+                    user=user,
+                    domain='default',
+                )
+            # List a user's roles on a domain
+            elif parsed_args.user and parsed_args.domain:
+                columns = ('ID', 'Name', 'Domain', 'User')
+                domain = utils.find_resource(
+                    identity_client.domains,
+                    parsed_args.domain,
+                )
+                data = identity_client.roles.list(
+                    user=user,
+                    domain=domain,
+                )
+                for user_role in data:
+                    user_role.user = user.name
+                    user_role.domain = domain.name
+            # List a user's roles on a project
+            elif parsed_args.user and parsed_args.project:
+                columns = ('ID', 'Name', 'Project', 'User')
+                project = utils.find_resource(
+                    identity_client.projects,
+                    parsed_args.project,
+                )
+                data = identity_client.roles.list(
+                    user=user,
+                    project=project,
+                )
+                for user_role in data:
+                    user_role.user = user.name
+                    user_role.project = project.name
+            else:
+                # TODO(dtroyer): raise exception here, this really is an error
+                sys.stderr.write("Error: Must specify --domain or --project "
+                                 "with --role\n")
+                return ([], [])
         else:
-            columns = ('ID', 'Name')
-        data = self.app.client_manager.identity.users.list()
+            # List users
+            if parsed_args.long:
+                columns = ('ID', 'Name', 'Project Id', 'Domain Id',
+                           'Description', 'Email', 'Enabled')
+            else:
+                columns = ('ID', 'Name')
+            data = self.app.client_manager.identity.users.list()
+
         return (columns,
                 (utils.get_item_properties(
                     s, columns,
@@ -253,7 +329,7 @@ class SetUser(command.Command):
             kwargs['enabled'] = parsed_args.enabled
 
         if not len(kwargs):
-            sys.stdout.write("User not updated, no arguments present")
+            sys.stderr.write("User not updated, no arguments present")
             return
         identity_client.users.update(user.id, **kwargs)
         return
diff --git a/openstackclient/shell.py b/openstackclient/shell.py
index 35d8255ddb..e5353194cd 100644
--- a/openstackclient/shell.py
+++ b/openstackclient/shell.py
@@ -37,6 +37,7 @@ DEFAULT_COMPUTE_API_VERSION = '2'
 DEFAULT_IDENTITY_API_VERSION = '2.0'
 DEFAULT_IMAGE_API_VERSION = '2'
 DEFAULT_VOLUME_API_VERSION = '1'
+DEFAULT_DOMAIN = 'default'
 
 
 def env(*vars, **kwargs):
@@ -134,6 +135,15 @@ class OpenStackShell(app.App):
             metavar='<auth-region-name>',
             default=env('OS_REGION_NAME'),
             help='Authentication region name (Env: OS_REGION_NAME)')
+        parser.add_argument(
+            '--os-default-domain',
+            metavar='<auth-domain>',
+            default=env(
+                'OS_DEFAULT_DOMAIN',
+                default=DEFAULT_DOMAIN),
+            help='Default domain ID, default=' +
+                 DEFAULT_DOMAIN +
+                 ' (Env: OS_DEFAULT_DOMAIN)')
         parser.add_argument(
             '--os-identity-api-version',
             metavar='<identity-api-version>',
@@ -304,7 +314,10 @@ class OpenStackShell(app.App):
         else:
             requests_log.setLevel(logging.WARNING)
 
-        # stash selected API versions for later
+        # Save default domain
+        self.default_domain = self.options.os_default_domain
+
+        # Stash selected API versions for later
         self.api_version = {
             'compute': self.options.os_compute_api_version,
             'identity': self.options.os_identity_api_version,
diff --git a/setup.cfg b/setup.cfg
index 535fb40507..03583c59d4 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -104,6 +104,7 @@ openstack.identity.v3 =
     role_create = openstackclient.identity.v3.role:CreateRole
     role_delete = openstackclient.identity.v3.role:DeleteRole
     role_list = openstackclient.identity.v3.role:ListRole
+    role_remove = openstackclient.identity.v3.role:RemoveRole
     role_show = openstackclient.identity.v3.role:ShowRole
     role_set = openstackclient.identity.v3.role:SetRole