From c39d98ccafa690cc88d0ed3da1eb37aefc52a3b0 Mon Sep 17 00:00:00 2001
From: Monty Taylor <mordred@inaugust.com>
Date: Fri, 1 Sep 2017 11:09:37 -0500
Subject: [PATCH] Move role normalization to normalize.py

Location has specific semantics for identity resources. Add a method to
get a projectless location. Add domain_id to project since all of the
identity resources have it already, but keep the parent-project
semantics already in place for project.

Change-Id: Ife37833baabf58d9e329071acb4187842815c7d2
---
 doc/source/user/model.rst | 84 ++++++++++++++++++++++++++-------------
 shade/_normalize.py       | 27 +++++++++----
 shade/_utils.py           | 12 ------
 shade/openstackcloud.py   | 12 ++++++
 shade/operatorcloud.py    |  6 +--
 5 files changed, 91 insertions(+), 50 deletions(-)

diff --git a/doc/source/user/model.rst b/doc/source/user/model.rst
index 3293b0306..ebfc3af97 100644
--- a/doc/source/user/model.rst
+++ b/doc/source/user/model.rst
@@ -65,6 +65,9 @@ If all of the project information is None, then
       domain_name=str() or None))
 
 
+Resources
+=========
+
 Flavor
 ------
 
@@ -324,34 +327,6 @@ A Floating IP from Neutron or Nova
     revision_number=int() or None,
     properties=dict())
 
-Project
--------
-
-A Project from Keystone (or a tenant if Keystone v2)
-
-Location information for Project has some specific semantics.
-
-If the project has a parent project, that will be in location.project.id,
-and if it doesn't that should be None. If the Project is associated with
-a domain that will be in location.project.domain_id regardless of the current
-user's token scope. location.project.name and location.project.domain_name
-will always be None. Finally, location.region_name will always be None as
-Projects are global to a cloud. If a deployer happens to deploy OpenStack
-in such a way that users and projects are not shared amongst regions, that
-necessitates treating each of those regions as separate clouds from shade's
-POV.
-
-.. code-block:: python
-
-  Project = dict(
-    location=Location(),
-    id=str(),
-    name=str(),
-    description=str(),
-    is_enabled=bool(),
-    is_domain=bool(),
-    properties=dict())
-
 Volume
 ------
 
@@ -502,3 +477,56 @@ A Stack from Heat
     tempate_description=str(),
     timeout_mins=int(),
     properties=dict())
+
+Identity Resources
+==================
+
+Identity Resources are slightly different.
+
+They are global to a cloud, so location.availability_zone and
+location.region_name and will always be None. If a deployer happens to deploy
+OpenStack in such a way that users and projects are not shared amongst regions,
+that necessitates treating each of those regions as separate clouds from
+shade's POV.
+
+The Identity Resources that are not Project do not exist within a Project,
+so all of the values in ``location.project`` will be None.
+
+Project
+-------
+
+A Project from Keystone (or a tenant if Keystone v2)
+
+Location information for Project has some additional specific semantics.
+If the project has a parent project, that will be in ``location.project.id``,
+and if it doesn't that should be ``None``.
+
+If the Project is associated with a domain that will be in
+``location.project.domain_id`` in addition to the normal ``domain_id``
+regardless of the current user's token scope.
+
+.. code-block:: python
+
+  Project = dict(
+    location=Location(),
+    id=str(),
+    name=str(),
+    description=str(),
+    is_enabled=bool(),
+    is_domain=bool(),
+    domain_id=str(),
+    properties=dict())
+
+Role
+----
+
+A Role from Keystone
+
+.. code-block:: python
+
+  Project = dict(
+    location=Location(),
+    id=str(),
+    name=str(),
+    domain_id=str(),
+    properties=dict())
diff --git a/shade/_normalize.py b/shade/_normalize.py
index b8e242b56..83783c776 100644
--- a/shade/_normalize.py
+++ b/shade/_normalize.py
@@ -643,19 +643,14 @@ class Normalizer(object):
         description = project.pop('description', '')
         is_enabled = project.pop('enabled', True)
 
-        # Projects are global - strip region
-        location = self._get_current_location(project_id=project_id)
-        location['region_name'] = None
-
         # v3 additions
         domain_id = project.pop('domain_id', 'default')
         parent_id = project.pop('parent_id', None)
         is_domain = project.pop('is_domain', False)
 
         # Projects have a special relationship with location
+        location = self._get_identity_location()
         location['project']['domain_id'] = domain_id
-        location['project']['domain_name'] = None
-        location['project']['name'] = None
         location['project']['id'] = parent_id
 
         ret = munch.Munch(
@@ -665,13 +660,13 @@ class Normalizer(object):
             description=description,
             is_enabled=is_enabled,
             is_domain=is_domain,
+            domain_id=domain_id,
             properties=project.copy()
         )
 
         # Backwards compat
         if not self.strict_mode:
             ret['enabled'] = is_enabled
-            ret['domain_id'] = domain_id
             ret['parent_id'] = parent_id
             for key, val in ret['properties'].items():
                 ret.setdefault(key, val)
@@ -1089,3 +1084,21 @@ class Normalizer(object):
         # TODO(mordred) Normalize this resource
 
         return machine
+
+    def _normalize_roles(self, roles):
+        """Normalize Keystone roles"""
+        ret = []
+        for role in roles:
+            ret.append(self._normalize_role(role))
+        return ret
+
+    def _normalize_role(self, role):
+        """Normalize Identity roles."""
+
+        return munch.Munch(
+            id=role.get('id'),
+            name=role.get('name'),
+            domain_id=role.get('domain_id'),
+            location=self._get_identity_location(),
+            properties={},
+        )
diff --git a/shade/_utils.py b/shade/_utils.py
index 4838fc7e7..750f07ab8 100644
--- a/shade/_utils.py
+++ b/shade/_utils.py
@@ -374,18 +374,6 @@ def normalize_role_assignments(assignments):
     return new_assignments
 
 
-def normalize_roles(roles):
-    """Normalize Identity roles."""
-    ret = [
-        dict(
-            domain_id=role.get('domain_id'),
-            id=role.get('id'),
-            name=role.get('name'),
-        ) for role in roles
-    ]
-    return meta.obj_list_to_munch(ret)
-
-
 def normalize_flavor_accesses(flavor_accesses):
     """Normalize Flavor access list."""
     return [munch.Munch(
diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py
index 39012bc72..531a66ba3 100644
--- a/shade/openstackcloud.py
+++ b/shade/openstackcloud.py
@@ -670,6 +670,18 @@ class OpenStackCloud(
             project=self._get_project_info(project_id),
         )
 
+    def _get_identity_location(self):
+        '''Identity resources do not exist inside of projects.'''
+        return munch.Munch(
+            cloud=self.name,
+            region_name=None,
+            zone=None,
+            project=munch.Munch(
+                id=None,
+                name=None,
+                domain_id=None,
+                domain_name=None))
+
     def _get_project_id_param_dict(self, name_or_id):
         if name_or_id:
             project = self.get_project(name_or_id)
diff --git a/shade/operatorcloud.py b/shade/operatorcloud.py
index 248bcd91f..3270a8f98 100644
--- a/shade/operatorcloud.py
+++ b/shade/operatorcloud.py
@@ -1390,7 +1390,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud):
         url = '/OS-KSADM/roles' if v2 else '/roles'
         data = self._identity_client.get(
             url, params=kwargs, error_message="Failed to list roles")
-        return _utils.normalize_roles(self._get_and_munchify('roles', data))
+        return self._normalize_roles(self._get_and_munchify('roles', data))
 
     @_utils.valid_kwargs('domain_id')
     def search_roles(self, name_or_id=None, filters=None, **kwargs):
@@ -1711,7 +1711,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud):
         data = self._identity_client.post(
             url, json={'role': kwargs}, error_message=msg)
         role = self._get_and_munchify('role', data)
-        return _utils.normalize_roles([role])[0]
+        return self._normalize_role(role)
 
     @_utils.valid_kwargs('domain_id')
     def update_role(self, name_or_id, name, **kwargs):
@@ -1740,7 +1740,7 @@ class OperatorCloud(openstackcloud.OpenStackCloud):
         data = self._identity_client.patch('/roles', error_message=msg,
                                            json=json_kwargs)
         role = self._get_and_munchify('role', data)
-        return _utils.normalize_roles([role])[0]
+        return self._normalize_role(role)
 
     @_utils.valid_kwargs('domain_id')
     def delete_role(self, name_or_id, **kwargs):