From 54f6bd1da3e459293c6cef8d3ee66215f5275836 Mon Sep 17 00:00:00 2001 From: Seunghun Lee Date: Wed, 5 Aug 2026 10:06:01 +0100 Subject: [PATCH 1/2] Add support for group definition and projects role assignment --- roles/os_projects/defaults/main.yml | 30 +++++++++- roles/os_projects/tasks/groups.yml | 62 ++++++++++++++++++++ roles/os_projects/tasks/projects.yml | 13 +++++ roles/os_projects/tasks/role_assignment.yml | 65 +++++++++++++++++++++ roles/os_projects/tasks/users.yml | 44 -------------- 5 files changed, 168 insertions(+), 46 deletions(-) create mode 100644 roles/os_projects/tasks/groups.yml create mode 100644 roles/os_projects/tasks/role_assignment.yml diff --git a/roles/os_projects/defaults/main.yml b/roles/os_projects/defaults/main.yml index 59a6a2b..125c4c6 100644 --- a/roles/os_projects/defaults/main.yml +++ b/roles/os_projects/defaults/main.yml @@ -17,6 +17,29 @@ os_projects_environment: # - 'name': The name of the domain. # - 'description': Optional description for the domain. os_projects_domains: [] +# List of OpenStack groups to create. +# Each item should be a dict containing the following items: +# - 'name': The name of the group. +# - 'domain': The domain to contain the new group. +# - 'description': Optional description for the group. +# - 'create_group': Optional Bookean (Default: true) to indicate whether to +# create the group. This can be useful if the group already exists. +# (e.g. LDAP defined groups) +# - 'users': Optional list of users to add to the group. Each user should be a +# dict containing following items: +# - 'name': The name of the user. +# - 'domain': The domain where user is registered. +os_projects_groups: [] +# List of OpenStack users to create. +# Each item should be a dict containing the following items: +# - 'name': The name of the user. +# - 'password': The user's password. +# - 'domain': The domain in which to register users. +# - 'openrc_file': Optional path to an environment file to create. +# - 'create_user': Optional boolean (Default: true) to indicate whether to +# create the user. This can be useful if the user already exists. +# (e.g. LDAP defined users) +os_projects_users: [] # List of OpenStack projects to create. # Each item should be a dict containing the following items: # - 'name': The name of the project. @@ -26,11 +49,14 @@ os_projects_domains: [] # - 'users': Optional list of users to register. Each user should be a dict # containing the following items: # - 'name': The name of the user. -# - 'password': The user's password. # - 'roles': Optional list of roles to assign to the user in the project. # - 'domain_roles': Optional list of roles to assign to the user in the user # domain. -# - 'openrc_file': Optional path to an environment file to create. +# - 'groups': Optional list of groups to register. Each group should be a dict +# containing the following items: +# - 'name': The name of the group. +# - 'domain': The domain to contain the new group. +# - 'roles': Optional list of roles to assign to the group in the project. # - 'keypairs': Optional list of SSH key pairs to register with Nova. Each key # pair should be a dict containing the following items: # - 'name': The name of the keypair. diff --git a/roles/os_projects/tasks/groups.yml b/roles/os_projects/tasks/groups.yml new file mode 100644 index 0000000..30866a1 --- /dev/null +++ b/roles/os_projects/tasks/groups.yml @@ -0,0 +1,62 @@ +--- +- name: Ensure the groups exist + openstack.cloud.identity_group: + auth_type: "{{ os_projects_auth_type }}" + auth: "{{ os_projects_admin_auth }}" + region_name: "{{ os_projects_region | default(omit) }}" + cacert: "{{ os_projects_cacert | default(omit) }}" + cloud: "{{ os_projects_cloud | default(omit) }}" + interface: "{{ os_projects_interface | default(omit, true) }}" + name: "{{ group.name }}" + description: "{{ group.description }}" + domain_id: "{{ group_domain_id }}" + state: present + wait: true + when: group.create_group | default(true) | bool + environment: "{{ os_projects_environment }}" + vars: + group_domain_id: "{{ (group.domain in os_projects_domain_to_id.values()) | ternary(group.domain, os_projects_domain_to_id[group.domain]) }}" + +- name: Ensure group users exist + openstack.cloud.identity_user: + auth_type: "{{ os_projects_auth_type }}" + auth: "{{ os_projects_admin_auth }}" + region_name: "{{ os_projects_region | default(omit) }}" + cacert: "{{ os_projects_cacert | default(omit) }}" + cloud: "{{ os_projects_cloud | default(omit) }}" + interface: "{{ os_projects_interface | default(omit, true) }}" + name: "{{ item.name }}" + description: "{{ item.description | default(omit) }}" + email: "{{ item.email | default(omit) }}" + password: "{{ item.password | default(omit) }}" + domain: "{{ user_domain_id }}" + state: present + enabled: true + wait: true + with_items: "{{ group.users }}" + when: item.create_user | default(true) | bool + environment: "{{ os_projects_environment }}" + vars: + user_domain_id: "{{ (item.domain in os_projects_domain_to_id.values()) | ternary(item.domain, os_projects_domain_to_id[item.domain]) }}" + loop_control: + label: "{{ group.name }}: {{ item.name }}" + +- name: Ensure users are added to the group + openstack.cloud.group_assignment: + auth_type: "{{ os_projects_auth_type }}" + auth: "{{ os_projects_admin_auth }}" + region_name: "{{ os_projects_region | default(omit) }}" + cacert: "{{ os_projects_cacert | default(omit) }}" + cloud: "{{ os_projects_cloud | default(omit) }}" + interface: "{{ os_projects_interface | default(omit, true) }}" + user: "{{ item.name }}" + group: "{{ group.name }}" + state: present + wait: true + with_items: "{{ group.users }}" + when: item.create_user | default(true) | bool + environment: "{{ os_projects_environment }}" + vars: + user_domain_id: "{{ (item.domain in os_projects_domain_to_id.values()) | ternary(item.domain, os_projects_domain_to_id[item.domain]) }}" + loop_control: + label: "{{ group.name }}: {{ item.name }}" diff --git a/roles/os_projects/tasks/projects.yml b/roles/os_projects/tasks/projects.yml index 54c6fa4..f4ebd5b 100644 --- a/roles/os_projects/tasks/projects.yml +++ b/roles/os_projects/tasks/projects.yml @@ -144,6 +144,19 @@ loop_control: loop_var: project +- name: Include groups.yml + ansible.builtin.include_tasks: groups.yml + with_items: "{{ os_projects_groups }}" + loop_control: + loop_var: group + +- name: Include role_assignment.yml + ansible.builtin.include_tasks: role_assignment.yml + with_items: "{{ os_projects }}" + when: project.users is defined or project.groups is defined + loop_control: + loop_var: project + - name: Ensure SSH keypairs are registered openstack.cloud.keypair: auth_type: "{{ os_projects_auth_type }}" diff --git a/roles/os_projects/tasks/role_assignment.yml b/roles/os_projects/tasks/role_assignment.yml new file mode 100644 index 0000000..ec9b532 --- /dev/null +++ b/roles/os_projects/tasks/role_assignment.yml @@ -0,0 +1,65 @@ +--- +- name: Ensure users have required project roles + openstack.cloud.role_assignment: + auth_type: "{{ os_projects_auth_type }}" + auth: "{{ os_projects_admin_auth }}" + region_name: "{{ os_projects_region | default(omit) }}" + cacert: "{{ os_projects_cacert | default(omit) }}" + cloud: "{{ os_projects_cloud | default(omit) }}" + interface: "{{ os_projects_interface | default(omit, true) }}" + user: "{{ item.0.name }}" + project: "{{ project.name }}" + role: "{{ item.1 }}" + user_domain: "{{ project.user_domain | default(omit, true) }}" + project_domain: "{{ project.project_domain | default(omit, true) }}" + state: present + with_subelements: + - "{{ project.users }}" + - roles + - skip_missing: true + environment: "{{ os_projects_environment }}" + loop_control: + label: "{{ project.name }}: {{ item.0.name }}: {{ item.1 }}" + +- name: Ensure users have required domain roles + openstack.cloud.role_assignment: + auth_type: "{{ os_projects_auth_type }}" + auth: "{{ os_projects_admin_auth }}" + region_name: "{{ os_projects_region | default(omit) }}" + cacert: "{{ os_projects_cacert | default(omit) }}" + cloud: "{{ os_projects_cloud | default(omit) }}" + interface: "{{ os_projects_interface | default(omit, true) }}" + user: "{{ item.0.name }}" + domain: "{{ domain_is_id | ternary(project.user_domain, os_projects_domain_to_id[project.user_domain]) }}" + role: "{{ item.1 }}" + state: present + with_subelements: + - "{{ project.users }}" + - domain_roles + - skip_missing: true + environment: "{{ os_projects_environment }}" + vars: + domain_is_id: "{{ project.user_domain in os_projects_domain_to_id.values() }}" + loop_control: + label: "{{ project.user_domain }}: {{ item.0.name }}: {{ item.1 }}" + +- name: Ensure groups have required project roles + openstack.cloud.role_assignment: + auth_type: "{{ os_projects_auth_type }}" + auth: "{{ os_projects_admin_auth }}" + region_name: "{{ os_projects_region | default(omit) }}" + cacert: "{{ os_projects_cacert | default(omit) }}" + cloud: "{{ os_projects_cloud | default(omit) }}" + interface: "{{ os_projects_interface | default(omit, true) }}" + group: "{{ item.0.name }}" + project: "{{ project.name }}" + role: "{{ item.1 }}" + project_domain: "{{ project.project_domain | default(omit, true) }}" + state: present + with_subelements: + - "{{ project.groups }}" + - roles + - skip_missing: true + environment: "{{ os_projects_environment }}" + loop_control: + label: "{{ project.name }}: {{ item.0.name }}: {{ item.1 }}" diff --git a/roles/os_projects/tasks/users.yml b/roles/os_projects/tasks/users.yml index ecb219d..81b9d1c 100644 --- a/roles/os_projects/tasks/users.yml +++ b/roles/os_projects/tasks/users.yml @@ -27,47 +27,3 @@ project_domain_id: "{{ (project.project_domain in os_projects_domain_to_id.values()) | ternary(project.project_domain, os_projects_domain_to_id[project.project_domain]) }}" loop_control: label: "{{ project.name }}: {{ item.name }}" - -- name: Ensure users have required project roles - openstack.cloud.role_assignment: - auth_type: "{{ os_projects_auth_type }}" - auth: "{{ os_projects_admin_auth }}" - region_name: "{{ os_projects_region | default(omit) }}" - cacert: "{{ os_projects_cacert | default(omit) }}" - cloud: "{{ os_projects_cloud | default(omit) }}" - interface: "{{ os_projects_interface | default(omit, true) }}" - user: "{{ item.0.name }}" - project: "{{ project.name }}" - role: "{{ item.1 }}" - user_domain: "{{ project.user_domain | default(omit, true) }}" - project_domain: "{{ project.project_domain | default(omit, true) }}" - state: present - with_subelements: - - "{{ project.users }}" - - roles - - skip_missing: true - environment: "{{ os_projects_environment }}" - loop_control: - label: "{{ project.name }}: {{ item.0.name }}: {{ item.1 }}" - -- name: Ensure users have required domain roles - openstack.cloud.role_assignment: - auth_type: "{{ os_projects_auth_type }}" - auth: "{{ os_projects_admin_auth }}" - region_name: "{{ os_projects_region | default(omit) }}" - cacert: "{{ os_projects_cacert | default(omit) }}" - cloud: "{{ os_projects_cloud | default(omit) }}" - interface: "{{ os_projects_interface | default(omit, true) }}" - user: "{{ item.0.name }}" - domain: "{{ domain_is_id | ternary(project.user_domain, os_projects_domain_to_id[project.user_domain]) }}" - role: "{{ item.1 }}" - state: present - with_subelements: - - "{{ project.users }}" - - domain_roles - - skip_missing: true - environment: "{{ os_projects_environment }}" - vars: - domain_is_id: "{{ project.user_domain in os_projects_domain_to_id.values() }}" - loop_control: - label: "{{ project.user_domain }}: {{ item.0.name }}: {{ item.1 }}" From 6223dffb776a09e515070e09160ed30d1adf1276 Mon Sep 17 00:00:00 2001 From: Seunghun Lee Date: Thu, 6 Aug 2026 17:01:04 +0100 Subject: [PATCH 2/2] Decouple user definition with projects --- roles/os_projects/defaults/main.yml | 1 - roles/os_projects/tasks/groups.yml | 27 +-------------------------- roles/os_projects/tasks/projects.yml | 5 ++--- roles/os_projects/tasks/users.yml | 22 ++++++++-------------- 4 files changed, 11 insertions(+), 44 deletions(-) diff --git a/roles/os_projects/defaults/main.yml b/roles/os_projects/defaults/main.yml index 125c4c6..c27a6ac 100644 --- a/roles/os_projects/defaults/main.yml +++ b/roles/os_projects/defaults/main.yml @@ -28,7 +28,6 @@ os_projects_domains: [] # - 'users': Optional list of users to add to the group. Each user should be a # dict containing following items: # - 'name': The name of the user. -# - 'domain': The domain where user is registered. os_projects_groups: [] # List of OpenStack users to create. # Each item should be a dict containing the following items: diff --git a/roles/os_projects/tasks/groups.yml b/roles/os_projects/tasks/groups.yml index 30866a1..0557109 100644 --- a/roles/os_projects/tasks/groups.yml +++ b/roles/os_projects/tasks/groups.yml @@ -16,30 +16,8 @@ environment: "{{ os_projects_environment }}" vars: group_domain_id: "{{ (group.domain in os_projects_domain_to_id.values()) | ternary(group.domain, os_projects_domain_to_id[group.domain]) }}" - -- name: Ensure group users exist - openstack.cloud.identity_user: - auth_type: "{{ os_projects_auth_type }}" - auth: "{{ os_projects_admin_auth }}" - region_name: "{{ os_projects_region | default(omit) }}" - cacert: "{{ os_projects_cacert | default(omit) }}" - cloud: "{{ os_projects_cloud | default(omit) }}" - interface: "{{ os_projects_interface | default(omit, true) }}" - name: "{{ item.name }}" - description: "{{ item.description | default(omit) }}" - email: "{{ item.email | default(omit) }}" - password: "{{ item.password | default(omit) }}" - domain: "{{ user_domain_id }}" - state: present - enabled: true - wait: true - with_items: "{{ group.users }}" - when: item.create_user | default(true) | bool - environment: "{{ os_projects_environment }}" - vars: - user_domain_id: "{{ (item.domain in os_projects_domain_to_id.values()) | ternary(item.domain, os_projects_domain_to_id[item.domain]) }}" loop_control: - label: "{{ group.name }}: {{ item.name }}" + label: "{{ group.name }}" - name: Ensure users are added to the group openstack.cloud.group_assignment: @@ -54,9 +32,6 @@ state: present wait: true with_items: "{{ group.users }}" - when: item.create_user | default(true) | bool environment: "{{ os_projects_environment }}" - vars: - user_domain_id: "{{ (item.domain in os_projects_domain_to_id.values()) | ternary(item.domain, os_projects_domain_to_id[item.domain]) }}" loop_control: label: "{{ group.name }}: {{ item.name }}" diff --git a/roles/os_projects/tasks/projects.yml b/roles/os_projects/tasks/projects.yml index f4ebd5b..9c38698 100644 --- a/roles/os_projects/tasks/projects.yml +++ b/roles/os_projects/tasks/projects.yml @@ -139,10 +139,9 @@ - name: Include users.yml ansible.builtin.include_tasks: users.yml - with_items: "{{ os_projects }}" - when: project.users is defined + with_items: "{{ os_projects_users }}" loop_control: - loop_var: project + loop_var: user - name: Include groups.yml ansible.builtin.include_tasks: groups.yml diff --git a/roles/os_projects/tasks/users.yml b/roles/os_projects/tasks/users.yml index 81b9d1c..2056d00 100644 --- a/roles/os_projects/tasks/users.yml +++ b/roles/os_projects/tasks/users.yml @@ -1,5 +1,5 @@ --- -- name: Ensure project users exist +- name: Ensure users exist openstack.cloud.identity_user: auth_type: "{{ os_projects_auth_type }}" auth: "{{ os_projects_admin_auth }}" @@ -7,23 +7,17 @@ cacert: "{{ os_projects_cacert | default(omit) }}" cloud: "{{ os_projects_cloud | default(omit) }}" interface: "{{ os_projects_interface | default(omit, true) }}" - name: "{{ item.name }}" - description: "{{ item.description | default(omit) }}" - email: "{{ item.email | default(omit) }}" - password: "{{ item.password | default(omit) }}" - # NOTE(bbezak): identity_user looks up default_project in the user domain - # ('domain'). Omit default_project for cross-domain users to avoid false - # "No Project found" errors. - default_project: "{{ (user_domain_id == project_domain_id) | ternary(project.name, omit) }}" + name: "{{ user.name }}" + description: "{{ user.description | default(omit) }}" + email: "{{ user.email | default(omit) }}" + password: "{{ user.password | default(omit) }}" domain: "{{ user_domain_id }}" state: present enabled: true wait: true - with_items: "{{ project.users }}" - when: item.create_user | default(true) | bool + when: user.create_user | default(true) | bool environment: "{{ os_projects_environment }}" vars: - user_domain_id: "{{ (project.user_domain in os_projects_domain_to_id.values()) | ternary(project.user_domain, os_projects_domain_to_id[project.user_domain]) }}" - project_domain_id: "{{ (project.project_domain in os_projects_domain_to_id.values()) | ternary(project.project_domain, os_projects_domain_to_id[project.project_domain]) }}" + user_domain_id: "{{ (user.domain in os_projects_domain_to_id.values()) | ternary(user.domain, os_projects_domain_to_id[user.domain]) }}" loop_control: - label: "{{ project.name }}: {{ item.name }}" + label: "{{ user.name }}"