diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 000000000..4361b29c3 --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,4 @@ +--- + +exclude_paths: + - ansible/.venv/ diff --git a/.github/workflows/containers.yaml b/.github/workflows/containers.yaml index 4752f7093..b80c45174 100644 --- a/.github/workflows/containers.yaml +++ b/.github/workflows/containers.yaml @@ -7,12 +7,14 @@ on: branches: - main paths: + - "ansible/**" - "containers/**" - ".github/workflows/containers.yaml" - "python/**" pull_request: types: [opened, synchronize, reopened, closed] paths: + - "ansible/**" - "containers/**" - ".github/workflows/containers.yaml" - "python/**" @@ -123,6 +125,7 @@ jobs: container: - name: ironic-nautobot-client - name: nova-flavors + - name: ansible steps: - name: setup docker buildx @@ -182,6 +185,7 @@ jobs: - dnsmasq - ironic-nautobot-client - nova-flavors + - ansible steps: - name: clean up PR container diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 06eb80ce5..93a96c502 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,6 +48,15 @@ repos: - id: ruff args: [--fix] - id: ruff-format + - repo: https://github.com/ansible/ansible-lint + rev: v25.1.2 + hooks: + - id: ansible-lint + entry: "sh -c 'cd ansible && python3 -m ansiblelint -v --force-color'" + additional_dependencies: + - ansible + - jmespath + files: '^ansible/.*$' - repo: https://github.com/python-poetry/poetry rev: '1.7.1' hooks: diff --git a/ansible/playbooks/debug.yaml b/ansible/playbooks/debug.yaml new file mode 100644 index 000000000..1332b97c9 --- /dev/null +++ b/ansible/playbooks/debug.yaml @@ -0,0 +1,20 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Debug + hosts: localhost + + roles: + - role: debug diff --git a/ansible/playbooks/keystone_bootstrap.yaml b/ansible/playbooks/keystone_bootstrap.yaml new file mode 100644 index 000000000..1d733a658 --- /dev/null +++ b/ansible/playbooks/keystone_bootstrap.yaml @@ -0,0 +1,30 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Keystone Bootstrap + hosts: keystone + connection: local + + pre_tasks: + - name: Fail if ENV variables are not set + ansible.builtin.fail: + msg: "Environment variable {{ item }} is not set. Exiting playbook." + when: lookup('env', item) == '' + loop: + - OS_USERNAME + - OS_DEFAULT_DOMAIN + + roles: + - role: keystone_bootstrap diff --git a/ansible/requirements.txt b/ansible/requirements.txt new file mode 100644 index 000000000..d260a2110 --- /dev/null +++ b/ansible/requirements.txt @@ -0,0 +1,7 @@ +ansible-core==2.18.4 +ansible-runner==2.4.0 +openstacksdk==4.3.0 +pynautobot==2.6.1 +jmespath==1.0.1 +# remove me after the inherited roles workaround can be dropped +python-openstackclient diff --git a/ansible/requirements.yml b/ansible/requirements.yml new file mode 100644 index 000000000..a88b98bd5 --- /dev/null +++ b/ansible/requirements.yml @@ -0,0 +1,7 @@ +collections: + - name: community.general + version: "==10.5.0" + - name: openstack.cloud + version: "==2.4.1" + - name: networktocode.nautobot + version: "==5.6.0" diff --git a/ansible/roles/debug/tasks/main.yml b/ansible/roles/debug/tasks/main.yml new file mode 100644 index 000000000..3fdf58da3 --- /dev/null +++ b/ansible/roles/debug/tasks/main.yml @@ -0,0 +1,18 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Debug + ansible.builtin.debug: + msg: debug diff --git a/ansible/roles/keystone_bootstrap/defaults/main.yml b/ansible/roles/keystone_bootstrap/defaults/main.yml new file mode 100644 index 000000000..93b0fb3b8 --- /dev/null +++ b/ansible/roles/keystone_bootstrap/defaults/main.yml @@ -0,0 +1,21 @@ +--- +keystone_bootstrap_dex_url: "{{ dex_url | default('https://dex.' + lookup('ansible.builtin.env', 'DNS_ZONE', default='localnet')) }}" + +keystone_bootstrap_groups: + - name: ucadmin + desc: 'Users Federated with Admin' + roles: + - member + - admin + - name: ucuser + desc: 'Regular Federated Users' + roles: + - member + - name: ucneteng + desc: 'Federated Network Engineers' + roles: + - member + - name: ucdctech + desc: 'Federated DC Technicians' + roles: + - member diff --git a/ansible/roles/keystone_bootstrap/tasks/baremetal.yml b/ansible/roles/keystone_bootstrap/tasks/baremetal.yml new file mode 100644 index 000000000..1112e8542 --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/baremetal.yml @@ -0,0 +1,27 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Create 'infra' domain + openstack.cloud.identity_domain: + name: infra + description: 'System Infra' + state: present + +- name: Create 'baremetal' project in 'infra' domain + openstack.cloud.project: + name: baremetal + domain: infra + description: 'Ironic Resources' + state: present diff --git a/ansible/roles/keystone_bootstrap/tasks/main.yml b/ansible/roles/keystone_bootstrap/tasks/main.yml new file mode 100644 index 000000000..661e67532 --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/main.yml @@ -0,0 +1,17 @@ +--- + +- name: Admin needs admin role for default domain + openstack.cloud.role_assignment: + user: "{{ lookup('ansible.builtin.env', 'OS_USERNAME', default=Undefined) }}" + domain: "{{ lookup('ansible.builtin.env', 'OS_DEFAULT_DOMAIN', default=Undefined) }}" + role: admin + state: present + +- name: Define baremetal + ansible.builtin.include_tasks: baremetal.yml + +- name: Define SSO + ansible.builtin.include_tasks: sso.yml + +- name: Define misc keystone + ansible.builtin.include_tasks: misc.yml diff --git a/ansible/roles/keystone_bootstrap/tasks/misc.yml b/ansible/roles/keystone_bootstrap/tasks/misc.yml new file mode 100644 index 000000000..d5d952413 --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/misc.yml @@ -0,0 +1,64 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Create 'argoworkflow' user + openstack.cloud.identity_user: + name: argoworkflow + password: demo + domain: infra + state: present + +- name: Set 'argoworkflow' role + openstack.cloud.role_assignment: + domain: infra + user: argoworkflow + project: baremetal + role: admin + state: present + +- name: Create 'monitoring' user + openstack.cloud.identity_user: + name: monitoring + password: monitoring_demo + domain: infra + state: present + +- name: Set 'monitoring' role + openstack.cloud.role_assignment: + domain: infra + user: monitoring + project: baremetal + role: admin + state: present + +- name: Create 'flavorsync' user + openstack.cloud.identity_user: + name: flavorsync + password: abcd1234 + domain: service + state: present + register: _flavor_sync_user + +- name: Create 'flavorsync' role + openstack.cloud.identity_role: + name: flavorsync + state: present + +- name: Set 'flavorsync' role + openstack.cloud.role_assignment: + user: "{{ _flavor_sync_user.user.id }}" + domain: default + role: flavorsync + state: present diff --git a/ansible/roles/keystone_bootstrap/tasks/sso.yml b/ansible/roles/keystone_bootstrap/tasks/sso.yml new file mode 100644 index 000000000..50b4b7c57 --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/sso.yml @@ -0,0 +1,66 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Create 'sso' domain + openstack.cloud.identity_domain: + name: sso + description: 'SSO to dex' + state: present + register: _domain_sso + +- name: Display 'sso' configuration + ansible.builtin.debug: + var: keystone_bootstrap_dex_url + +- name: Create 'sso' identity provider + openstack.cloud.federation_idp: + name: sso + domain_id: "{{ _domain_sso.domain.id }}" + description: 'Identity Provider to dex' + remote_ids: + - "{{ keystone_bootstrap_dex_url }}" + +- name: Create sso mapping + openstack.cloud.federation_mapping: + name: sso_mapping + rules: + - local: + - user: + id: '{0}' + name: '{1}' + email: '{2}' + groups: '{3}' + domain: + id: "{{ _domain_sso.domain.id }}" + remote: + - type: HTTP_OIDC_SUB + - type: REMOTE_USER + - type: HTTP_OIDC_EMAIL + - type: HTTP_OIDC_GROUPS + +- name: Create openid protocol + openstack.cloud.keystone_federation_protocol: + name: openid + idp: sso + mapping: sso_mapping + +- name: Create federated group mappings + ansible.builtin.include_tasks: sso_member_groups.yml + loop: "{{ keystone_bootstrap_groups }}" + +- name: Grant admin for groups + ansible.builtin.include_tasks: sso_role_admin.yml + loop: + - ucadmin diff --git a/ansible/roles/keystone_bootstrap/tasks/sso_member_groups.yml b/ansible/roles/keystone_bootstrap/tasks/sso_member_groups.yml new file mode 100644 index 000000000..784862227 --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/sso_member_groups.yml @@ -0,0 +1,28 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +- name: Create group + openstack.cloud.identity_group: + name: "{{ item.name }}" + domain_id: "{{ _domain_sso.domain.id }}" + description: "{{ item.desc }}" + state: present + register: _group + +# role assignment module is lacking inherited and cross domain assignments +- name: Assign member access + ansible.builtin.command: openstack role add --group "{{ _group.group.id }}" --domain default --inherited member + when: dont_set_roles is not defined + changed_when: false diff --git a/ansible/roles/keystone_bootstrap/tasks/sso_role_admin.yml b/ansible/roles/keystone_bootstrap/tasks/sso_role_admin.yml new file mode 100644 index 000000000..1fbb58460 --- /dev/null +++ b/ansible/roles/keystone_bootstrap/tasks/sso_role_admin.yml @@ -0,0 +1,31 @@ +--- +# Copyright (c) 2025 Rackspace Technology, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +- name: Find group + openstack.cloud.identity_group_info: + name: "{{ item }}" + domain: "{{ _domain_sso.domain.id }}" + +# role assignment module is lacking inherited and cross domain assignments +- name: Assign member access + ansible.builtin.command: openstack role add --group "{{ _group.group.id }}" --domain default --inherited admin + when: dont_set_roles is not defined + changed_when: false + +# role assignment module is lacking inherited and cross domain assignments +- name: Assign member access + ansible.builtin.command: openstack role add --group "{{ _group.group.id }}" --domain infra --inherited admin + when: dont_set_roles is not defined + changed_when: false diff --git a/apps/appsets/openstack.yaml b/apps/appsets/openstack.yaml index 792706b1b..2046d1d2a 100644 --- a/apps/appsets/openstack.yaml +++ b/apps/appsets/openstack.yaml @@ -18,7 +18,7 @@ spec: elements: - component: keystone repoURL: https://tarballs.opendev.org/openstack/openstack-helm - chartVersion: 2024.2.4+79d4b689-eb60e37c + chartVersion: 2024.2.6+06d763432 - component: openvswitch repoURL: https://tarballs.opendev.org/openstack/openstack-helm-infra chartVersion: 2024.2.0 diff --git a/components/images-openstack.yaml b/components/images-openstack.yaml index 950b92143..16cddba98 100644 --- a/components/images-openstack.yaml +++ b/components/images-openstack.yaml @@ -14,7 +14,6 @@ images: # keystone keystone_api: "ghcr.io/rackerlabs/understack/keystone:2024.2-ubuntu_jammy" - keystone_bootstrap: "docker.io/openstackhelm/heat:2024.2-ubuntu_jammy" keystone_credential_rotate: "ghcr.io/rackerlabs/understack/keystone:2024.2-ubuntu_jammy" keystone_credential_setup: "ghcr.io/rackerlabs/understack/keystone:2024.2-ubuntu_jammy" keystone_db_sync: "ghcr.io/rackerlabs/understack/keystone:2024.2-ubuntu_jammy" diff --git a/components/keystone/values.yaml b/components/keystone/values.yaml index d0f952ae3..29ebb404d 100644 --- a/components/keystone/values.yaml +++ b/components/keystone/values.yaml @@ -2,102 +2,15 @@ --- release_group: null +images: + tags: + bootstrap: "ghcr.io/rackerlabs/understack/ansible:latest" + bootstrap: enabled: true ks_user: admin script: | - # admin needs the admin role for the default domain - openstack role add \ - --user="${OS_USERNAME}" \ - --domain="${OS_DEFAULT_DOMAIN}" \ - "admin" - # create 'infra' domain - openstack domain create --or-show infra - # create 'baremetal' project for our ironic nodes to live in - openstack project create --or-show --domain infra baremetal - # create 'argoworkflow' user for automation - openstack user create --or-show --domain infra --password demo argoworkflow - # give 'argoworkflow' 'admin' over the 'baremetal' project - openstack role add --user-domain infra --project-domain infra --user argoworkflow --project baremetal admin - - # create 'flavorsync' user to allow synchronization of the flavors to nova - openstack user create --or-show --domain service --password abcd1234 flavorsync - openstack role create --or-show flavorsync - openstack role add --user flavorsync --user-domain service --domain default --inherited flavorsync - - # create 'monitoring' user for monitoring usage - openstack user create --or-show --domain infra --password monitoring_demo monitoring - # give 'monitoring' the 'admin' over the 'baremetal' project - openstack role add --user-domain infra --project-domain infra --user monitoring --project baremetal admin - - # this is too early because ironic won't exist - openstack role add --project service --user ironic --user-domain service service - - # OIDC integration - RULES_FILE=$(mktemp) - cat < ${RULES_FILE} - [ - { - "local": [ - { - "user": { - "id": "{0}", - "name": "{1}", - "email": "{2}" - }, - "groups": "{3}", - "domain": { - "name": "sso" - } - } - ], - "remote": [ - { - "type": "HTTP_OIDC_SUB" - }, - { - "type": "REMOTE_USER" - }, - { - "type": "HTTP_OIDC_EMAIL" - }, - { - "type": "HTTP_OIDC_GROUPS" - } - ] - } - ] - EOF - # look up or create our domain for SSO integration, called 'sso' - sso_domain_id=$(openstack domain create --or-show --description "Domain for 'sso' identity provider" -f value -c id sso) - - # define our identity provider 'sso' for the domain 'sso' - openstack identity provider show sso 2>/dev/null || openstack identity provider create --domain "${sso_domain_id}" --description "Identity Provider to our dex" --remote-id https://dex.dev.undercloud.rackspace.net sso - - # create or update the mapping that we'll use for the 'sso' identity - if openstack mapping show sso_mapping 2>/dev/null; then - openstack mapping set --rules ${RULES_FILE} sso_mapping; - else - openstack mapping create --rules ${RULES_FILE} sso_mapping; - fi - - # clean up - rm -f "${RULES_FILE}" - - # create the federation protocol 'openid' to use the identity provider 'sso' with the 'sso_mapping' - openstack federation protocol show openid --identity-provider sso || openstack federation protocol create openid --mapping sso_mapping --identity-provider sso - - for group in ucadmin ucuser ucneteng ucdctech; do - # create groups which map to our groups claim from dex which can be mapped to permissions - openstack group create --domain "${sso_domain_id}" ${group} --or-show - # give each of the groups the member role on the domain and have them inherit it to each project - # in the domain - openstack role add --group-domain "${sso_domain_id}" --group ${group} --inherited --domain default member - done - # ucadmin can manage the standard project domain - openstack role add --group-domain "${sso_domain_id}" --group ucadmin --inherited --domain default manager - # ucadmin can manage the infra domain - openstack role add --group-domain "${sso_domain_id}" --group ucadmin --inherited --domain infra manager + ansible-runner run /runner --playbook keystone_bootstrap.yaml -vv network: # configure OpenStack Helm to use Undercloud's ingress @@ -238,6 +151,20 @@ pod: - name: oidc-secret secret: secretName: sso-passphrase + keystone_bootstrap: + keystone_bootstrap: + volumeMounts: + - name: ansible-inventory + mountPath: /runner/inventory/ + - name: ansible-group-vars + mountPath: /runner/inventory/group_vars/ + volumes: + - name: ansible-inventory + configMap: + name: ansible-inventory + - name: ansible-group-vars + configMap: + name: ansible-group-vars replicas: api: 2 lifecycle: diff --git a/containers/ansible/Dockerfile.ansible b/containers/ansible/Dockerfile.ansible new file mode 100644 index 000000000..48bd3a9ff --- /dev/null +++ b/containers/ansible/Dockerfile.ansible @@ -0,0 +1,28 @@ +FROM python:3.12-slim AS prod + +ENV PIP_NO_CACHE_DIR=off \ + PIP_DISABLE_PIP_VERSION_CHECK=on \ + PIP_DEFAULT_TIMEOUT=100 + +RUN apt-get update \ + && apt-get install -y --no-install-recommends \ + git \ + && apt-get autoremove -y \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + +COPY ansible/requirements.txt ansible/requirements.yml / + +RUN --mount=type=cache,target=/root/.cache/pip pip install -r /requirements.txt dumb-init==1.2.5 + +RUN useradd -m -d /runner -s /bin/bash runner +WORKDIR /runner +USER runner + +RUN --mount=type=cache,target=/root/.cache/pip ansible-galaxy collection install -r /requirements.yml + +COPY ansible/playbooks/ /runner/project/ +COPY ansible/roles/ /runner/project/roles/ + +ENTRYPOINT ["dumb-init"] +CMD ["ansible-runner"] diff --git a/docs/component-ansible.md b/docs/component-ansible.md new file mode 100644 index 000000000..eb60424c6 --- /dev/null +++ b/docs/component-ansible.md @@ -0,0 +1,28 @@ +# Ansible + +Ansible is used to configure different parts of the overall system +in a consistent manner. To this effect a container is produced with +playbooks, roles and collections pre-installed and it can be run by +providing system configuration to it. + +## Execution Environment + +Ansible is executed within a container which is build within this repo. +The configuration and the source are contained within the +[`ansible/`][ansible-src] directory. + +## Configuration + +The execution environment within the container is [ansible-runner][ansible-runner]. +An inventory directory is necessary to be provided which would be part +of your system deployment data. + +## Sample Execution + +```bash +docker run --rm -it ghcr.io/rackerlabs/understack/ansible:latest -- \ + ansible-runner run /runner --playbook debug.yaml +``` + +[ansible-src]: +[ansible-runner]: diff --git a/mkdocs.yml b/mkdocs.yml index 240536195..346d4e93c 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -116,6 +116,7 @@ nav: - component-networking-neutron.md - component-argo-workflows.md - component-understack-workflows.md + - component-ansible.md - 'Deployment Guide': - deploy-guide/welcome.md - deploy-guide/getting-started.md