diff --git a/roles/os_projects/README.md b/roles/os_projects/README.md index c182c8d..7ac7a5c 100644 --- a/roles/os_projects/README.md +++ b/roles/os_projects/README.md @@ -55,6 +55,25 @@ Each item should be a dict containing the following items: - `create_user`: Boolean to indicate whether or not to create the user. Can be useful if the user already exists e.g the user is defined in LDAP. (optional) +- `groups`: Optional list of groups to register. Each group should be a dict + containing the following items: + - `name`: The name of the group. + - `description`: Optional description for the group. + - `domain`: The domain of the group. + - `roles`: Optional list of roles to assign to the group in the project. + - `domain_roles`: Optional list of roles to assign to the group in the + group domain. + - `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. + - `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) + - `create_group`: Optional boolean (Default: true) to indicate whether to + create the group. This can be useful if the group already exists. + (e.g. LDAP defined groups) - `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. @@ -106,6 +125,14 @@ resources. roles: - admin openrc_file: /home/user/user1.openrc + groups: + - name: group_a + domain: default + roles: + - member + users: + - name: user2 + password: ashpineapplevictorcommand keypairs: - name: keypair1 public_key_file: /path/to/key @@ -116,3 +143,4 @@ Author Information ------------------ - Mark Goddard () +- Seunghun Lee () diff --git a/roles/os_projects/defaults/main.yml b/roles/os_projects/defaults/main.yml index 59a6a2b..b6e4130 100644 --- a/roles/os_projects/defaults/main.yml +++ b/roles/os_projects/defaults/main.yml @@ -31,6 +31,28 @@ os_projects_domains: [] # - '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. +# - '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) +# - 'groups': Optional list of groups to register. Each group should be a dict +# containing the following items: +# - 'name': The name of the group. +# - 'description': Optional description for the group. +# - 'domain': The domain of the group. +# - 'roles': Optional list of roles to assign to the group in the project. +# - 'domain_roles': Optional list of roles to assign to the group in the +# group domain. +# - '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. +# - '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) +# - 'create_group': Optional boolean (Default: true) to indicate whether to +# create the group. This can be useful if the group already exists. +# (e.g. LDAP defined groups) # - '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/group-user-openrc.yml b/roles/os_projects/tasks/group-user-openrc.yml new file mode 100644 index 0000000..c9ba52a --- /dev/null +++ b/roles/os_projects/tasks/group-user-openrc.yml @@ -0,0 +1,16 @@ +--- +- name: Ensure openrc environment file exists for group users + ansible.builtin.template: + src: group-openrc.j2 + dest: "{{ item.1.openrc_file }}" + mode: "0600" + with_subelements: + - "{{ project.groups }}" + - users + - skip_missing: true + when: + - project.groups is defined + - item.1.openrc_file is defined + loop_control: + label: "{{ project.name }}: {{ item.0.name }}: {{ item.1.name }}" + delegate_to: localhost diff --git a/roles/os_projects/tasks/group-users.yml b/roles/os_projects/tasks/group-users.yml new file mode 100644 index 0000000..5292ade --- /dev/null +++ b/roles/os_projects/tasks/group-users.yml @@ -0,0 +1,103 @@ +--- +- 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.1.name }}" + description: "{{ item.1.description | default(omit) }}" + email: "{{ item.1.email | default(omit) }}" + password: "{{ item.1.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) }}" + domain: "{{ user_domain_id }}" + state: present + enabled: true + wait: true + with_subelements: + - "{{ project.groups }}" + - users + - skip_missing: true + when: + - project.groups is defined + - item.1.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]) }}" + loop_control: + label: "{{ item.0.name }}: {{ item.1.name }}" + +# This task is to ensure we don't miss users that have ``create_user`` set to false +- name: Look up users + openstack.cloud.identity_user_info: + 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) }}" + domain: "{{ user_domain_id }}" + name: "{{ item.1.name }}" + with_subelements: + - "{{ project.groups }}" + - users + - skip_missing: true + when: + - project.groups is defined + 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]) }}" + loop_control: + label: "{{ item.0.name }}: {{ item.1.name }}" + index_var: idx + register: os_projects_fetched_users + +- name: Assert that all users exist + ansible.builtin.assert: + quiet: true + that: + - os_projects_fetched_users.results[idx].users is defined + - os_projects_fetched_users.results[idx].users | length > 0 + fail_msg: "The user '{{ item.1.name }}' does not exist in the '{{ project.user_domain }}' domain" + with_subelements: + - "{{ project.groups }}" + - users + - skip_missing: true + when: + - project.groups is defined + loop_control: + label: "{{ item.0.name }}: {{ item.1.name }}" + index_var: idx + +- 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: "{{ user_id }}" + group: "{{ group_id }}" + state: present + wait: true + with_subelements: + - "{{ project.groups }}" + - users + - skip_missing: true + when: project.groups is defined + environment: "{{ os_projects_environment }}" + vars: + user_id: "{{ os_projects_fetched_users.results[idx].users[0].id }}" + group_domain_id: "{{ (item.0.domain in os_projects_domain_to_id.values()) | ternary(item.0.domain, os_projects_domain_to_id[item.0.domain]) }}" + group_id: "{{ os_projects_group_to_id[group_domain_id][item.0.name] }}" + loop_control: + label: "{{ item.0.name }}: {{ item.1.name }}" + index_var: idx diff --git a/roles/os_projects/tasks/groups.yml b/roles/os_projects/tasks/groups.yml new file mode 100644 index 0000000..7601b70 --- /dev/null +++ b/roles/os_projects/tasks/groups.yml @@ -0,0 +1,108 @@ +--- +- 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: "{{ item.name }}" + description: "{{ item.description | default(omit) }}" + domain_id: "{{ group_domain_id }}" + state: present + wait: true + with_items: "{{ project.groups }}" + when: item.create_group | default(true) | bool + environment: "{{ os_projects_environment }}" + vars: + group_domain_id: "{{ (item.domain in os_projects_domain_to_id.values()) | ternary(item.domain, os_projects_domain_to_id[item.domain]) }}" + loop_control: + label: "{{ project.name }}: {{ item.name }}" + +# This task is to ensure we don't miss groups that have ``create_group`` set to false +- name: Look up groups + openstack.cloud.identity_group_info: + 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 }}" + domain: "{{ group_domain_id }}" + with_items: "{{ project.groups }}" + environment: "{{ os_projects_environment }}" + vars: + group_domain_id: "{{ (item.domain in os_projects_domain_to_id.values()) | ternary(item.domain, os_projects_domain_to_id[item.domain]) }}" + loop_control: + label: "{{ project.name }}: {{ item.name }}" + index_var: idx + register: os_projects_fetched_groups + +- name: Assert that all groups exist + ansible.builtin.assert: + quiet: true + that: + - os_projects_fetched_groups.results[idx].groups is defined + - os_projects_fetched_groups.results[idx].groups | length > 0 + fail_msg: "The group '{{ item.name }}' does not exist in the '{{ item.domain }}' domain" + with_items: "{{ project.groups }}" + loop_control: + label: "{{ project.name }}: {{ item.name }}" + index_var: idx + +- name: Populate domain ID - group name - group ID mapping + ansible.builtin.set_fact: + os_projects_group_to_id: "{{ os_projects_group_to_id | combine(sub_map, recursive=true) }}" + with_items: "{{ project.groups }}" + vars: + group_domain_id: "{{ (item.domain in os_projects_domain_to_id.values()) | ternary(item.domain, os_projects_domain_to_id[item.domain]) }}" + sub_map: "{{ {group_domain_id: {item.name: os_projects_fetched_groups.results[idx].groups[0].id}} }}" + loop_control: + index_var: idx + label: "Domain: {{ item.domain }} - Group: {{ item.name }}" + +- 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 }}" + group_domain: "{{ item.0.domain | default(omit, true) }}" + 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 }}" + +- name: Ensure groups 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) }}" + group: "{{ item.0.name }}" + domain: "{{ domain_is_id | ternary(item.0.domain, os_projects_domain_to_id[item.0.domain]) }}" + role: "{{ item.1 }}" + state: present + with_subelements: + - "{{ project.groups }}" + - domain_roles + - skip_missing: true + environment: "{{ os_projects_environment }}" + vars: + domain_is_id: "{{ item.0.domain in os_projects_domain_to_id.values() }}" + loop_control: + label: "{{ item.0.domain }}: {{ item.0.name }}: {{ item.1 }}" diff --git a/roles/os_projects/tasks/projects.yml b/roles/os_projects/tasks/projects.yml index 54c6fa4..012dbac 100644 --- a/roles/os_projects/tasks/projects.yml +++ b/roles/os_projects/tasks/projects.yml @@ -69,6 +69,21 @@ loop_control: label: "{{ item.name }}" +- name: Fail if the group's domain was not found + ansible.builtin.fail: + msg: > + OpenStack domain {{ item.1.domain }} for group {{ item.1.name }} + was not found. + when: + - item.1.domain not in os_projects_domain_to_id + - item.1.domain not in os_projects_domain_to_id.values() + with_subelements: + - "{{ os_projects }}" + - groups + - skip_missing: true + loop_control: + label: "{{ item.1.name }}" + - name: Fail if the project's user domain was not found ansible.builtin.fail: msg: > @@ -121,6 +136,12 @@ map(attribute='users') | sum(start=[]) | list }} + os_groups: > + {{ os_projects | + selectattr('groups', 'defined') | + map(attribute='groups') | + sum(start=[]) | + list }} project_roles: > {{ users | selectattr('roles', 'defined') | @@ -135,7 +156,33 @@ sum(start=[]) | unique | list }} - all_roles: "{{ project_roles + domain_roles }}" + group_project_roles: > + {{ os_groups | + selectattr('roles', 'defined') | + map(attribute='roles') | + sum(start=[]) | + unique | + list }} + group_domain_roles: > + {{ os_groups | + selectattr('domain_roles', 'defined') | + map(attribute='domain_roles') | + sum(start=[]) | + unique | + list }} + all_roles: "{{ project_roles + domain_roles + group_project_roles + group_domain_roles }}" + +- name: Initialise a fact mapping group names to IDs + ansible.builtin.set_fact: + os_projects_group_to_id: {} + +- name: Include groups.yml + ansible.builtin.include_tasks: groups.yml + with_items: "{{ os_projects }}" + when: project.groups is defined + loop_control: + loop_var: project + label: "{{ project.name }}" - name: Include users.yml ansible.builtin.include_tasks: users.yml @@ -143,6 +190,15 @@ when: project.users is defined loop_control: loop_var: project + label: "{{ project.name }}" + +- name: Include group-users.yml + ansible.builtin.include_tasks: group-users.yml + with_items: "{{ os_projects }}" + when: project.groups is defined + loop_control: + loop_var: project + label: "{{ project.name }}" - name: Ensure SSH keypairs are registered openstack.cloud.keypair: @@ -236,3 +292,10 @@ loop_control: label: "{{ item.0.name }}: {{ item.1.name }}" delegate_to: localhost + +- name: Ensure openrc environment file exists for group users + ansible.builtin.include_tasks: group-user-openrc.yml + with_items: "{{ os_projects }}" + loop_control: + loop_var: project + label: "{{ project.name }}" diff --git a/roles/os_projects/templates/group-openrc.j2 b/roles/os_projects/templates/group-openrc.j2 new file mode 100644 index 0000000..0198e84 --- /dev/null +++ b/roles/os_projects/templates/group-openrc.j2 @@ -0,0 +1,17 @@ +# {{ ansible_managed }} +# This is an openrc environment file for OpenStack user {{ item.1.name }} in +# project {{ project.name }}. +{% set os_projects_user_auth_overrides = { + "project_domain_name": project.project_domain, + "user_domain_name": project.user_domain, + "project_name": project.name, + "username": item.1.name, + "password": item.1.password, +} %} +{% set os_projects_auth = os_projects_admin_auth | combine(os_projects_user_auth_overrides) %} +{% for name, value in os_projects_auth.items() %} +export OS_{{ name | upper }}={{ value | quote }} +{% endfor %} +{% for name, value in os_projects_environment.items() %} +export {{ name }}={{ value | quote }} +{% endfor %} diff --git a/roles/os_projects/templates/openrc.j2 b/roles/os_projects/templates/openrc.j2 index 612b0d5..2415ccf 100644 --- a/roles/os_projects/templates/openrc.j2 +++ b/roles/os_projects/templates/openrc.j2 @@ -10,8 +10,8 @@ } %} {% set os_projects_auth = os_projects_admin_auth | combine(os_projects_user_auth_overrides) %} {% for name, value in os_projects_auth.items() %} -export OS_{{ name | upper }}={{ value }} +export OS_{{ name | upper }}={{ value | quote }} {% endfor %} {% for name, value in os_projects_environment.items() %} -export {{ name }}={{ value }} +export {{ name }}={{ value | quote }} {% endfor %}