diff --git a/ansible/adhoc/backup-keytabs.yml b/ansible/adhoc/backup-keytabs.yml new file mode 100644 index 000000000..5566e48ac --- /dev/null +++ b/ansible/adhoc/backup-keytabs.yml @@ -0,0 +1,11 @@ +# Use ONE of the following tags on this playbook: +# - retrieve: copies keytabs out of the state volume to the environment +# - deploy: copies keytabs from the environment to the state volume + +- hosts: freeipa_client + become: yes + gather_facts: no + tasks: + - import_role: + name: freeipa + tasks_from: backup-keytabs.yml diff --git a/ansible/bootstrap.yml b/ansible/bootstrap.yml index 485c637f5..e3baf1e51 100644 --- a/ansible/bootstrap.yml +++ b/ansible/bootstrap.yml @@ -82,6 +82,21 @@ policy: "{{ selinux_policy }}" register: sestatus +- hosts: freeipa_server + # Done here as it might be providing DNS + tags: + - freeipa + - freeipa_server + gather_facts: yes + become: yes + tasks: + - name: Install FreeIPA server + import_role: + name: freeipa + tasks_from: server.yml + +# --- tasks after here require access to package repos --- + - hosts: firewalld gather_facts: false become: yes diff --git a/ansible/extras.yml b/ansible/extras.yml index efb39f40f..0a27d1806 100644 --- a/ansible/extras.yml +++ b/ansible/extras.yml @@ -1,3 +1,13 @@ +- hosts: basic_users + become: yes + tags: + - basic_users + - users + gather_facts: yes + tasks: + - import_role: + name: basic_users + - name: Setup EESSI hosts: eessi tags: eessi diff --git a/ansible/fatimage.yml b/ansible/fatimage.yml index 511bc3b82..ff27e34f3 100644 --- a/ansible/fatimage.yml +++ b/ansible/fatimage.yml @@ -34,6 +34,13 @@ state: stopped enabled: false + # - import_playbook: iam.yml + - name: Install FreeIPA client + import_role: + name: freeipa + tasks_from: client-install.yml + when: "'freeipa_client' in group_names" + # - import_playbook: filesystems.yml - name: nfs dnf: @@ -149,8 +156,6 @@ name: cloudalchemy.grafana tasks_from: install.yml - # - import_playbook: iam.yml - nothing to do - - name: Run post.yml hook vars: appliances_environment_root: "{{ lookup('env', 'APPLIANCES_ENVIRONMENT_ROOT') }}" diff --git a/ansible/iam.yml b/ansible/iam.yml index 266bca1ab..0286b9df3 100644 --- a/ansible/iam.yml +++ b/ansible/iam.yml @@ -1,8 +1,42 @@ -- hosts: basic_users +- hosts: freeipa_client + tags: + - freeipa + - freeipa_server # as this is only relevant if using freeipa_server + - freeipa_host + gather_facts: no become: yes + tasks: + - name: Ensure FreeIPA client hosts are added to the FreeIPA server + import_role: + name: freeipa + tasks_from: addhost.yml + when: groups['freeipa_server'] | length > 0 + +- hosts: freeipa_client tags: - - basic_users + - freeipa + - freeipa_client gather_facts: yes + become: yes + tasks: + - name: Install FreeIPA client + import_role: + name: freeipa + tasks_from: client-install.yml + - name: Enrol FreeIPA client + import_role: + name: freeipa + tasks_from: enrol.yml + +- hosts: freeipa_server + tags: + - freeipa + - freeipa_server + - users + gather_facts: yes + become: yes tasks: - - import_role: - name: basic_users + - name: Add FreeIPA users + import_role: + name: freeipa + tasks_from: users.yml diff --git a/ansible/roles/etc_hosts/README.md b/ansible/roles/etc_hosts/README.md index a06e2df80..0ad95681a 100644 --- a/ansible/roles/etc_hosts/README.md +++ b/ansible/roles/etc_hosts/README.md @@ -2,10 +2,13 @@ Hosts in the `etc_hosts` groups have `/etc/hosts` created with entries of the format `IP_address canonical_hostname [alias]`. -By default, an entry is created for each host in this group, using `ansible_host` as the IP_address and `inventory_hostname` as the canonical hostname. This may need overriding for multi-homed hosts or hosts with multiple aliases. +By default, an entry is created for each host in this group as follows: +- The value of `ansible_host` is used as the IP_address. +- If `node_fqdn` is defined then that is used as the canonical hostname and `inventory_hostname` as an alias. Otherwise `inventory_hostname` is used as the canonical hostname. +This may need overriding for multi-homed hosts or hosts with multiple aliases. # Variables - `etc_hosts_template`: Template file to use. Default is the in-role template. -- `etc_hosts_hostvars`: A list of variable names, used (in the order supplied) to create the entry for each host. Default is `['ansible_host', 'inventory_hostname']` +- `etc_hosts_hostvars`: A list of variable names, used (in the order supplied) to create the entry for each host. Default is described above. - `etc_hosts_extra_hosts`: String (possibly multi-line) defining additional hosts to add to `/etc/hosts`. Default is empty string. diff --git a/ansible/roles/etc_hosts/defaults/main.yml b/ansible/roles/etc_hosts/defaults/main.yml index d5717d1ac..c2ecbca0c 100644 --- a/ansible/roles/etc_hosts/defaults/main.yml +++ b/ansible/roles/etc_hosts/defaults/main.yml @@ -1,5 +1,3 @@ etc_hosts_template: hosts.j2 -etc_hosts_hostvars: - - ansible_host - - inventory_hostname +etc_hosts_hostvars: "{{ ['ansible_host'] + (['node_fqdn'] if node_fqdn is defined else []) + ['inventory_hostname'] }}" etc_hosts_extra_hosts: '' diff --git a/ansible/roles/freeipa/README.md b/ansible/roles/freeipa/README.md new file mode 100644 index 000000000..5f2f377b6 --- /dev/null +++ b/ansible/roles/freeipa/README.md @@ -0,0 +1,68 @@ + +# freeipa + +Support FreeIPA in the appliance. In production use it is expected the FreeIPA server(s) will be external to the cluster, implying that hosts and users are managed outside the appliance. However for testing and development the role can also deploy an "in-appliance" FreeIPA server, add hosts to it and manage users in FreeIPA. + +# FreeIPA Client + +## Usage +- Add hosts to the `freeipa_client` group and run (at a minimum) the `ansible/iam.yml` playbook. +- Host names must match the domain name. By default (using the skeleton Terraform) hostnames are of the form `nodename.cluster_name.cluster_domain_suffix` where `cluster_name` and `cluster_domain_suffix` are Terraform variables. +- Hosts discover the FreeIPA server FQDN (and their own domain) from DNS records. If DNS servers are not set this is not set from DHCP, then use the `resolv_conf` role to configure this. For example when using the in-appliance FreeIPA development server: + + ```ini + # environments//groups + ... + [resolv_conf:children] + freeipa_client + ... + ``` + + ```yaml + # environments//inventory/group_vars/all/resolv_conf.yml + resolv_conf_nameservers: + - "{{ hostvars[groups['freeipa_server'] | first].ansible_host }}" + ``` + + +- For production use with an external FreeIPA server, a random one-time password (OTP) must be generated when adding hosts to FreeIPA (e.g. using `ipa host-add --random ...`). This password should be set as a hostvar `freeipa_host_password`. Initial host enrolment will use this OTP to enrol the host. After this it becomes irrelevant so it does not need to be committed to git. This approach means the appliance does not require the FreeIPA administrator password. +- For development use with the in-appliance FreeIPA server, `freeipa_host_password` will be automatically generated in memory. +- The `control` host must define `appliances_state_dir` (on persistent storage). This is used to back-up keytabs to allow FreeIPA clients to automatically re-enrol after e.g. reimaging. Note that: + - This is implemented when using the skeleton Terraform; on the control node `appliances_state_dir` defaults to `/var/lib/state` which is mounted from a volume. + - Nodes are not re-enroled by a [Slurm-driven reimage](../../collections/ansible_collections/stackhpc/slurm_openstack_tools/roles/rebuild/README.md) (as that does not run this role). + - If both a backed-up keytab and `freeipa_host_password` exist, the former is used. + + +## Role Variables for Clients + +- `freeipa_host_password`. Required for initial enrolment only, FreeIPA host password as described above. +- `freeipa_setup_dns`: Optional, whether to use the FreeIPA server as the client's nameserver. Defaults to `true` when `freeipa_server` contains a host, otherwise `false`. + +See also use of `appliances_state_dir` on the control node as described above. + +# FreeIPA Server +As noted above this is only intended for development and testing. Note it cannot be run on the `openondemand` node as no other virtual servers must be defined in the Apache configuration. + +## Usage +- Add a single host to the `freeipa_server` group and run (at a minimum) the `ansible/bootstrap.yml` and `ansible/iam.yml` playbooks. +- As well as configuring the FreeIPA server, the role will also: + - Add ansible hosts in the group `freeipa_client` as FreeIPA hosts. + - Optionally control users in FreeIPA - see `freeipa_users` below. + +The FreeIPA GUI will be available on `https:///ipa/ui`. + +## Role Variables for Server + +These role variables are only required when using `freeipa_server`: + +- `freeipa_realm`: Optional, name of realm. Default is `{{ openhpc_cluster_name | upper }}.INVALID` +- `freeipa_domain`: Optional, name of domain. Default is lowercased `freeipa_realm`. +- `freeipa_ds_password`: Optional, password to be used by the Directory Server for the Directory Manager user (`ipa-server-install --ds-password`). Default is generated in `environments//inventory/group_vars/all/secrets.yml` +- `freeipa_admin_password`: Optional, password for the IPA `admin` user. Default is generated as for `freeipa_ds_password`. +- `freeipa_server_ip`: Optional, IP address of freeipa_server host. Default is `ansible_host` of the `freeipa_server` host. Default `false`. +- `freeipa_setup_dns`: Optional bool, whether to configure the FreeIPA server as an integrated DNS server and define a zone and records. NB: This also controls whether `freeipa_client` hosts use the `freeipa_server` host for name resolution. Default `true` when `freeipa_server` contains a host. +- `freeipa_client_ip`: Optional, IP address of FreeIPA client. Default is `ansible_host`. +- `freeipa_users`: A list of dicts defining users to add, with keys/values as for [community.general.ipa_user](https://docs.ansible.com/ansible/latest/collections/community/general/ipa_user_module.html): Note that: + - `name`, `givenname` (firstname) and `sn` (surname) are required. + - `ipa_host`, `ipa_port`, `ipa_prot`, `ipa_user`, `validate_certs` are automatically provided and cannot be overridden. + - If `password` is set, the value should *not* be a hash (unlike `ansible.builtin.user` as used by the `basic_users` role), and it must be changed on first login. `krbpasswordexpiration` does not appear to be able to override this. diff --git a/ansible/roles/freeipa/defaults/main.yml b/ansible/roles/freeipa/defaults/main.yml new file mode 100644 index 000000000..03b844c8a --- /dev/null +++ b/ansible/roles/freeipa/defaults/main.yml @@ -0,0 +1,14 @@ +#freeipa_realm: +freeipa_domain: "{{ freeipa_realm | lower }}" +#freeipa_ds_password: +#freeipa_admin_password: +#freeipa_server_ip: +freeipa_setup_dns: "{{ groups['freeipa_server'] | length > 0 }}" +freeipa_client_ip: "{{ ansible_host }}" # when run on freeipa_client group! +# freeipa_host_password: +freeipa_user_defaults: + ipa_pass: "{{ freeipa_admin_password | quote }}" + ipa_user: admin +freeipa_users: [] # see community.general.ipa_user + +_freeipa_keytab_backup_path: "{{ hostvars[groups['control'].0].appliances_state_dir }}/freeipa/{{ inventory_hostname }}/krb5.keytab" diff --git a/ansible/roles/freeipa/tasks/addhost.yml b/ansible/roles/freeipa/tasks/addhost.yml new file mode 100644 index 000000000..cf1f4475a --- /dev/null +++ b/ansible/roles/freeipa/tasks/addhost.yml @@ -0,0 +1,35 @@ +- name: Get ipa host information + # This uses DNS to find the ipa server, which works as this is running on the enrolled ipa server + # It doesn't fail even if the host doesn't exist + community.general.ipa_host: + name: "{{ node_fqdn }}" + ip_address: "{{ freeipa_client_ip }}" + ipa_host: "{{ groups['freeipa_server'].0 }}" + ipa_pass: "{{ vault_freeipa_admin_password }}" + ipa_user: admin + state: present + validate_certs: false + delegate_to: "{{ groups['freeipa_server'].0 }}" + register: _ipa_host_check + check_mode: yes + changed_when: false + +- name: Add host to IPA + # Using random_password=true this unenroles an enroled host, hence the check above + community.general.ipa_host: + name: "{{ node_fqdn }}" + ip_address: "{{ freeipa_client_ip }}" + ipa_host: "{{ groups['freeipa_server'].0 }}" + ipa_pass: "{{ vault_freeipa_admin_password }}" + ipa_user: admin + random_password: true + state: present + validate_certs: false + delegate_to: "{{ groups['freeipa_server'].0 }}" + when: "'sshpubkeyfp' not in _ipa_host_check.host" + register: _ipa_host_add + +- name: Set fact for ipa host password + set_fact: + freeipa_host_password: "{{ _ipa_host_add.host.randompassword }}" + when: _ipa_host_add.changed diff --git a/ansible/roles/freeipa/tasks/backup-keytabs.yml b/ansible/roles/freeipa/tasks/backup-keytabs.yml new file mode 100644 index 000000000..7fc77f9e1 --- /dev/null +++ b/ansible/roles/freeipa/tasks/backup-keytabs.yml @@ -0,0 +1,14 @@ +- name: Retrieve keytabs to localhost + fetch: + src: "{{ _freeipa_keytab_backup_path }}" + dest: "{{ appliances_environment_root }}/keytabs/{{ inventory_hostname }}/" + flat: true + delegate_to: "{{ groups['control'].0 }}" + tags: retrieve + +- name: Copy keytabs back to control node + copy: + src: "{{ appliances_environment_root }}/keytabs/{{ inventory_hostname }}/" + dest: "{{ _freeipa_keytab_backup_path | dirname }}" + delegate_to: "{{ groups['control'].0 }}" + tags: deploy diff --git a/ansible/roles/freeipa/tasks/client-install.yml b/ansible/roles/freeipa/tasks/client-install.yml new file mode 100644 index 000000000..a164cd26e --- /dev/null +++ b/ansible/roles/freeipa/tasks/client-install.yml @@ -0,0 +1,4 @@ + +- name: Install FreeIPA client package + dnf: + name: ipa-client diff --git a/ansible/roles/freeipa/tasks/enrol.yml b/ansible/roles/freeipa/tasks/enrol.yml new file mode 100644 index 000000000..07436509b --- /dev/null +++ b/ansible/roles/freeipa/tasks/enrol.yml @@ -0,0 +1,87 @@ +# based on https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_identity_management/assembly_installing-an-idm-client_installing-identity-management + +- name: Retrieve persisted keytab from previous enrolement + slurp: + src: "{{ _freeipa_keytab_backup_path }}" + delegate_to: "{{ groups['control'] | first }}" + register: _slurp_persisted_keytab + failed_when: false + +- name: Write persisted keytab from previous enrolment + copy: + content: "{{ _slurp_persisted_keytab.content | b64decode }}" + dest: /tmp/krb5.keytab + owner: root + group: root + mode: ug=rw,o= + when: '"content" in _slurp_persisted_keytab' + +- name: Re-enrol with FreeIPA using backed-up keytab + # Re-enrolment requires --force-join and --password, or --keytab + # Re-rolement means: + # 1. A new host certificate is issued + # 2. The old host certificate is revoked + # 3. New SSH keys are generated + # 4. ipaUniqueID is preserved + # and ALSO that the keytab is changed! + command: + cmd: > + ipa-client-install + --unattended + --mkhomedir + --enable-dns-updates + --keytab /tmp/krb5.keytab + when: '"content" in _slurp_persisted_keytab' + register: ipa_client_install_keytab + changed_when: ipa_client_install_keytab.rc == 0 + failed_when: > + ipa_client_install_keytab.rc !=0 and + 'IPA client is already configured' not in ipa_client_install_keytab.stderr + +- name: Enrol with FreeIPA using random password + # Note --password is overloaded - it's bulkpassword unless --principal or --force-join is used in which case it's admin password + command: + cmd: > + ipa-client-install + --unattended + --mkhomedir + --enable-dns-updates + --password '{{ freeipa_host_password }}' + when: + - '"content" not in _slurp_persisted_keytab' + - freeipa_host_password is defined + register: ipa_client_install_password + changed_when: ipa_client_install_password.rc == 0 + failed_when: > + ipa_client_install_password.rc != 0 and + 'IPA client is already configured' not in ipa_client_install_password.stderr + +- name: Ensure NFS RPC security service is running + # This service is installed by nfs-utils, which attempts to start it. + # It has ConditionPathExists=/etc/krb5.keytab which fails if host is not enroled. + # This task avoids a reboot. + systemd: + name: rpc-gssd.service + state: started + enabled: true + +- name: Retrieve current keytab + slurp: + src: /etc/krb5.keytab + register: _slurp_current_keytab + failed_when: false + +- name: Ensure keytab backup directory exists + file: + path: "{{ _freeipa_keytab_backup_path | dirname }}" + state: directory + owner: root + group: root + mode: ug=wrX,o= + delegate_to: "{{ groups['control'] | first }}" + +- name: Persist keytab + copy: + content: "{{ _slurp_current_keytab.content | b64decode }}" + dest: "{{ _freeipa_keytab_backup_path }}" + delegate_to: "{{ groups['control'] | first }}" diff --git a/ansible/roles/freeipa/tasks/server.yml b/ansible/roles/freeipa/tasks/server.yml new file mode 100644 index 000000000..33e15733d --- /dev/null +++ b/ansible/roles/freeipa/tasks/server.yml @@ -0,0 +1,63 @@ +# Based on https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/installing_identity_management/preparing-the-system-for-ipa-server-installation_installing-identity-management#host-name-and-dns-requirements-for-ipa_preparing-the-system-for-ipa-server-installation + +- name: Install freeipa server packages + dnf: + name: '@idm:DL1/dns' + state: present + +- name: Install ipa server +# TODO: make no-ui-redirect and dns configurable?? +# TODO: set file mask as per docs? Would be hard to cope with failures. Doesn't appear to be necessary actually. + command: + cmd: > + ipa-server-install + --realm {{ freeipa_realm | quote }} + --domain {{ freeipa_domain | lower | quote }} + --ds-password {{ freeipa_ds_password | quote }} + --admin-password {{ freeipa_admin_password | quote }} + --ip-address={{ freeipa_server_ip }} + {% if freeipa_setup_dns | bool %}--setup-dns{% endif %} + --auto-reverse + --auto-forwarders + --no-dnssec-validation + --no-ntp + --unattended + --no-ui-redirect + no_log: "{{ no_log | default(true) }}" + register: _ipa_server_install + changed_when: _ipa_server_install.rc == 0 + failed_when: > + (_ipa_server_install.rc != 0) and + ('IPA server is already configured' not in _ipa_server_install.stderr) + +- name: Disable redirects to hard-coded domain + # see https://pagure.io/freeipa/issue/7479 + replace: path=/etc/httpd/conf.d/ipa-rewrite.conf regexp='{{ item.regexp }}' replace='{{ item.replace }}' + with_items: + # RewriteRule ^/$ https://${FQDN}/ipa/ui [L,NC,R=301] - irrelevant if using --no-ui-redirect + - regexp: '^(RewriteRule \^/\$) (https://.*)(/ipa/ui.*)$' + replace: '\1 \3' + # RewriteRule ^/ipa/(.*) - occurs twice + - regexp: '^(RewriteRule \^\/ipa\/\(.*)$' + replace: '#\1' + - regexp: '^(RewriteCond .*)$' + replace: '#\1' + # RewriteRule ^/(.*) https://${FQDN}/$1 [L,R=301] + - regexp: '^(RewriteRule \^/\(\.\*\).*)$' + replace: '#\1' + register: _replace_freeipa_rewrites + +- name: Deactivate HTTP RefererError + replace: + path: '/usr/lib/python3.6/site-packages/ipaserver/rpcserver.py' + regexp: '{{ item }}' + replace: '\1pass # \2' + with_items: + - "^([ ]*)(return self.marshal\\(result, RefererError\\(referer)" + register: _replace_rpcserver_referrer + +- name: Reload apache configuration + service: + name: httpd + state: reloaded + when: _replace_freeipa_rewrites.changed or _replace_rpcserver_referrer.changed diff --git a/ansible/roles/freeipa/tasks/users.yml b/ansible/roles/freeipa/tasks/users.yml new file mode 100644 index 000000000..bd1cacad3 --- /dev/null +++ b/ansible/roles/freeipa/tasks/users.yml @@ -0,0 +1,27 @@ +- name: Add users to freeipa + # This uses DNS to find the ipa server, which works as this is running on the enrolled ipa server + community.general.ipa_user: + displayname: "{{ item.displayname | default(omit) }}" + gidnumber: "{{ item.gidnumber | default(omit) }}" + givenname: "{{ item.givenname }}" + #ipa_host + ipa_pass: "{{ freeipa_admin_password | quote }}" + #ipa_port + #ipa_prot + ipa_timeout: "{{ item.ipa_timeout | default(omit) }}" + #ipa_user + krbpasswordexpiration: "{{ item.krbpasswordexpiration | default(omit) }}" + loginshell: "{{ item.loginshell | default(omit) }}" + mail: "{{ item.mail | default(omit) }}" + password: "{{ item.password | default(omit) }}" + sn: "{{ item.sn }}" + sshpubkey: "{{ item.sshpubkey | default(omit) }}" + state: "{{ item.state | default(omit) }}" + telephonenumber: "{{ item.telephonenumber | default(omit) }}" + title: "{{ item.title | default(omit) }}" + uid: "{{ item.name | default(item.uid) }}" + uidnumber: "{{ item.uidnumber | default(omit) }}" + update_password: "{{ item.update_password | default(omit) }}" + userauthtype: "{{ item.userauthtype | default(omit) }}" + #validate_certs + loop: "{{ freeipa_users }}" diff --git a/ansible/roles/freeipa/tasks/validate.yml b/ansible/roles/freeipa/tasks/validate.yml new file mode 100644 index 000000000..238f89e60 --- /dev/null +++ b/ansible/roles/freeipa/tasks/validate.yml @@ -0,0 +1,36 @@ +- name: Get hostname as reported by command + command: hostname + register: _freeipa_validate_hostname + changed_when: false + when: "'freeipa_server' in group_names" + +- name: Ensure hostname is fully-qualified + # see section 2.7 of redhat guide to installing identity management + assert: + that: _freeipa_validate_hostname.stdout | split('.') | length >= 3 + fail_msg: "freeipa_server hostname '{{ _freeipa_validate_hostname.stdout }}' is not fully-qualified (a.b.c)" + when: "'freeipa_server' in group_names" + +- name: Check for virtual servers in httpd configuration of freeipa_server + # e.g. fatimage with OOD config; community.general.ipa_host fails with "401 Unauthorized: No session cookie found" + # https://lists.fedoraproject.org/archives/list/freeipa-users@lists.fedorahosted.org/message/7RH7XDFR35KDPYJ7AQCQI2H2EOWIZCWA/ + find: + path: /etc/httpd/conf.d/ + contains: '/dev/null | base64') }}" diff --git a/ansible/roles/proxy/tasks/main.yml b/ansible/roles/proxy/tasks/main.yml index 5368d6da2..3bc33cfa2 100644 --- a/ansible/roles/proxy/tasks/main.yml +++ b/ansible/roles/proxy/tasks/main.yml @@ -60,4 +60,4 @@ - name: Reset connection to get new /etc/environment meta: reset_connection - # NB: conditionals not supported \ No newline at end of file + # NB: conditionals not supported diff --git a/ansible/roles/resolv_conf/templates/resolv.conf.j2 b/ansible/roles/resolv_conf/templates/resolv.conf.j2 index 59c2c000f..b752046c9 100644 --- a/ansible/roles/resolv_conf/templates/resolv.conf.j2 +++ b/ansible/roles/resolv_conf/templates/resolv.conf.j2 @@ -1,5 +1,7 @@ # Created by slurm appliance ansible/roles/resolv_conf -search {{ openhpc_cluster_name }}.{{ tld }} +{% if cluster_domain_suffix is defined %} +search {{ openhpc_cluster_name }}.{{ cluster_domain_suffix }} +{% endif %} {% for ns in resolv_conf_nameservers[0:3] %} nameserver {{ ns }} diff --git a/ansible/site.yml b/ansible/site.yml index 37befa547..fd564367f 100644 --- a/ansible/site.yml +++ b/ansible/site.yml @@ -17,12 +17,12 @@ import_playbook: "{{ hook_path if hook_path | exists else 'noop.yml' }}" when: hook_path | exists +- import_playbook: iam.yml - import_playbook: filesystems.yml - import_playbook: extras.yml - import_playbook: slurm.yml - import_playbook: portal.yml - import_playbook: monitoring.yml -- import_playbook: iam.yml - name: Run post.yml hook vars: diff --git a/ansible/validate.yml b/ansible/validate.yml index d294e98e5..02da9d285 100644 --- a/ansible/validate.yml +++ b/ansible/validate.yml @@ -75,3 +75,11 @@ - openondemand - openondemand_server - grafana + +- name: Validate freeipa configuration + hosts: freeipa + tags: freeipa + tasks: + - import_role: + name: freeipa + tasks_from: validate.yml diff --git a/environments/.stackhpc/inventory/extra_groups b/environments/.stackhpc/inventory/extra_groups index cc87628e7..d1da6f4a8 100644 --- a/environments/.stackhpc/inventory/extra_groups +++ b/environments/.stackhpc/inventory/extra_groups @@ -7,3 +7,18 @@ compute [etc_hosts:children] cluster + +# -- Example of enabling FreeIPA with an in-appliance (dev-only) server +# NB: The etc_hosts and basic_users group definitions above should be commented out +# The freeipa_* hosts will pick up configuration from environments/.stackhpc/inventory/group_vars/all/freeipa.yml + +# [freeipa_server:children] +# control +# +# [freeipa_client:children] +# login +# compute +# +# [resolv_conf:children] +# freeipa_client +# --- end of FreeIPA example --- diff --git a/environments/.stackhpc/inventory/group_vars/all/basic_users.yml b/environments/.stackhpc/inventory/group_vars/all/basic_users.yml index 2f90a1d60..ae416cf72 100644 --- a/environments/.stackhpc/inventory/group_vars/all/basic_users.yml +++ b/environments/.stackhpc/inventory/group_vars/all/basic_users.yml @@ -1,7 +1,6 @@ -# has to be defined on 'all' group so localhost can template out for cloud-init -testuser_password: "{{ lookup('env', 'TESTUSER_PASSWORD') | default(vault_testuser_password, true) }}" +test_user_password: "{{ lookup('env', 'TESTUSER_PASSWORD') | default(vault_testuser_password, true) }}" # CI uses env, debug can set vault_testuser_password basic_users_users: - name: testuser # can't use rocky as $HOME isn't shared! - password: "{{ testuser_password | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}" # idempotent + password: "{{ test_user_password | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}" # idempotent uid: 1005 diff --git a/environments/.stackhpc/inventory/group_vars/all/freeipa.yml b/environments/.stackhpc/inventory/group_vars/all/freeipa.yml new file mode 100644 index 000000000..4b3750650 --- /dev/null +++ b/environments/.stackhpc/inventory/group_vars/all/freeipa.yml @@ -0,0 +1,12 @@ +# This file provides examples of using freeipa role variables. These are NOT functional in CI as freeipa_{server,client} groups are not defined. + +# NB: Users defined this way have expired passwords +freeipa_users: + - name: testuser # can't use rocky as $HOME isn't shared! + password: "{{ test_user_password }}" + givenname: test + sn: test + +# freeipa_client hosts must use a FreeIPA server for name resolution - requires hosts to be in group `resolv_conf`. +resolv_conf_nameservers: + - "{{ hostvars[groups['freeipa_server'].0].ansible_host }}" diff --git a/environments/.stackhpc/inventory/group_vars/basic_users/overrides.yml b/environments/.stackhpc/inventory/group_vars/basic_users/overrides.yml deleted file mode 100644 index ae416cf72..000000000 --- a/environments/.stackhpc/inventory/group_vars/basic_users/overrides.yml +++ /dev/null @@ -1,6 +0,0 @@ -test_user_password: "{{ lookup('env', 'TESTUSER_PASSWORD') | default(vault_testuser_password, true) }}" # CI uses env, debug can set vault_testuser_password - -basic_users_users: - - name: testuser # can't use rocky as $HOME isn't shared! - password: "{{ test_user_password | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}" # idempotent - uid: 1005 diff --git a/environments/common/inventory/group_vars/all/freeipa_server.yml b/environments/common/inventory/group_vars/all/freeipa_server.yml new file mode 100644 index 000000000..7f0fee713 --- /dev/null +++ b/environments/common/inventory/group_vars/all/freeipa_server.yml @@ -0,0 +1,7 @@ +# See ansible/roles/freeipa/README.md +# These vars are only used when freeipa_server is enabled. They are not required when enabling only freeipa_client +freeipa_realm: "{{ openhpc_cluster_name | upper }}.{{ cluster_domain_suffix | upper }}" +freeipa_ds_password: "{{ vault_freeipa_ds_password }}" +freeipa_admin_password: "{{ vault_freeipa_admin_password }}" +# the below doesn't use ansible_default_ipv4.address as that requires facts, and allows for templating when group freeipa_server is empty +freeipa_server_ip: "{{ hostvars[groups['freeipa_server'].0].ansible_host if groups['freeipa_server'] else false }}" diff --git a/environments/common/inventory/groups b/environments/common/inventory/groups index 475052d3b..59629c04e 100644 --- a/environments/common/inventory/groups +++ b/environments/common/inventory/groups @@ -104,8 +104,20 @@ grafana control prometheus +[freeipa_server] +# Hosts to be a FreeIPA server. **NB**: Intended only for test/development use. See ansible/roles/freeipa/README.md + +[freeipa_client] +# Hosts to be a FreeIPA client. See ansible/roles/freeipa/README.md + +[freeipa:children] +# Allows defining variables common to freeipa_server and _client +freeipa_server +freeipa_client + [cuda] # Hosts to install NVIDIA CUDA on - see ansible/roles/cuda/README.md + [resolv_conf] # Allows defining nameservers in /etc/resolv.conf - see ansible/roles/resolv_conf/README.md diff --git a/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tf b/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tf index d7298015c..5f195caf2 100644 --- a/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tf +++ b/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tf @@ -2,13 +2,13 @@ resource "local_file" "hosts" { content = templatefile("${path.module}/inventory.tpl", { "cluster_name": var.cluster_name, - "control": openstack_networking_port_v2.control, + "cluster_domain_suffix": var.cluster_domain_suffix, + "control_instances": openstack_compute_instance_v2.control + "login_instances": openstack_compute_instance_v2.login + "compute_instances": openstack_compute_instance_v2.compute "state_dir": var.state_dir, - "logins": openstack_networking_port_v2.login, - "computes": openstack_networking_port_v2.compute, "compute_types": var.compute_types, "compute_nodes": var.compute_nodes, - "subnet": data.openstack_networking_subnet_v2.cluster_subnet, }, ) filename = "../inventory/hosts" diff --git a/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tpl b/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tpl index 43c3250ee..11b2cfd45 100644 --- a/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tpl +++ b/environments/skeleton/{{cookiecutter.environment}}/terraform/inventory.tpl @@ -1,21 +1,24 @@ [all:vars] openhpc_cluster_name=${cluster_name} +cluster_domain_suffix=${cluster_domain_suffix} [control] -${control.name} ansible_host=${control.all_fixed_ips[0]} +%{ for control in control_instances ~} +${ control.name } ansible_host=${[for n in control.network: n.fixed_ip_v4 if n.access_network][0]} node_fqdn=${ control.name }.${cluster_name}.${cluster_domain_suffix} +%{ endfor ~} [control:vars] # NB needs to be set on group not host otherwise it is ignored in packer build! appliances_state_dir=${state_dir} [login] -%{ for login in logins ~} -${login.name} ansible_host=${login.all_fixed_ips[0]} +%{ for login in login_instances ~} +${ login.name } ansible_host=${[for n in login.network: n.fixed_ip_v4 if n.access_network][0]} node_fqdn=${ login.name }.${cluster_name}.${cluster_domain_suffix} %{ endfor ~} [compute] -%{ for compute in computes ~} -${compute.name} ansible_host=${compute.all_fixed_ips[0]} +%{ for compute in compute_instances ~} +${ compute.name } ansible_host=${[for n in compute.network: n.fixed_ip_v4 if n.access_network][0]} node_fqdn=${ compute.name }.${cluster_name}.${cluster_domain_suffix} %{ endfor ~} # Define groups for slurm parititions: @@ -23,7 +26,7 @@ ${compute.name} ansible_host=${compute.all_fixed_ips[0]} [${cluster_name}_${type_name}] %{~ for node_name, node_type in compute_nodes ~} %{~ if node_type == type_name ~} -${cluster_name}-${node_name} +${ compute_instances[node_name].name } %{~ endif ~} %{~ endfor ~} %{ endfor ~} diff --git a/environments/skeleton/{{cookiecutter.environment}}/terraform/nodes.tf b/environments/skeleton/{{cookiecutter.environment}}/terraform/nodes.tf index dc38ad487..914b5f1e1 100644 --- a/environments/skeleton/{{cookiecutter.environment}}/terraform/nodes.tf +++ b/environments/skeleton/{{cookiecutter.environment}}/terraform/nodes.tf @@ -124,6 +124,8 @@ resource "openstack_compute_instance_v2" "control" { user_data = <<-EOF #cloud-config + fqdn: ${var.cluster_name}-${each.key}.${var.cluster_name}.${var.cluster_domain_suffix} + fs_setup: - label: state filesystem: ext4 @@ -177,6 +179,11 @@ resource "openstack_compute_instance_v2" "login" { environment_root = var.environment_root } + user_data = <<-EOF + #cloud-config + fqdn: ${var.cluster_name}-${each.key}.${var.cluster_name}.${var.cluster_domain_suffix} + EOF + lifecycle{ ignore_changes = [ image_name, @@ -215,6 +222,11 @@ resource "openstack_compute_instance_v2" "compute" { environment_root = var.environment_root } + user_data = <<-EOF + #cloud-config + fqdn: ${var.cluster_name}-${each.key}.${var.cluster_name}.${var.cluster_domain_suffix} + EOF + lifecycle{ ignore_changes = [ image_name, diff --git a/environments/skeleton/{{cookiecutter.environment}}/terraform/variables.tf b/environments/skeleton/{{cookiecutter.environment}}/terraform/variables.tf index b0bfd2366..0804c6f33 100644 --- a/environments/skeleton/{{cookiecutter.environment}}/terraform/variables.tf +++ b/environments/skeleton/{{cookiecutter.environment}}/terraform/variables.tf @@ -1,6 +1,12 @@ variable "cluster_name" { type = string - description = "Name for cluster, used as prefix for resources" + description = "Name of cluster, used as part of domain name" +} + +variable "cluster_domain_suffix" { + type = string + description = "Domain suffix for cluster" + default = "invalid" } variable "cluster_net" {