diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c01a5d710..5814ebb93 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,7 @@ repos: - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ + exclude: '^.zuul.yaml' - repo: local hooks: - id: flake8 diff --git a/.zuul.yaml b/.zuul.yaml index 5244a4df7..4db226942 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -439,16 +439,43 @@ - job: name: openstacksdk-acceptance-base parent: openstack-tox - description: Acceptance test of the OpenStackSDK on real clouds + description: | + Acceptance test of the OpenStackSDK on real clouds. + + .. zuul:jobsvar::openstack_credentials + :type: dict + + This is expected to be a Zuul Secret with these keys: + + .. zuul:jobvar: auth + :type: dict + + Dictionary with authentication information with mandatory auth_url + and others. The structure mimics `clouds.yaml` structure. + + By default all jobs that inherit from here are non voting. + + attempts: 1 + voting: false pre-run: - playbooks/acceptance/pre.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 +# Acceptance tests for devstack are different from running for real cloud since +# we need to actually deploy devstack first and API is available only on the +# devstack host. - job: name: openstacksdk-acceptance-devstack parent: openstacksdk-functional-devstack - description: Acceptance test of the OpenStackSDK on real clouds + description: Acceptance test of the OpenStackSDK on real clouds. + attempts: 1 run: - playbooks/acceptance/run-with-devstack.yaml post-run: @@ -459,17 +486,25 @@ 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' + auth_url: "https://{{ hostvars['controller']['nodepool']['private_ipv4'] }}/identity" + secrets: + - secret: credentials-devstack + name: openstack_credentials + +# Devstack secret is not specifying auth_url because of how Zuul treats secrets. +# Auth_url comes extra in the job vars and is being used if no auth_url in the +# secret is present. +- secret: + name: credentials-devstack + data: + auth: + username: demo + password: secretadmin + project_domain_id: default + project_name: demo + user_domain_id: default + region_name: RegionOne + verify: false - project-template: name: openstacksdk-functional-tips diff --git a/openstack/tests/functional/base.py b/openstack/tests/functional/base.py index 381aece28..42cf40b99 100644 --- a/openstack/tests/functional/base.py +++ b/openstack/tests/functional/base.py @@ -106,7 +106,7 @@ class BaseFunctionalTest(base.TestCase): return None flavors = self.user_cloud.list_flavors(get_extra=False) - self.add_info_on_exception('flavors', flavors) + # self.add_info_on_exception('flavors', flavors) flavor_name = os.environ.get('OPENSTACKSDK_FLAVOR') @@ -146,7 +146,7 @@ class BaseFunctionalTest(base.TestCase): return None images = self.user_cloud.list_images() - self.add_info_on_exception('images', images) + # self.add_info_on_exception('images', images) image_name = os.environ.get('OPENSTACKSDK_IMAGE') diff --git a/playbooks/acceptance/library b/playbooks/acceptance/library deleted file mode 120000 index 53bed9684..000000000 --- a/playbooks/acceptance/library +++ /dev/null @@ -1 +0,0 @@ -../library \ No newline at end of file diff --git a/playbooks/acceptance/post.yaml b/playbooks/acceptance/post.yaml index 4e3e00e82..32d0f80ad 100644 --- a/playbooks/acceptance/post.yaml +++ b/playbooks/acceptance/post.yaml @@ -1,18 +1,42 @@ -- hosts: localhost +--- +# This could be running on localhost only, but then the devstack job would need +# to perform API call on the worker node. To keep the code a bit less crazy +# rather address all hosts and perform certain steps on the localhost (zuul +# executor). +- hosts: all 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 + + # Token is saved on the zuul executor node + - name: Check token file + delegate_to: localhost + ansible.builtin.stat: + path: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" + register: token_file + + # no_log is important since content WILL in logs + - name: Read the token from file + delegate_to: localhost no_log: true + ansible.builtin.slurp: + src: "{{ token_file.stat.path }}" + register: token_data + when: "token_file.stat.exists" - - name: delete data file - command: "shred {{ zuul.executor.work_root }}/.{{ zuul.build }}" + - name: Delete data file + delegate_to: localhost + command: "shred {{ token_file.stat.path }}" + when: "token_file.stat.exists" - - include_role: - name: revoke_token - vars: - cloud: "{{ openstack_credentials }}" - token: "{{ token_data.stdout }}" + # no_log is important since content WILL appear in logs + - name: Revoke token + no_log: true + ansible.builtin.uri: + url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}/v3/auth/tokens" + method: "DELETE" + headers: + X-Auth-Token: "{{ token_data['content'] | b64decode }}" + X-Subject-Token: "{{ token_data['content'] | b64decode }}" + status_code: 204 + when: "token_file.stat.exists and 'content' in token_data" diff --git a/playbooks/acceptance/pre.yaml b/playbooks/acceptance/pre.yaml index 078fd2040..091c9a32e 100644 --- a/playbooks/acceptance/pre.yaml +++ b/playbooks/acceptance/pre.yaml @@ -1,40 +1,45 @@ +--- - hosts: all tasks: - name: Get temporary token for the cloud - # nolog is important to keep job-output.json clean + # nolog is important since content WILL appear in logs no_log: true - os_auth: - cloud: - profile: "{{ openstack_credentials.profile | default(omit) }}" + ansible.builtin.uri: + url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}/v3/auth/tokens" + method: "POST" + body_format: "json" + body: 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) }}" + identity: + methods: ["password"] + password: + user: + name: "{{ openstack_credentials.auth.username | default(omit) }}" + id: "{{ openstack_credentials.auth.user_id | default(omit) }}" + password: "{{ openstack_credentials.auth.password }}" + domain: + name: "{{ openstack_credentials.auth.user_domain_name | default(omit) }}" + id: "{{ openstack_credentials.auth.user_domain_id | default(omit) }}" + scope: + project: + name: "{{ openstack_credentials.auth.project_name | default(omit) }}" + id: "{{ openstack_credentials.auth.project_id | default(omit) }}" + domain: + name: "{{ openstack_credentials.auth.project_domain_name | default(omit) }}" + id: "{{ openstack_credentials.auth.project_domain_id | default(omit) }}" + return_content: true + status_code: 201 register: os_auth - delegate_to: localhost - name: Verify token + # nolog is important since content WILL appear in logs 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 + ansible.builtin.uri: + url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}/v3/auth/tokens" + method: "GET" + headers: + X-Auth-Token: "{{ os_auth.x_subject_token }}" + X-Subject-Token: "{{ os_auth.x_subject_token }}" - name: Include deploy-clouds-config role include_role: @@ -43,18 +48,22 @@ cloud_config: clouds: acceptance: - profile: "{{ openstack_credentials.profile | default(omit) }}" + profile: "{{ openstack_credentials.profile | default('') }}" 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 }}" + auth_url: "{{ openstack_credentials.auth.auth_url | default(auth_url) }}" + project_name: "{{ openstack_credentials.auth.project_name | default('') }}" + project_domain_id: "{{ openstack_credentials.auth.project_domain_id | default('') }}" + project_domain_name: "{{ openstack_credentials.auth.project_domain_name | default('') }}" + token: "{{ os_auth.x_subject_token }}" + region_name: "{{ openstack_credentials.region_name | default('') }}" + verify: "{{ openstack_credentials.verify | default(true) }}" # 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 + - name: Save the token delegate_to: localhost copy: dest: "{{ zuul.executor.work_root }}/.{{ zuul.build }}" - content: "{{ os_auth.auth_token }}" - mode: "0440" + content: "{{ os_auth.x_subject_token }}" + mode: "0640" diff --git a/playbooks/acceptance/run-with-devstack.yaml b/playbooks/acceptance/run-with-devstack.yaml index a8e8b3522..26dcdf68e 100644 --- a/playbooks/acceptance/run-with-devstack.yaml +++ b/playbooks/acceptance/run-with-devstack.yaml @@ -1,75 +1,11 @@ +--- # 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" +- name: Get the token + ansible.builtin.import_playbook: pre.yaml # Run the rest - hosts: all diff --git a/playbooks/library/os_auth.py b/playbooks/library/os_auth.py deleted file mode 100644 index 48903a085..000000000 --- a/playbooks/library/os_auth.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/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/revoke_token/README.rst b/roles/revoke_token/README.rst deleted file mode 100644 index e69de29bb..000000000 diff --git a/roles/revoke_token/library/os_auth_revoke.py b/roles/revoke_token/library/os_auth_revoke.py deleted file mode 100644 index 85a16a462..000000000 --- a/roles/revoke_token/library/os_auth_revoke.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/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 deleted file mode 100644 index d7730eb08..000000000 --- a/roles/revoke_token/tasks/main.yaml +++ /dev/null @@ -1,7 +0,0 @@ -- name: Revoke token - delegate_to: localhost - no_log: true - os_auth_revoke: - cloud: "{{ cloud }}" - revoke_token: "{{ token }}" - failed_when: false