From 43ab59d8b38152e255ebd0501a4d763eb815ad03 Mon Sep 17 00:00:00 2001 From: Artem Goncharov Date: Wed, 5 Oct 2022 12:23:57 +0200 Subject: [PATCH] Implement acceptance test job Implement acceptance tests. Those jobs will run in the post-review pipeline requiring access to secrets containing credentials of friendly public clouds to test sdk with them. Base job is generating a token from the given credentials and writes clouds.yaml file with the token inside instead of password. As a post step the token is physically revoked. This is done to prevent potential leakage of real credentials from the test jobs/logs. Since devstack is not a real cloud we do not use zuul secrets. Change-Id: I95af9b81e6abd51af2a7dd91cae14b56926a869c --- .zuul.yaml | 38 +++++++++ playbooks/acceptance/library | 1 + playbooks/acceptance/post.yaml | 18 ++++ playbooks/acceptance/pre.yaml | 60 ++++++++++++++ playbooks/acceptance/run-with-devstack.yaml | 83 +++++++++++++++++++ playbooks/library/os_auth.py | 45 ++++++++++ roles/deploy-clouds-config/README.rst | 0 roles/deploy-clouds-config/defaults/main.yaml | 1 + roles/deploy-clouds-config/tasks/main.yaml | 11 +++ .../templates/clouds.yaml.j2 | 2 + roles/revoke_token/README.rst | 0 roles/revoke_token/library/os_auth_revoke.py | 72 ++++++++++++++++ roles/revoke_token/tasks/main.yaml | 7 ++ 13 files changed, 338 insertions(+) create mode 120000 playbooks/acceptance/library create mode 100644 playbooks/acceptance/post.yaml create mode 100644 playbooks/acceptance/pre.yaml create mode 100644 playbooks/acceptance/run-with-devstack.yaml create mode 100644 playbooks/library/os_auth.py create mode 100644 roles/deploy-clouds-config/README.rst create mode 100644 roles/deploy-clouds-config/defaults/main.yaml create mode 100644 roles/deploy-clouds-config/tasks/main.yaml create mode 100644 roles/deploy-clouds-config/templates/clouds.yaml.j2 create mode 100644 roles/revoke_token/README.rst create mode 100644 roles/revoke_token/library/os_auth_revoke.py create mode 100644 roles/revoke_token/tasks/main.yaml diff --git a/.zuul.yaml b/.zuul.yaml index 446be8113..5244a4df7 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -436,6 +436,41 @@ required-projects: - openstack/openstacksdk +- job: + name: openstacksdk-acceptance-base + parent: openstack-tox + description: Acceptance test of the OpenStackSDK on real clouds + pre-run: + - playbooks/acceptance/pre.yaml + post-run: + - playbooks/acceptance/post.yaml + +- job: + name: openstacksdk-acceptance-devstack + parent: openstacksdk-functional-devstack + description: Acceptance test of the OpenStackSDK on real clouds + run: + - playbooks/acceptance/run-with-devstack.yaml + post-run: + - playbooks/acceptance/post.yaml + vars: + tox_envlist: acceptance-regular-user + tox_environment: + OPENSTACKSDK_DEMO_CLOUD: acceptance + OS_CLOUD: acceptance + OS_TEST_CLOUD: acceptance + openstack_credentials: + auth: + auth_url: "https://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity" + username: demo + password: secretadmin + project_domain_id: default + project_name: demo + user_domain_id: default + identity_api_version: '3' + region_name: RegionOne + volume_api_version: '3' + - project-template: name: openstacksdk-functional-tips check: @@ -486,6 +521,9 @@ voting: false - ansible-collections-openstack-functional-devstack: voting: false + post-review: + jobs: + - openstacksdk-acceptance-devstack gate: jobs: - opendev-buildset-registry diff --git a/playbooks/acceptance/library b/playbooks/acceptance/library new file mode 120000 index 000000000..53bed9684 --- /dev/null +++ b/playbooks/acceptance/library @@ -0,0 +1 @@ +../library \ No newline at end of file diff --git a/playbooks/acceptance/post.yaml b/playbooks/acceptance/post.yaml new file mode 100644 index 000000000..4e3e00e82 --- /dev/null +++ b/playbooks/acceptance/post.yaml @@ -0,0 +1,18 @@ +- hosts: localhost + tasks: + # TODO: + # - clean the resources, which might have been created + # - revoke the temp token explicitly + - name: read token + command: "cat {{ zuul.executor.work_root }}/.{{ zuul.build }}" + register: token_data + no_log: true + + - name: delete data file + command: "shred {{ zuul.executor.work_root }}/.{{ zuul.build }}" + + - include_role: + name: revoke_token + vars: + cloud: "{{ openstack_credentials }}" + token: "{{ token_data.stdout }}" diff --git a/playbooks/acceptance/pre.yaml b/playbooks/acceptance/pre.yaml new file mode 100644 index 000000000..078fd2040 --- /dev/null +++ b/playbooks/acceptance/pre.yaml @@ -0,0 +1,60 @@ +- hosts: all + tasks: + - name: Get temporary token for the cloud + # nolog is important to keep job-output.json clean + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + username: "{{ openstack_credentials.auth.username }}" + password: "{{ openstack_credentials.auth.password }}" + user_domain_name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" + user_domain_id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" + domain_name: "{{ openstack_credentials.auth.domain_name | default(omit) }}" + domain_id: "{{ openstack_credentials.auth.domain_id | default(omit) }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + register: os_auth + delegate_to: localhost + + - name: Verify token + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: token + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + token: "{{ os_auth.auth_token }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + delegate_to: localhost + + - name: Include deploy-clouds-config role + include_role: + name: deploy-clouds-config + vars: + cloud_config: + clouds: + acceptance: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: "token" + auth: + auth_url: "{{ openstack_credentials.auth.auth_url | default(omit) }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + token: "{{ os_auth.auth_token }}" + + # Intruders might want to corrupt clouds.yaml to avoid revoking token in the post phase + # To prevent this we save token on the executor for later use. + - name: Save token + delegate_to: localhost + copy: + dest: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" + content: "{{ os_auth.auth_token }}" + mode: "0440" diff --git a/playbooks/acceptance/run-with-devstack.yaml b/playbooks/acceptance/run-with-devstack.yaml new file mode 100644 index 000000000..a8e8b3522 --- /dev/null +++ b/playbooks/acceptance/run-with-devstack.yaml @@ -0,0 +1,83 @@ +# Need to actually start devstack first +- hosts: all + roles: + - run-devstack + +# Prepare local clouds.yaml +# We can't rely on pre.yaml, since it is specifically delegates to +# localhost, while on devstack it will not work unless APIs are available +# over the net. +- hosts: all + tasks: + - name: Get temporary token for the cloud + # nolog is important to keep job-output.json clean + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + username: "{{ openstack_credentials.auth.username }}" + password: "{{ openstack_credentials.auth.password }}" + user_domain_name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" + user_domain_id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" + domain_name: "{{ openstack_credentials.auth.domain_name | default(omit) }}" + domain_id: "{{ openstack_credentials.auth.domain_id | default(omit) }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + register: os_auth + + - name: Verify token + # nolog is important to keep job-output.json clean + no_log: true + os_auth: + cloud: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: token + auth: + auth_url: "{{ openstack_credentials.auth.auth_url }}" + token: "{{ os_auth.auth_token }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + + - name: Include deploy-clouds-config role + include_role: + name: deploy-clouds-config + vars: + cloud_config: + clouds: + acceptance: + profile: "{{ openstack_credentials.profile | default(omit) }}" + auth_type: "token" + auth: + + auth_url: "{{ openstack_credentials.auth.auth_url }}" + project_name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + token: "{{ os_auth.auth_token }}" + verify: false + + # Intruders might want to corrupt clouds.yaml to avoid revoking token in + # the post phase. To prevent this we save token on the executor for later + # use. + - name: Save token + delegate_to: localhost + copy: + dest: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" + content: "{{ os_auth.auth_token }}" + mode: "0640" + +# Run the rest +- hosts: all + roles: + - role: bindep + bindep_profile: test + bindep_dir: "{{ zuul_work_dir }}" + - test-setup + - ensure-tox + - get-devstack-os-environment + - tox diff --git a/playbooks/library/os_auth.py b/playbooks/library/os_auth.py new file mode 100644 index 000000000..48903a085 --- /dev/null +++ b/playbooks/library/os_auth.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +# +# 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. + +""" +Utility to get Keystone token +""" +from ansible.module_utils.basic import AnsibleModule + +import openstack + + +def get_cloud(cloud): + if isinstance(cloud, dict): + config = openstack.config.loader.OpenStackConfig().get_one(**cloud) + return openstack.connection.Connection(config=config) + else: + return openstack.connect(cloud=cloud) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cloud=dict(required=True, type='raw', no_log=True), + ) + ) + cloud = get_cloud(module.params.get('cloud')) + module.exit_json( + changed=True, + auth_token=cloud.auth_token + ) + + +if __name__ == '__main__': + main() diff --git a/roles/deploy-clouds-config/README.rst b/roles/deploy-clouds-config/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/roles/deploy-clouds-config/defaults/main.yaml b/roles/deploy-clouds-config/defaults/main.yaml new file mode 100644 index 000000000..9739eb171 --- /dev/null +++ b/roles/deploy-clouds-config/defaults/main.yaml @@ -0,0 +1 @@ +zuul_work_dir: "{{ zuul.project.src_dir }}" diff --git a/roles/deploy-clouds-config/tasks/main.yaml b/roles/deploy-clouds-config/tasks/main.yaml new file mode 100644 index 000000000..f10533bda --- /dev/null +++ b/roles/deploy-clouds-config/tasks/main.yaml @@ -0,0 +1,11 @@ +- name: Create OpenStack config dir + ansible.builtin.file: + dest: ~/.config/openstack + state: directory + recurse: true + +- name: Deploy clouds.yaml + ansible.builtin.template: + src: clouds.yaml.j2 + dest: ~/.config/openstack/clouds.yaml + mode: 0440 diff --git a/roles/deploy-clouds-config/templates/clouds.yaml.j2 b/roles/deploy-clouds-config/templates/clouds.yaml.j2 new file mode 100644 index 000000000..267d90065 --- /dev/null +++ b/roles/deploy-clouds-config/templates/clouds.yaml.j2 @@ -0,0 +1,2 @@ +--- +{{ cloud_config | to_nice_yaml }} diff --git a/roles/revoke_token/README.rst b/roles/revoke_token/README.rst new file mode 100644 index 000000000..e69de29bb diff --git a/roles/revoke_token/library/os_auth_revoke.py b/roles/revoke_token/library/os_auth_revoke.py new file mode 100644 index 000000000..85a16a462 --- /dev/null +++ b/roles/revoke_token/library/os_auth_revoke.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright 2014 Rackspace Australia +# Copyright 2018 Red Hat, 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. + +""" +Utility to revoke Keystone token +""" + +import logging +import traceback + +from ansible.module_utils.basic import AnsibleModule +import keystoneauth1.exceptions +import requests +import requests.exceptions + +import openstack + + +def get_cloud(cloud): + if isinstance(cloud, dict): + config = openstack.config.loader.OpenStackConfig().get_one(**cloud) + return openstack.connection.Connection(config=config) + else: + return openstack.connect(cloud=cloud) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + cloud=dict(required=True, type='raw', no_log=True), + revoke_token=dict(required=True, type='str', no_log=True) + ) + ) + + p = module.params + cloud = get_cloud(p.get('cloud')) + try: + cloud.identity.delete( + '/auth/tokens', + headers={ + 'X-Subject-Token': p.get('revoke_token') + } + ) + except (keystoneauth1.exceptions.http.HttpError, + requests.exceptions.RequestException): + s = "Error performing token revoke" + logging.exception(s) + s += "\n" + traceback.format_exc() + module.fail_json( + changed=False, + msg=s, + cloud=cloud.name, + region_name=cloud.config.region_name) + module.exit_json(changed=True) + + +if __name__ == '__main__': + main() diff --git a/roles/revoke_token/tasks/main.yaml b/roles/revoke_token/tasks/main.yaml new file mode 100644 index 000000000..d7730eb08 --- /dev/null +++ b/roles/revoke_token/tasks/main.yaml @@ -0,0 +1,7 @@ +- name: Revoke token + delegate_to: localhost + no_log: true + os_auth_revoke: + cloud: "{{ cloud }}" + revoke_token: "{{ token }}" + failed_when: false