├── src ├── files │ └── .gitkeep ├── lib │ ├── __init__.py │ └── charm │ │ ├── __init__.py │ │ └── openstack │ │ ├── __init__.py │ │ └── tempest.py ├── copyright ├── reactive │ ├── __init__.py │ └── tempest_handlers.py ├── .stestr.conf ├── actions │ ├── run-tempest │ ├── __init__.py │ └── run-tempest.py ├── templates │ ├── pip.conf │ └── tempest.conf ├── wheelhouse.txt ├── unit_tests │ ├── __init__.py │ └── test_noop.py ├── layer.yaml ├── tests │ ├── gate-basic-bionic-queens │ ├── gate-basic-xenial-mitaka │ ├── README.md │ ├── gate-basic-bionic-stein │ ├── gate-basic-bionic-train │ ├── gate-basic-xenial-pike │ ├── gate-basic-bionic-rocky │ ├── gate-basic-xenial-ocata │ ├── gate-basic-xenial-queens │ ├── tests.yaml │ └── basic_deployment.py ├── actions.yaml ├── hooks │ ├── install.real │ └── install ├── metadata.yaml ├── test-requirements.txt ├── README.md ├── config.yaml ├── tox.ini └── icon.svg ├── .stestr.conf ├── .gitignore ├── .gitreview ├── .zuul.yaml ├── rebuild ├── requirements.txt ├── test-requirements.txt ├── unit_tests ├── tempest_output.py ├── test_tempest_handlers.py ├── __init__.py └── test_lib_charm_openstack_tempest.py ├── README.md ├── copyright └── tox.ini /src/files/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/copyright: -------------------------------------------------------------------------------- 1 | ../copyright -------------------------------------------------------------------------------- /src/lib/charm/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/reactive/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/.stestr.conf: -------------------------------------------------------------------------------- 1 | ../.stestr.conf -------------------------------------------------------------------------------- /src/actions/run-tempest: -------------------------------------------------------------------------------- 1 | run-tempest.py -------------------------------------------------------------------------------- /src/lib/charm/openstack/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.stestr.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | test_path=./unit_tests 3 | top_dir=./ 4 | -------------------------------------------------------------------------------- /src/actions/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | sys.path.append('lib') 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | build 3 | .coverage 4 | .testrepository 5 | .tox 6 | *.sw[nop] 7 | *.pyc 8 | .stestr 9 | -------------------------------------------------------------------------------- /.gitreview: -------------------------------------------------------------------------------- 1 | [gerrit] 2 | host=review.opendev.org 3 | port=29418 4 | project=openstack/charm-tempest.git 5 | -------------------------------------------------------------------------------- /src/templates/pip.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | {% if index_url -%} 3 | index-url = {{ index_url }} 4 | {% endif -%} 5 | -------------------------------------------------------------------------------- /.zuul.yaml: -------------------------------------------------------------------------------- 1 | - project: 2 | templates: 3 | - python35-charm-jobs 4 | - openstack-python3-ussuri-jobs 5 | - openstack-cover-jobs 6 | -------------------------------------------------------------------------------- /src/wheelhouse.txt: -------------------------------------------------------------------------------- 1 | 2 | git+https://opendev.org/openstack/charms.openstack.git#egg=charms.openstack 3 | 4 | git+https://github.com/juju/charm-helpers.git#egg=charmhelpers 5 | -------------------------------------------------------------------------------- /rebuild: -------------------------------------------------------------------------------- 1 | # This file is used to trigger rebuilds 2 | # when dependencies of the charm change, 3 | # but nothing in the charm needs to. 4 | # simply change the uuid to something new 5 | bed4920e-64d9-11eb-b348-a7e02257c332 6 | -------------------------------------------------------------------------------- /src/unit_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # By design, this unit_tests dir is inside the src charm (layer), 2 | # and it will be included in the resultant built charm asset. 3 | # 4 | # Include unit tests here which are intended to be executable 5 | # from the built charm. 6 | -------------------------------------------------------------------------------- /src/layer.yaml: -------------------------------------------------------------------------------- 1 | includes: ['layer:openstack', 'interface:keystone-admin'] 2 | options: 3 | basic: 4 | use_venv: True 5 | include_system_packages: False 6 | python_packages: ['keystoneauth1', 'python-glanceclient', 'python-novaclient', 'python-neutronclient'] 7 | -------------------------------------------------------------------------------- /src/tests/gate-basic-bionic-queens: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on bionic-queens.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | deployment = TempestBasicDeployment(series='bionic') 9 | deployment.run_tests() 10 | 11 | -------------------------------------------------------------------------------- /src/tests/gate-basic-xenial-mitaka: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on xenial-mitaka.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | deployment = TempestBasicDeployment(series='xenial') 9 | deployment.run_tests() 10 | 11 | -------------------------------------------------------------------------------- /src/reactive/tempest_handlers.py: -------------------------------------------------------------------------------- 1 | import charms.reactive as reactive 2 | import charm.openstack.tempest as tempest 3 | 4 | # config is rendered when the run tempest action is called 5 | 6 | 7 | @reactive.when_not('charm.installed') 8 | def install_packages(): 9 | tempest.install() 10 | reactive.set_state('charm.installed') 11 | 12 | 13 | @reactive.when('charm.installed') 14 | def assess_status(): 15 | tempest.assess_status() 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is managed centrally by release-tools and should not be modified 2 | # within individual charm repos. See the 'global' dir contents for available 3 | # choices of *requirements.txt files for OpenStack Charms: 4 | # https://github.com/openstack-charmers/release-tools 5 | # 6 | # Build requirements 7 | charm-tools>=2.4.4 8 | # importlib-resources 1.1.0 removed Python 3.5 support 9 | importlib-resources<1.1.0 10 | simplejson 11 | -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is managed centrally. If you find the need to modify this as a 2 | # one-off, please don't. Intead, consult #openstack-charms and ask about 3 | # requirements management in charms via bot-control. Thank you. 4 | # 5 | # Lint and unit test requirements 6 | flake8>=2.2.4,<=2.4.1 7 | stestr>=2.2.0 8 | requests>=2.18.4 9 | charms.reactive 10 | mock>=1.2 11 | nose>=1.3.7 12 | coverage>=3.6 13 | git+https://github.com/openstack/charms.openstack.git#egg=charms.openstack 14 | -------------------------------------------------------------------------------- /src/tests/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | This directory provides Amulet tests to verify basic deployment functionality 4 | from the perspective of this charm, its requirements and its features, as 5 | exercised in a subset of the full OpenStack deployment test bundle topology. 6 | 7 | For full details on functional testing of OpenStack charms please refer to 8 | the [functional testing](http://docs.openstack.org/developer/charm-guide/testing.html#functional-testing) 9 | section of the OpenStack Charm Guide. 10 | -------------------------------------------------------------------------------- /src/actions.yaml: -------------------------------------------------------------------------------- 1 | run-tempest: 2 | description: Run tempest 3 | params: 4 | service-whitelist: 5 | type: string 6 | description: Space seperated list services to check or auto to have the charm produce the list based on the environment 7 | default: 'auto' 8 | branch: 9 | type: string 10 | description: Git branch 11 | default: 'master' 12 | run-tempest-args: 13 | type: string 14 | description: Args to run tempest with 15 | default: '-V --smoke' 16 | -------------------------------------------------------------------------------- /unit_tests/tempest_output.py: -------------------------------------------------------------------------------- 1 | TEMPEST_OUT = """ 2 | ====== 3 | Totals 4 | ====== 5 | Ran: 62 tests in 64.8226 sec. 6 | - Passed: 21 7 | - Skipped: 41 8 | - Expected Fail: 0 9 | - Unexpected Success: 0 10 | - Failed: 0 11 | Sum of execute time for each test: 13.7436 sec. 12 | 13 | ============== 14 | Worker Balance 15 | ============== 16 | - Worker 0 (62 tests) => 0:00:59.719541 17 | ___________________________________ summary 18 | ____________________________________ 19 | smoke: commands succeeded 20 | congratulations :) 21 | """ 22 | -------------------------------------------------------------------------------- /src/tests/gate-basic-bionic-stein: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on bionic-stein.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | # Tempest is installed through pip so cloud archive is not needed here 9 | deployment = TempestBasicDeployment(series='bionic', 10 | openstack='cloud:bionic-stein', 11 | source='cloud:bionic-stein') 12 | deployment.run_tests() 13 | -------------------------------------------------------------------------------- /src/tests/gate-basic-bionic-train: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on bionic-train.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | # Tempest is installed through pip so cloud archive is not needed here 9 | deployment = TempestBasicDeployment(series='bionic', 10 | openstack='cloud:bionic-train', 11 | source='cloud:bionic-train') 12 | deployment.run_tests() 13 | -------------------------------------------------------------------------------- /src/tests/gate-basic-xenial-pike: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on xenial-pike.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | # Tempest is installed through pip so cloud archive is not needed here 9 | deployment = TempestBasicDeployment(series='xenial', 10 | openstack='cloud:xenial-pike', 11 | source='cloud:xenial-updates/pike') 12 | deployment.run_tests() 13 | -------------------------------------------------------------------------------- /src/tests/gate-basic-bionic-rocky: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on bionic-rocky.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | # Tempest is installed through pip so cloud archive is not needed here 9 | deployment = TempestBasicDeployment(series='bionic', 10 | openstack='cloud:bionic-rocky', 11 | source='cloud:bionic-updates/rocky') 12 | deployment.run_tests() 13 | -------------------------------------------------------------------------------- /src/tests/gate-basic-xenial-ocata: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on xenial-ocata.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | # Tempest is installed through pip so cloud archive is not needed here 9 | deployment = TempestBasicDeployment(series='xenial', 10 | openstack='cloud:xenial-ocata', 11 | source='cloud:xenial-updates/ocata') 12 | deployment.run_tests() 13 | -------------------------------------------------------------------------------- /src/tests/gate-basic-xenial-queens: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """Amulet tests on a basic tempest deployment on xenial-queens.""" 4 | 5 | from basic_deployment import TempestBasicDeployment 6 | 7 | if __name__ == '__main__': 8 | # Tempest is installed through pip so cloud archive is not needed here 9 | deployment = TempestBasicDeployment(series='xenial', 10 | openstack='cloud:xenial-queens', 11 | source='cloud:xenial-updates/queens') 12 | deployment.run_tests() 13 | -------------------------------------------------------------------------------- /src/hooks/install.real: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Load modules from $CHARM_DIR/lib 4 | import sys 5 | sys.path.append('lib') 6 | 7 | from charms.layer import basic 8 | basic.bootstrap_charm_deps() 9 | basic.init_config_states() 10 | 11 | 12 | # This will load and run the appropriate @hook and other decorated 13 | # handlers from $CHARM_DIR/reactive, $CHARM_DIR/hooks/reactive, 14 | # and $CHARM_DIR/hooks/relations. 15 | # 16 | # See https://jujucharms.com/docs/stable/authors-charm-building 17 | # for more information on this pattern. 18 | from charms.reactive import main 19 | main() 20 | -------------------------------------------------------------------------------- /src/unit_tests/test_noop.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestNoOp(unittest.TestCase): 5 | """Placeholder - Write Me!""" 6 | # XXX (beisner): with the charm.openstack vs lib/charm/openstack/tempest 7 | # module namespace collision, and with the hard requirement to have some 8 | # sort of unit test passing, here is a temporary inert noop test. After 9 | # charms_openstack module is completed, and this tempest charm is 10 | # refactored to use it, revisit this and add actual unit tests. 11 | def test_noop(self): 12 | """Test Nothing""" 13 | pass 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | > **WARNING**: DO NOT USE OR CONTRIBUTE. 4 | > [THIS CHARM IS DEPRECATED](https://docs.openstack.org/charm-guide/latest/openstack-charms.html). 5 | 6 | This is a "source" charm, which is intended to be strictly the top 7 | layer of a built charm. This structure declares that any included 8 | layer assets are not intended to be consumed as a layer from a 9 | functional or design standpoint. 10 | 11 | # Test and Build 12 | 13 | ``` 14 | tox -e pep8 15 | tox -e py34 # or py27 or py35 16 | tox -e build 17 | ``` 18 | 19 | # Contact Information 20 | 21 | OFTC IRC: #openstack-charms 22 | -------------------------------------------------------------------------------- /copyright: -------------------------------------------------------------------------------- 1 | Copyright 2016 Canonical Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /src/metadata.yaml: -------------------------------------------------------------------------------- 1 | name: tempest 2 | summary: OpenStack integration test suite 3 | maintainer: OpenStack Charmers 4 | description: | 5 | Tempest is a set of integration tests to be run against a live Openstack 6 | cluster. Tempest has batteries of tests for Openstack API validation, 7 | scenarios, and other specific tests useful in validating an Openstack 8 | deployment. 9 | tags: 10 | - openstack 11 | series: 12 | - xenial 13 | - bionic 14 | - focal 15 | - groovy 16 | subordinate: false 17 | requires: 18 | identity-admin: 19 | interface: keystone-admin 20 | dashboard: 21 | interface: http 22 | -------------------------------------------------------------------------------- /src/tests/tests.yaml: -------------------------------------------------------------------------------- 1 | # Bootstrap the model if necessary. 2 | bootstrap: True 3 | # Re-use bootstrap node. 4 | reset: True 5 | # Use tox/requirements to drive the venv instead of bundletester's venv feature. 6 | virtualenv: False 7 | # Leave makefile empty, otherwise unit/lint tests will rerun ahead of amulet. 8 | makefile: [] 9 | # Do not specify juju PPA sources. Juju is presumed to be pre-installed 10 | # and configured in all test runner environments. 11 | #sources: 12 | # Do not specify or rely on system packages. 13 | #packages: 14 | # Do not specify python packages here. Use test-requirements.txt 15 | # and tox instead. ie. The venv is constructed before bundletester 16 | # is invoked. 17 | #python-packages: 18 | reset_timeout: 600 19 | -------------------------------------------------------------------------------- /src/actions/run-tempest.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/sbin/charm-env python3 2 | import sys 3 | sys.path.append('lib') 4 | 5 | # Make sure that reactive is bootstrapped and all the states are setup 6 | # properly 7 | from charms.layer import basic 8 | basic.bootstrap_charm_deps() 9 | basic.init_config_states() 10 | 11 | import charm.openstack.tempest as tempest 12 | import charms.reactive.relations as relations 13 | import charmhelpers.core.hookenv as hookenv 14 | 15 | 16 | if __name__ == '__main__': 17 | identity_int = relations.endpoint_from_flag('identity-admin.available') 18 | if identity_int is None: 19 | # The interface isn't connected, so we can't do this yet 20 | hookenv.action_fail( 21 | "The identity-admin interface is not available - bailing") 22 | else: 23 | tempest.render_configs([identity_int]) 24 | tempest.run_test('smoke') 25 | -------------------------------------------------------------------------------- /src/test-requirements.txt: -------------------------------------------------------------------------------- 1 | # This file is managed centrally. If you find the need to modify this as a 2 | # one-off, please don't. Intead, consult #openstack-charms and ask about 3 | # requirements management in charms via bot-control. Thank you. 4 | charm-tools>=2.4.4 5 | coverage>=3.6 6 | mock>=1.2 7 | flake8>=2.2.4,<=2.4.1 8 | stestr>=2.2.0 9 | requests>=2.18.4 10 | # BEGIN: Amulet OpenStack Charm Helper Requirements 11 | # Liberty client lower constraints 12 | amulet>=1.14.3,<2.0;python_version=='2.7' 13 | bundletester>=0.6.1,<1.0;python_version=='2.7' 14 | aodhclient>=0.1.0 15 | gnocchiclient>=3.1.0,<3.2.0 16 | python-barbicanclient>=4.0.1 17 | python-ceilometerclient>=1.5.0 18 | python-cinderclient>=1.4.0,<5.0.0 19 | python-designateclient>=1.5 20 | python-glanceclient>=1.1.0 21 | python-heatclient>=0.8.0 22 | python-keystoneclient>=1.7.1 23 | python-manilaclient>=1.8.1 24 | python-neutronclient>=3.1.0 25 | python-novaclient>=2.30.1 26 | python-openstackclient>=1.7.0 27 | python-swiftclient>=2.6.0 28 | pika>=0.10.0,<1.0 29 | distro-info 30 | git+https://github.com/juju/charm-helpers.git#egg=charmhelpers 31 | # END: Amulet OpenStack Charm Helper Requirements 32 | pytz # workaround for 14.04 pip/tox 33 | pyudev # for ceph-* charm unit tests (not mocked?) 34 | -------------------------------------------------------------------------------- /unit_tests/test_tempest_handlers.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Canonical Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import reactive.tempest_handlers as handlers 16 | import charms_openstack.test_utils as test_utils 17 | 18 | 19 | class TestRegisteredHooks(test_utils.TestRegisteredHooks): 20 | 21 | def test_hooks(self): 22 | defaults = [] 23 | hook_set = { 24 | 'when': { 25 | 'install_packages': ('charm.installed',), 26 | 'assess_status': ('charm.installed',), 27 | } 28 | } 29 | # test that the hooks were registered via the 30 | # reactive.barbican_handlers 31 | self.registered_hooks_test_helper(handlers, hook_set, defaults) 32 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | > **WARNING**: DO NOT USE OR CONTRIBUTE. 4 | > [THIS CHARM IS DEPRECATED](https://docs.openstack.org/charm-guide/latest/openstack-charms.html). 5 | 6 | This charm exists to provide an example integration of Tempest, for the purpose 7 | of test and reference. It is not intended for production use in any case. 8 | 9 | Tempest is a set of integration tests to be run against a live OpenStack 10 | cluster. Tempest has batteries of tests for OpenStack API validation, 11 | Scenarios, and other specific tests useful in validating an OpenStack 12 | deployment. 13 | 14 | The Tempest Charm can be deployed into a new or existing Juju model containing 15 | an OpenStack deployment to execute sets or subsets of Tempest tests. 16 | 17 | # Usage 18 | 19 | NOTICE: At this time, the Tempest charm is in development and is in a 20 | proof-of-concept alpha state. 21 | 22 | Development and related discussion occurs on the OFTC #openstack-charms IRC 23 | channel. 24 | 25 | TLDR: Deploy the built charm and relate it to keystone and openstack-dashboard. 26 | See config.yaml as annotated. 27 | 28 | More docs to come as this matures. 29 | 30 | Executing the run-tempest action: 31 | 32 | juju run-action tempest/0 run-tempest --wait 33 | 34 | # Contact Information 35 | 36 | See the [OpenStack Charm Guide](http://docs.openstack.org/developer/charm-guide/) 37 | or discuss on OFTC IRC: #openstack-charms 38 | -------------------------------------------------------------------------------- /src/hooks/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | # Wrapper to deal with newer Ubuntu versions that don't have py2 installed 3 | # by default. 4 | 5 | check_and_install() { 6 | pkg="${1}" 7 | if ! dpkg -s ${pkg} 2>&1 > /dev/null; then 8 | apt-get -y install ${pkg} 9 | fi 10 | } 11 | 12 | if [[ "$(lsb_release -sc)" == "trusty" ]]; then 13 | juju-log "Enabling cloud archive to work around old trusty tools" 14 | # Add a random cloud archive for the Openstack python3 clients 15 | add-apt-repository --yes ppa:ubuntu-cloud-archive/mitaka-staging 16 | apt-get update 17 | check_and_install 'python3-pip' 18 | # The trusty version of tox is too low (tox version is 1.6, required is at least 2.3.1) 19 | # pip install tox to get around this and die a little inside 20 | pip3 install tox 21 | else 22 | juju-log "Installing tox" 23 | check_and_install 'tox' 24 | fi 25 | 26 | declare -a DEPS=('libssl-dev' 'libffi-dev' 'apt' 'python3-netaddr' 'python3-netifaces' 'python3-pip' 'python3-yaml' 'python-cinderclient' 'python-glanceclient' 'python-heatclient' 'python-keystoneclient' 'python-neutronclient' 'python-novaclient' 'python-swiftclient' 'python-ceilometerclient' 'openvswitch-test' 'python3-cinderclient' 'python3-glanceclient' 'python3-heatclient' 'python3-keystoneclient' 'python3-neutronclient' 'python3-novaclient' 'python3-swiftclient' 'python3-ceilometerclient') 27 | 28 | 29 | PYTHON="python" 30 | 31 | for dep in ${DEPS[@]}; do 32 | check_and_install ${dep} 33 | done 34 | 35 | exec ./hooks/install.real 36 | -------------------------------------------------------------------------------- /src/config.yaml: -------------------------------------------------------------------------------- 1 | options: 2 | tempest-source: 3 | type: string 4 | default: "https://github.com/openstack/tempest" 5 | description: "Location to pull tempest source from" 6 | result-format: 7 | type: string 8 | default: "Default Value" 9 | description: "Format to return results in" 10 | glance-image-name: 11 | type: string 12 | default: "cirros" 13 | description: "Main image to use" 14 | glance-alt-image-name: 15 | type: string 16 | default: "precise" 17 | description: "Alt image to use" 18 | flavor-name: 19 | type: string 20 | default: "m1.small" 21 | description: "Main image to use" 22 | flavor-alt-name: 23 | type: string 24 | default: "m1.medium" 25 | description: "Alt image to use" 26 | image-ssh-user: 27 | type: string 28 | default: "cirros" 29 | description: "User to connect to main image as" 30 | image-alt-ssh-user: 31 | type: string 32 | default: "ubuntu" 33 | description: "User to connect to alt image as" 34 | router-name: 35 | type: string 36 | default: "provider-router" 37 | description: "neutron Router" 38 | network-name: 39 | type: string 40 | default: "ext_net" 41 | description: "neutron network" 42 | floating-network-name: 43 | type: string 44 | default: "ext_net" 45 | description: "floating IP network" 46 | swift-resource-ip: 47 | type: string 48 | default: 49 | description: "undercloud swift ip" 50 | cidr-priv: 51 | type: string 52 | default: "192.168.21.0/24" 53 | description: "cidr priv" 54 | name-server: 55 | type: string 56 | default: 57 | description: "name server" 58 | http-proxy: 59 | type: string 60 | default: '' 61 | description: "http proxy address" 62 | https-proxy: 63 | type: string 64 | default: '' 65 | description: "https proxy address" 66 | pip-index-url: 67 | type: string 68 | default: '' 69 | description: "Base URL of Python Package Index" 70 | -------------------------------------------------------------------------------- /unit_tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Canonical Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import mock 16 | import sys 17 | 18 | sys.path.append('src') 19 | sys.path.append('src/lib') 20 | 21 | # Mock out charmhelpers so that we can test without it. 22 | import charms_openstack.test_mocks # noqa 23 | charms_openstack.test_mocks.mock_charmhelpers() 24 | 25 | 26 | # Mock out OpenStack clients the tempest charm imports so they are not 27 | # required for testing. 28 | glanceclient = mock.MagicMock() 29 | sys.modules['glanceclient'] = glanceclient 30 | 31 | keystoneauth1 = mock.MagicMock() 32 | sys.modules['keystoneauth1'] = keystoneauth1 33 | sys.modules['keystoneauth1.identity'] = keystoneauth1.identity 34 | sys.modules['keystoneauth1.identity.v1'] = keystoneauth1.identity.v1 35 | sys.modules['keystoneauth1.identity.v2'] = keystoneauth1.identity.v2 36 | sys.modules['keystoneauth1.session'] = keystoneauth1.session 37 | 38 | keystoneclient = mock.MagicMock() 39 | sys.modules['keystoneclient'] = keystoneclient 40 | sys.modules['keystoneclient.auth'] = keystoneclient.auth 41 | sys.modules['keystoneclient.auth.identity'] = keystoneclient.auth.identity 42 | sys.modules['keystoneclient.auth.identity.v3'] = ( 43 | keystoneclient.auth.identity.v3) 44 | sys.modules['keystoneclient.v2_0'] = keystoneclient.v2_0 45 | sys.modules['keystoneclient.v2_0.client'] = keystoneclient.v2_0.client 46 | sys.modules['keystoneclient.v3'] = keystoneclient.v3 47 | sys.modules['keystoneclient.v3.client'] = keystoneclient.v3.client 48 | sys.modules['keystoneclient.session'] = keystoneclient.session 49 | 50 | neutronclient = mock.MagicMock() 51 | sys.modules['neutronclient'] = neutronclient 52 | sys.modules['neutronclient.v2_0'] = neutronclient.v2_0 53 | sys.modules['neutronclient.v2_0.client'] = neutronclient.v2_0.client 54 | 55 | novaclient = mock.MagicMock() 56 | sys.modules['novaclient'] = novaclient 57 | sys.modules['novaclient.client'] = novaclient.client 58 | -------------------------------------------------------------------------------- /src/tox.ini: -------------------------------------------------------------------------------- 1 | # Source charm (with amulet): ./src/tox.ini 2 | # This file is managed centrally by release-tools and should not be modified 3 | # within individual charm repos. See the 'global' dir contents for available 4 | # choices of tox.ini for OpenStack Charms: 5 | # https://github.com/openstack-charmers/release-tools 6 | 7 | [tox] 8 | envlist = pep8 9 | skipsdist = True 10 | # NOTE: Avoid build/test env pollution by not enabling sitepackages. 11 | sitepackages = False 12 | # NOTE: Avoid false positives by not skipping missing interpreters. 13 | skip_missing_interpreters = False 14 | # NOTES: 15 | # * We avoid the new dependency resolver by pinning pip < 20.3, see 16 | # https://github.com/pypa/pip/issues/9187 17 | # * Pinning dependencies requires tox >= 3.2.0, see 18 | # https://tox.readthedocs.io/en/latest/config.html#conf-requires 19 | # * It is also necessary to pin virtualenv as a newer virtualenv would still 20 | # lead to fetching the latest pip in the func* tox targets, see 21 | # https://stackoverflow.com/a/38133283 22 | requires = pip < 20.3 23 | virtualenv < 20.0 24 | # NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci 25 | minversion = 3.2.0 26 | 27 | [testenv] 28 | setenv = VIRTUAL_ENV={envdir} 29 | PYTHONHASHSEED=0 30 | CHARM_DIR={envdir} 31 | AMULET_SETUP_TIMEOUT=5400 32 | whitelist_externals = juju 33 | passenv = HOME TERM AMULET_* CS_* OS_* TEST_* 34 | deps = -r{toxinidir}/test-requirements.txt 35 | install_command = 36 | pip install {opts} {packages} 37 | 38 | [testenv:pep8] 39 | basepython = python3 40 | commands = charm-proof 41 | 42 | [testenv:func-noop] 43 | # DRY RUN - For Debug 44 | basepython = python2.7 45 | commands = 46 | bundletester -vl DEBUG -r json -o func-results.json --test-pattern "gate-*" -n --no-destroy 47 | 48 | [testenv:func] 49 | # Run all gate tests which are +x (expected to always pass) 50 | basepython = python2.7 51 | commands = 52 | bundletester -vl DEBUG -r json -o func-results.json --test-pattern "gate-*" --no-destroy 53 | 54 | [testenv:func-smoke] 55 | # Run a specific test as an Amulet smoke test (expected to always pass) 56 | basepython = python2.7 57 | commands = 58 | bundletester -vl DEBUG -r json -o func-results.json gate-basic-bionic-stein --no-destroy 59 | 60 | [testenv:func-dev] 61 | # Run all development test targets which are +x (may not always pass!) 62 | basepython = python2.7 63 | commands = 64 | bundletester -vl DEBUG -r json -o func-results.json --test-pattern "dev-*" --no-destroy 65 | 66 | [testenv:venv] 67 | commands = {posargs} 68 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Source charm: ./tox.ini 2 | # This file is managed centrally by release-tools and should not be modified 3 | # within individual charm repos. See the 'global' dir contents for available 4 | # choices of tox.ini for OpenStack Charms: 5 | # https://github.com/openstack-charmers/release-tools 6 | 7 | [tox] 8 | skipsdist = True 9 | envlist = pep8,py3 10 | # NOTE: Avoid build/test env pollution by not enabling sitepackages. 11 | sitepackages = False 12 | # NOTE: Avoid false positives by not skipping missing interpreters. 13 | skip_missing_interpreters = False 14 | 15 | [testenv] 16 | setenv = VIRTUAL_ENV={envdir} 17 | PYTHONHASHSEED=0 18 | TERM=linux 19 | LAYER_PATH={toxinidir}/layers 20 | INTERFACE_PATH={toxinidir}/interfaces 21 | JUJU_REPOSITORY={toxinidir}/build 22 | passenv = http_proxy https_proxy INTERFACE_PATH LAYER_PATH JUJU_REPOSITORY 23 | install_command = 24 | pip install {opts} {packages} 25 | deps = 26 | -r{toxinidir}/requirements.txt 27 | 28 | [testenv:build] 29 | basepython = python3 30 | commands = 31 | charm-build --log-level DEBUG -o {toxinidir}/build src {posargs} 32 | 33 | [testenv:py3] 34 | basepython = python3 35 | deps = -r{toxinidir}/test-requirements.txt 36 | commands = stestr run --slowest {posargs} 37 | 38 | [testenv:py35] 39 | basepython = python3.5 40 | deps = -r{toxinidir}/test-requirements.txt 41 | commands = stestr run --slowest {posargs} 42 | 43 | [testenv:py36] 44 | basepython = python3.6 45 | deps = -r{toxinidir}/test-requirements.txt 46 | commands = stestr run --slowest {posargs} 47 | 48 | [testenv:py37] 49 | basepython = python3.7 50 | deps = -r{toxinidir}/test-requirements.txt 51 | commands = stestr run --slowest {posargs} 52 | 53 | [testenv:py38] 54 | basepython = python3.8 55 | deps = -r{toxinidir}/test-requirements.txt 56 | commands = stestr run --slowest {posargs} 57 | 58 | [testenv:pep8] 59 | basepython = python3 60 | deps = -r{toxinidir}/test-requirements.txt 61 | commands = flake8 {posargs} src unit_tests 62 | 63 | [testenv:cover] 64 | # Technique based heavily upon 65 | # https://github.com/openstack/nova/blob/master/tox.ini 66 | basepython = python3 67 | deps = -r{toxinidir}/requirements.txt 68 | -r{toxinidir}/test-requirements.txt 69 | setenv = 70 | {[testenv]setenv} 71 | PYTHON=coverage run 72 | commands = 73 | coverage erase 74 | stestr run --slowest {posargs} 75 | coverage combine 76 | coverage html -d cover 77 | coverage xml -o cover/coverage.xml 78 | coverage report 79 | 80 | [coverage:run] 81 | branch = True 82 | concurrency = multiprocessing 83 | parallel = True 84 | source = 85 | . 86 | omit = 87 | .tox/* 88 | */charmhelpers/* 89 | unit_tests/* 90 | 91 | [testenv:venv] 92 | basepython = python3 93 | commands = {posargs} 94 | 95 | [flake8] 96 | # E402 ignore necessary for path append before sys module import in actions 97 | ignore = E402,W503,W504 98 | -------------------------------------------------------------------------------- /src/templates/tempest.conf: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | lock_path=/tmp 3 | [baremetal] 4 | {% if identity_admin.ec2_creds.access_token -%} 5 | [boto] 6 | ec2_url = {{ identity_admin.compute_info.nova_base }}:8773/services/Cloud 7 | s3_url = {{ identity_admin.compute_info.nova_base }}:3333 8 | aws_access = {{ identity_admin.ec2_creds.access_token }} 9 | aws_secret = {{ identity_admin.ec2_creds.secret_token }} 10 | {% endif -%} 11 | [cli] 12 | enabled=true 13 | timeout=60 14 | cli_dir=/usr/local/bin 15 | [compute] 16 | flavor_ref={{ identity_admin.compute_info.flavor_id }} 17 | flavor_ref_alt={{ identity_admin.compute_info.flavor_alt_id }} 18 | image_ssh_user={{ identity_admin.image_info.image_ssh_user }} 19 | image_alt_ssh_user={{ identity_admin.image_info.image_alt_ssh_user }} 20 | image_ref={{ identity_admin.image_info.image_id }} 21 | image_ref_alt={{ identity_admin.image_info.image_alt_id }} 22 | allow_tenant_isolation = true 23 | allow_tenant_reuse = true 24 | [compute-admin] 25 | [compute-feature-enabled] 26 | api_v3=false 27 | console_output=false 28 | resize=true 29 | [dashboard] 30 | dashboard_url={{ dashboard_url }}/horizon 31 | login_url={{ dashboard_url }}/horizon/auth/login/ 32 | [data_processing] 33 | [debug] 34 | [auth] 35 | default_credentials_domain_name = {{ identity_admin.keystone_info.default_credentials_domain_name }} 36 | admin_username={{ identity_admin.keystone_info.service_username }} 37 | admin_project_name={{ identity_admin.keystone_info.service_tenant_name }} 38 | admin_password={{ identity_admin.keystone_info.service_password }} 39 | admin_domain_name={{ identity_admin.keystone_info.service_user_domain_name }} 40 | 41 | [identity] 42 | admin_domain_scope=true 43 | uri=http://{{ identity_admin.keystone_info.service_hostname }}:5000/v2.0 44 | uri_v3=http://{{ identity_admin.keystone_info.service_hostname }}:5000/v3 45 | username = demo 46 | password = pass 47 | tenant_name = demo 48 | alt_username = alt_demo 49 | alt_password = secret 50 | alt_tenant_name = alt_demo 51 | admin_role = Admin 52 | auth_version=v{{ identity_admin.keystone_info.api_version }} 53 | [identity-feature-enabled] 54 | {% if identity_admin.keystone_info.api_version == '3' -%} 55 | api_v3=true 56 | api_v2=false 57 | {% else -%} 58 | api_v3=false 59 | api_v2=true 60 | {% endif -%} 61 | [image] 62 | http_image = http://{{ options.swift_undercloud_ep }}:80/swift/v1/images/cirros-0.3.3-x86_64-uec.tar.gz 63 | [image-feature-enabled] 64 | [input-scenario] 65 | [network] 66 | tenant_network_cidr={{ options.cidr_priv }} 67 | public_network_id={{ identity_admin.network_info.public_network_id }} 68 | {% if options.name_server -%} 69 | dns_servers={{ options.name_server }} 70 | {% endif -%} 71 | tenant_networks_reachable = false 72 | floating_network_name={{ identity_admin.network_info.floating_network_name }} 73 | [network-feature-enabled] 74 | ipv6=false 75 | [object-storage] 76 | accounts_quotas_available = True 77 | container_quotas_available = True 78 | [object-storage-feature-enabled] 79 | [orchestration] 80 | instance_type = m1.small 81 | keypair_name = testkey 82 | stack_owner_role = Admin 83 | [scenario] 84 | ssh_user=cirros 85 | [service_available] 86 | ceilometer = {{ identity_admin.service_info.ceilometer }} 87 | cinder = {{ identity_admin.service_info.cinder }} 88 | glance = {{ identity_admin.service_info.glance }} 89 | heat = {{ identity_admin.service_info.heat }} 90 | horizon = {{ identity_admin.service_info.horizon }} 91 | ironic = {{ identity_admin.service_info.ironic }} 92 | neutron = {{ identity_admin.service_info.neutron }} 93 | nova = {{ identity_admin.service_info.nova }} 94 | sahara = {{ identity_admin.service_info.sahara }} 95 | swift = {{ identity_admin.service_info.swift }} 96 | trove = {{ identity_admin.service_info.trove }} 97 | zaqar = {{ identity_admin.service_info.zaqar }} 98 | [stress] 99 | max_instances = 4 100 | [telemetry] 101 | [volume] 102 | storage_protocol=ceph 103 | backend1_name=cinder-ceph 104 | catalog_type = volume 105 | [volume-feature-enabled] 106 | backup=false 107 | -------------------------------------------------------------------------------- /src/tests/basic_deployment.py: -------------------------------------------------------------------------------- 1 | 2 | from charmhelpers.contrib.openstack.amulet.deployment import ( 3 | OpenStackAmuletDeployment 4 | ) 5 | 6 | from charmhelpers.contrib.openstack.amulet.utils import ( 7 | OpenStackAmuletUtils, 8 | DEBUG, 9 | ) 10 | 11 | # Use DEBUG to turn on debug logging 12 | u = OpenStackAmuletUtils(DEBUG) 13 | 14 | 15 | class TempestBasicDeployment(OpenStackAmuletDeployment): 16 | """Amulet tests on a basic tempest deployment.""" 17 | 18 | def __init__(self, series, openstack=None, source=None, stable=False): 19 | """Deploy the entire test environment.""" 20 | super(TempestBasicDeployment, self).__init__(series, openstack, 21 | source, stable) 22 | self._add_services() 23 | self._add_relations() 24 | self._configure_services() 25 | self._deploy() 26 | 27 | u.log.info('Waiting on extended status checks...') 28 | exclude_services = [] 29 | self._auto_wait_for_status(exclude_services=exclude_services) 30 | 31 | self.d.sentry.wait() 32 | self._initialize_tests() 33 | 34 | def _add_services(self): 35 | """Add services 36 | 37 | Add the services that we're testing, where tempest is local, 38 | and the rest of the service are from lp branches that are 39 | compatible with the local charm (e.g. stable or next). 40 | """ 41 | this_service = {'name': 'tempest'} 42 | other_services = [ 43 | {'name': 'percona-cluster', 'constraints': {'mem': '3072M'}}, 44 | {'name': 'rabbitmq-server'}, 45 | {'name': 'keystone'}, 46 | {'name': 'openstack-dashboard'}, 47 | {'name': 'glance'} 48 | ] 49 | super(TempestBasicDeployment, self)._add_services( 50 | this_service, 51 | other_services, 52 | no_origin=['tempest']) 53 | 54 | def _add_relations(self): 55 | """Add all of the relations for the services.""" 56 | relations = { 57 | 'keystone:identity-admin': 'tempest:identity-admin', 58 | 'tempest:dashboard': 'openstack-dashboard:website', 59 | 'openstack-dashboard:identity-service': 60 | 'keystone:identity-service', 61 | 'keystone:shared-db': 'percona-cluster:shared-db', 62 | 'glance:identity-service': 'keystone:identity-service', 63 | 'glance:shared-db': 'percona-cluster:shared-db', 64 | 'glance:amqp': 'rabbitmq-server:amqp' 65 | } 66 | super(TempestBasicDeployment, self)._add_relations(relations) 67 | 68 | def _configure_services(self): 69 | """Configure all of the services.""" 70 | pxc_config = { 71 | 'dataset-size': '25%', 72 | 'max-connections': 1000, 73 | 'root-password': 'ChangeMe123', 74 | 'sst-password': 'ChangeMe123', 75 | } 76 | configs = { 77 | 'percona-cluster': pxc_config, 78 | } 79 | super(TempestBasicDeployment, self)._configure_services(configs) 80 | 81 | def _get_token(self): 82 | return self.keystone.service_catalog.catalog['token']['id'] 83 | 84 | def _initialize_tests(self): 85 | """Perform final initialization before tests get run.""" 86 | # Access the sentries for inspecting service units 87 | self.tempest_sentry = self.d.sentry['tempest'][0] 88 | self.openstack_dashboard_sentry = \ 89 | self.d.sentry['openstack-dashboard'][0] 90 | u.log.debug('openstack release val: {}'.format( 91 | self._get_openstack_release())) 92 | u.log.debug('openstack release str: {}'.format( 93 | self._get_openstack_release_string())) 94 | 95 | def test_run_tempest(self): 96 | u.log.debug('Running Tempest...') 97 | unit = self.tempest_sentry 98 | assert u.status_get(unit)[0] == "active" 99 | 100 | action_id = u.run_action(unit, "run-tempest") 101 | assert u.wait_on_action(action_id), "run-tempest action failed." 102 | -------------------------------------------------------------------------------- /unit_tests/test_lib_charm_openstack_tempest.py: -------------------------------------------------------------------------------- 1 | # Copyright 2016 Canonical Ltd 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import io 16 | import mock 17 | 18 | import charms_openstack.test_utils as test_utils 19 | import charm.openstack.tempest as tempest 20 | import unit_tests.tempest_output 21 | 22 | 23 | class Helper(test_utils.PatchHelper): 24 | 25 | def setUp(self): 26 | super().setUp() 27 | self.patch_release(tempest.TempestCharm.release) 28 | 29 | 30 | class TestTempestAdminAdapter(test_utils.PatchHelper): 31 | 32 | def test_init(self): 33 | self.patch_object(tempest.hookenv, 'config') 34 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 35 | self.patch_object( 36 | tempest.adapters.OpenStackRelationAdapter, '__init__') 37 | tempest.TempestAdminAdapter('rel2') 38 | self.init_keystone_client.assert_called_once_with() 39 | self.__init__.assert_called_once_with('rel2') 40 | 41 | def test_init_keystone_client(self): 42 | ks_info = { 43 | 'service_hostname': 'kshost', 44 | 'service_port': '5001', 45 | 'service_username': 'user1', 46 | 'service_password': 'pass1', 47 | 'service_tenant_name': 'svc', 48 | 'service_region': 'reg1'} 49 | self.patch_object(tempest.keystoneclient_v2, 'Client') 50 | self.patch_object(tempest.hookenv, 'config') 51 | self.patch_object( 52 | tempest.adapters.OpenStackRelationAdapter, '__init__') 53 | self.patch_object( 54 | tempest.TempestAdminAdapter, 55 | 'keystone_info', 56 | new=ks_info) 57 | a = tempest.TempestAdminAdapter('rel2') 58 | a.init_keystone_client() 59 | self.Client.assert_called_once_with( 60 | auth_url='http://kshost:5001/v2.0', 61 | password='pass1', 62 | region_name='reg1', 63 | tenant_name='svc', 64 | username='user1') 65 | 66 | def test_ec2_creds(self): 67 | self.patch_object(tempest.hookenv, 'config') 68 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 69 | self.patch_object(tempest.TempestAdminAdapter, '_setup_properties') 70 | kc = mock.MagicMock() 71 | kc.user_id = 'bob' 72 | kc.ec2.list = lambda x: [{'access_token': 'ac2', 73 | 'secret_token': 'st2'}] 74 | self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) 75 | a = tempest.TempestAdminAdapter('rel2') 76 | self.assertEqual(a.ec2_creds, {'access_token': 'ac2', 77 | 'secret_token': 'st2'}) 78 | 79 | def test_image_info(self): 80 | self.patch_object(tempest.hookenv, 'config') 81 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 82 | self.patch_object(tempest.TempestAdminAdapter, 'service_present', 83 | return_value=True) 84 | self.patch_object( 85 | tempest.adapters.OpenStackRelationAdapter, '__init__') 86 | self.patch_object(tempest.TempestAdminAdapter, 'ks_client') 87 | self.patch_object(tempest.glanceclient, 'Client') 88 | self.config.return_value = { 89 | 'glance-image-name': 'img1', 90 | 'glance-alt-image-name': 'altimg', 91 | } 92 | kc = mock.MagicMock() 93 | kc.service_catalog.url_for = \ 94 | lambda service_type=None, endpoint_type=None: 'http://glance' 95 | self.ks_client.return_value = kc 96 | img1 = mock.MagicMock() 97 | img1.name = 'img1' 98 | img1.id = 'img1_id' 99 | img2 = mock.MagicMock() 100 | img2.name = 'img2' 101 | img2.id = 'img2_id' 102 | gc = mock.MagicMock() 103 | gc.images.list = lambda: [img1, img2] 104 | self.Client.return_value = gc 105 | a = tempest.TempestAdminAdapter('rel2') 106 | self.assertEqual(a.image_info, {'image_id': 'img1_id'}) 107 | 108 | def test_network_info(self): 109 | self.patch_object(tempest.hookenv, 'config') 110 | self.patch_object(tempest.TempestAdminAdapter, 'service_present', 111 | return_value=True) 112 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 113 | self.patch_object( 114 | tempest.adapters.OpenStackRelationAdapter, '__init__') 115 | self.patch_object(tempest.TempestAdminAdapter, 'ks_client') 116 | self.patch_object(tempest.neutronclient, 'Client') 117 | router1 = {'id': '16'} 118 | net1 = {'id': 'pubnet1'} 119 | kc = mock.MagicMock() 120 | kc.service_catalog.url_for = \ 121 | lambda service_type=None, endpoint_type=None: 'http://neutron' 122 | self.ks_client.return_value = kc 123 | self.config.return_value = { 124 | 'router-name': 'route1', 125 | 'floating-network-name': 'ext_net', 126 | 'network-name': 'net1'} 127 | nc = mock.MagicMock() 128 | nc.list_routers = lambda name=None: {'routers': [router1]} 129 | nc.list_networks = lambda name=None: {'networks': [net1]} 130 | self.Client.return_value = nc 131 | a = tempest.TempestAdminAdapter('rel2') 132 | self.assertEqual( 133 | a.network_info, 134 | {'floating_network_name': 'ext_net', 135 | 'public_network_id': 'pubnet1', 136 | 'router_id': '16'}) 137 | 138 | def test_compute_info(self): 139 | self.patch_object(tempest.hookenv, 'config') 140 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 141 | self.patch_object(tempest.TempestAdminAdapter, 'service_present', 142 | return_value=True) 143 | self.patch_object( 144 | tempest.adapters.OpenStackRelationAdapter, '__init__') 145 | ki = { 146 | 'service_username': 'user', 147 | 'service_password': 'pass', 148 | 'service_tenant_name': 'ten', 149 | } 150 | self.patch_object( 151 | tempest.TempestAdminAdapter, 152 | 'keystone_info', 153 | new=ki) 154 | self.patch_object( 155 | tempest.TempestAdminAdapter, 156 | 'keystone_auth_url', 157 | return_value='auth_url') 158 | self.config.return_value = { 159 | 'flavor-alt-name': None, 160 | 'flavor-name': 'm3.huuge'} 161 | kc = mock.MagicMock() 162 | kc.service_catalog.url_for = \ 163 | lambda service_type=None, endpoint_type=None: 'http://nova:999/bob' 164 | self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) 165 | self.patch_object(tempest.novaclient_client, 'Client') 166 | _flavor1 = mock.MagicMock() 167 | _flavor1.name = 'm3.huuge' 168 | _flavor1.id = 'id1' 169 | nc = mock.MagicMock() 170 | nc.flavors.list = lambda: [_flavor1] 171 | self.Client.return_value = nc 172 | a = tempest.TempestAdminAdapter('rel2') 173 | self.assertEqual( 174 | a.compute_info, 175 | { 176 | 'flavor_id': 'id1', 177 | 'nova_base': 'http://nova'}) 178 | 179 | def test_get_present_services(self): 180 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 181 | self.patch_object( 182 | tempest.adapters.OpenStackRelationAdapter, '__init__') 183 | kc = mock.MagicMock() 184 | svc1 = mock.Mock() 185 | svc2 = mock.Mock() 186 | svc3 = mock.Mock() 187 | svc1.name = 'compute' 188 | svc1.enabled = True 189 | svc2.name = 'image' 190 | svc2.enabled = False 191 | svc3.name = 'network' 192 | svc3.enabled = True 193 | svcs = [svc1, svc2, svc3] 194 | kc.services.list = lambda: svcs 195 | self.patch_object(tempest.TempestAdminAdapter, 'ks_client', new=kc) 196 | a = tempest.TempestAdminAdapter('rel2') 197 | self.assertEqual( 198 | a.get_present_services(), 199 | ['compute', 'network']) 200 | 201 | def test_service_info(self): 202 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 203 | self.patch_object( 204 | tempest.adapters.OpenStackRelationAdapter, '__init__') 205 | self.patch_object(tempest.TempestAdminAdapter, 'get_present_services') 206 | self.get_present_services.return_value = ['cinder', 'glance'] 207 | self.patch_object(tempest.hookenv, 'action_get') 208 | self.action_get.return_value = { 209 | 'service-whitelist': 'swift glance'} 210 | a = tempest.TempestAdminAdapter('rel2') 211 | self.assertEqual( 212 | a.service_info, 213 | { 214 | 'ceilometer': 'false', 215 | 'cinder': 'false', 216 | 'glance': 'true', 217 | 'heat': 'false', 218 | 'horizon': 'false', 219 | 'ironic': 'false', 220 | 'neutron': 'false', 221 | 'nova': 'false', 222 | 'sahara': 'false', 223 | 'swift': 'true', 224 | 'trove': 'false', 225 | 'zaqar': 'false', 226 | 'neutron': 'false'}) 227 | 228 | def test_service_info_auto(self): 229 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 230 | self.patch_object( 231 | tempest.adapters.OpenStackRelationAdapter, '__init__') 232 | self.patch_object(tempest.TempestAdminAdapter, 'get_present_services') 233 | self.get_present_services.return_value = ['cinder', 'glance'] 234 | self.patch_object(tempest.hookenv, 'action_get') 235 | self.action_get.return_value = { 236 | 'service-whitelist': 'auto'} 237 | a = tempest.TempestAdminAdapter('rel2') 238 | self.assertEqual( 239 | a.service_info, 240 | { 241 | 'ceilometer': 'false', 242 | 'cinder': 'true', 243 | 'glance': 'true', 244 | 'heat': 'false', 245 | 'horizon': 'false', 246 | 'ironic': 'false', 247 | 'neutron': 'false', 248 | 'nova': 'false', 249 | 'sahara': 'false', 250 | 'swift': 'false', 251 | 'trove': 'false', 252 | 'zaqar': 'false', 253 | 'neutron': 'false'}) 254 | 255 | def test_service_present(self): 256 | self.patch_object(tempest.TempestAdminAdapter, 'init_keystone_client') 257 | self.patch_object( 258 | tempest.adapters.OpenStackRelationAdapter, '__init__') 259 | self.patch_object(tempest.TempestAdminAdapter, 'get_present_services', 260 | return_value=['svc1', 'svc2']) 261 | a = tempest.TempestAdminAdapter('rel2') 262 | self.assertTrue(a.service_present('svc1')) 263 | self.assertFalse(a.service_present('svc3')) 264 | 265 | 266 | class TestTempestCharm(Helper): 267 | 268 | def test_check_tox_installed_trusty(self): 269 | # Test for Bug #1648493 270 | self.patch_object(tempest.host, 'lsb_release') 271 | self.lsb_release.return_value = {'DISTRIB_RELEASE': '14.04'} 272 | self.assertTrue('python-tox' in tempest.TempestCharm().all_packages) 273 | 274 | def test_check_tox_installed_xenial(self): 275 | # Test for Bug #1648493 276 | self.patch_object(tempest.host, 'lsb_release') 277 | self.lsb_release.return_value = {'DISTRIB_RELEASE': '16.04'} 278 | self.assertTrue('tox' in tempest.TempestCharm().all_packages) 279 | 280 | def test_setup_directories(self): 281 | self.patch_object(tempest.os.path, 'exists') 282 | self.patch_object(tempest.os, 'mkdir') 283 | self.exists.return_value = False 284 | c = tempest.TempestCharm() 285 | c.setup_directories() 286 | calls = [ 287 | mock.call('/var/lib/tempest'), 288 | mock.call('/var/lib/tempest/logs') 289 | ] 290 | self.mkdir.assert_has_calls(calls) 291 | 292 | def test_setup_git(self): 293 | self.patch_object(tempest.hookenv, 'config') 294 | self.patch_object(tempest.os.path, 'exists') 295 | self.patch_object(tempest.os, 'symlink') 296 | self.patch_object(tempest.fetch, 'install_remote') 297 | self.config.return_value = {'tempest-source': 'git_url'} 298 | self.exists.return_value = False 299 | c = tempest.TempestCharm() 300 | c.setup_git('git_branch', 'git_dir') 301 | self.install_remote.assert_called_once_with( 302 | 'git_url', 303 | branch='git_branch', 304 | depth='1', 305 | dest='git_dir') 306 | self.symlink.assert_called_once_with( 307 | '/var/lib/tempest/tempest.conf', 308 | 'git_dir/tempest/etc/tempest.conf') 309 | 310 | def test_setup_git_noop(self): 311 | self.patch_object(tempest.hookenv, 'config') 312 | self.patch_object(tempest.os.path, 'exists') 313 | self.config.return_value = {'tempest-source': 'git_url'} 314 | self.patch_object(tempest.os, 'symlink') 315 | self.patch_object(tempest.fetch, 'install_remote') 316 | self.exists.return_value = True 317 | c = tempest.TempestCharm() 318 | c.setup_git('git_branch', 'git_dir') 319 | self.assertFalse(self.install_remote.called) 320 | self.assertFalse(self.symlink.called) 321 | 322 | def test_execute_tox(self): 323 | # XXX env seems unused 324 | self.patch_object(tempest.hookenv, 'config') 325 | self.patch_object(tempest.os.environ, 'copy') 326 | self.patch_object(tempest.subprocess, 'call') 327 | os_env = mock.MagicMock() 328 | self.copy.return_value = os_env 329 | self.config.return_value = { 330 | 'http-proxy': 'http://proxy', 331 | 'https-proxy': 'https://proxy', 332 | } 333 | with mock.patch("builtins.open", return_value="fhandle"): 334 | c = tempest.TempestCharm() 335 | c.execute_tox('/tmp/run', '/tmp/t.log', 'py38') 336 | self.call.assert_called_with( 337 | ['tox', '-e', 'py38'], 338 | cwd='/tmp/run', 339 | stderr='fhandle', 340 | stdout='fhandle', 341 | env=os_env) 342 | 343 | def test_get_tempest_files(self): 344 | self.patch_object(tempest.time, 'strftime') 345 | self.strftime.return_value = 'teatime' 346 | c = tempest.TempestCharm() 347 | self.assertEqual( 348 | c.get_tempest_files('br1'), 349 | ('/var/lib/tempest/tempest-br1', 350 | '/var/lib/tempest/logs/run_teatime.log', 351 | '/var/lib/tempest/tempest-br1/tempest')) 352 | 353 | def test_parse_tempest_log(self): 354 | _log_contents = io.StringIO(unit_tests.tempest_output.TEMPEST_OUT) 355 | expect = { 356 | 'expected-fail': '0', 357 | 'failed': '0', 358 | 'passed': '21', 359 | 'skipped': '41', 360 | 'unexpected-success': '0'} 361 | with mock.patch("builtins.open", return_value=_log_contents): 362 | c = tempest.TempestCharm() 363 | self.assertEqual(c.parse_tempest_log("logfile"), expect) 364 | 365 | def test_run_test(self): 366 | self.patch_object(tempest.hookenv, 'action_set') 367 | self.patch_object(tempest.hookenv, 'action_get') 368 | self.action_get.return_value = { 369 | 'branch': 'br1'} 370 | self.patch_object(tempest.TempestCharm, 'get_tempest_files') 371 | self.patch_object(tempest.TempestCharm, 'setup_directories') 372 | self.patch_object(tempest.TempestCharm, 'setup_git') 373 | self.patch_object(tempest.TempestCharm, 'execute_tox') 374 | self.patch_object(tempest.TempestCharm, 'parse_tempest_log') 375 | self.get_tempest_files.return_value = ( 376 | 'git_dir1', 377 | '/var/log/t.log', 378 | '/var/tempest/run') 379 | self.parse_tempest_log.return_value = {'run_info': 'OK'} 380 | c = tempest.TempestCharm() 381 | c.run_test('py39') 382 | self.action_set.assert_called_once_with( 383 | {'run_info': 'OK', 'tempest-logfile': '/var/log/t.log'}) 384 | -------------------------------------------------------------------------------- /src/lib/charm/openstack/tempest.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import subprocess 4 | import time 5 | import urllib 6 | 7 | import glanceclient 8 | import keystoneauth1 9 | import keystoneauth1.identity.v2 as keystoneauth1_v2 10 | import keystoneauth1.session as keystoneauth1_session 11 | import keystoneclient.v2_0.client as keystoneclient_v2 12 | import keystoneclient.v3.client as keystoneclient_v3 13 | import keystoneclient.auth.identity.v3 as keystone_id_v3 14 | import keystoneclient.session as session 15 | import neutronclient.v2_0.client as neutronclient 16 | import novaclient.client as novaclient_client 17 | 18 | import charms_openstack.charm as charm 19 | import charms_openstack.adapters as adapters 20 | import charmhelpers.core.hookenv as hookenv 21 | import charmhelpers.core.host as host 22 | import charmhelpers.fetch as fetch 23 | 24 | 25 | def install(): 26 | """Use the singleton from the TempestCharm to install the packages on the 27 | unit 28 | """ 29 | TempestCharm.singleton.install() 30 | 31 | 32 | def render_configs(interfaces_list): 33 | """Using a list of interfaces, render the configs and, if they have 34 | changes, restart the services on the unit. 35 | """ 36 | if not os.path.isdir(TempestCharm.TEMPEST_LOGDIR): 37 | os.makedirs(TempestCharm.TEMPEST_LOGDIR) 38 | TempestCharm.singleton.render_with_interfaces(interfaces_list) 39 | TempestCharm.singleton.assess_status() 40 | 41 | 42 | def run_test(tox_target): 43 | """Use the singleton from the TempestCharm to install the packages on the 44 | unit 45 | """ 46 | TempestCharm.singleton.run_test(tox_target) 47 | 48 | 49 | def assess_status(): 50 | """Use the singleton from the TempestCharm to install the packages on the 51 | unit 52 | """ 53 | TempestCharm.singleton.assess_status() 54 | 55 | 56 | class TempestAdminAdapter(adapters.OpenStackRelationAdapter): 57 | 58 | """Inspect relations and provide properties that can be used when 59 | rendering templates""" 60 | 61 | interface_type = "identity-admin" 62 | 63 | def __init__(self, relation): 64 | """Initialise a keystone client and collect user defined config""" 65 | self.kc = None 66 | self.keystone_session = None 67 | self.api_version = '2' 68 | super(TempestAdminAdapter, self).__init__(relation) 69 | self.init_keystone_client() 70 | self.uconfig = hookenv.config() 71 | 72 | @property 73 | def keystone_info(self): 74 | """Collection keystone information from keystone relation""" 75 | ks_info = self.relation.credentials() 76 | ks_info['default_credentials_domain_name'] = 'default' 77 | if ks_info.get('api_version'): 78 | ks_info['api_version'] = ks_info.get('api_version') 79 | else: 80 | ks_info['api_version'] = self.api_version 81 | if not ks_info.get('service_user_domain_name'): 82 | ks_info['service_user_domain_name'] = 'admin_domain' 83 | 84 | return ks_info 85 | 86 | @property 87 | def ks_client(self): 88 | if not self.kc: 89 | self.init_keystone_client() 90 | return self.kc 91 | 92 | def keystone_auth_url(self, api_version=None): 93 | if not api_version: 94 | api_version = self.keystone_info.get('api_version', '2') 95 | ep_suffix = { 96 | '2': 'v2.0', 97 | '3': 'v3'}[api_version] 98 | return '{}://{}:{}/{}'.format( 99 | 'http', 100 | self.keystone_info['service_hostname'], 101 | self.keystone_info['service_port'], 102 | ep_suffix, 103 | ) 104 | 105 | def resolve_endpoint(self, service_type, interface): 106 | if self.api_version == '2': 107 | ep = self.ks_client.service_catalog.url_for( 108 | service_type=service_type, 109 | endpoint_type='{}URL'.format(interface) 110 | ) 111 | else: 112 | svc_id = self.ks_client.services.find(type=service_type).id 113 | ep = self.ks_client.endpoints.find( 114 | service_id=svc_id, 115 | interface=interface).url 116 | return ep 117 | 118 | def set_keystone_v2_client(self): 119 | self.keystone_session = None 120 | self.kc = keystoneclient_v2.Client(**self.admin_creds_v2) 121 | 122 | def set_keystone_v3_client(self): 123 | auth = keystone_id_v3.Password(**self.admin_creds_v3) 124 | self.keystone_session = session.Session(auth=auth) 125 | self.kc = keystoneclient_v3.Client(session=self.keystone_session) 126 | 127 | def init_keystone_client(self): 128 | """Initialise keystone client""" 129 | if self.kc: 130 | return 131 | if self.keystone_info.get('api_version', '2') > '2': 132 | self.set_keystone_v3_client() 133 | self.api_version = '3' 134 | else: 135 | # XXX Temporarily catching the Unauthorized exception to deal with 136 | # the case (pre-17.02) where the keystone charm maybe in v3 mode 137 | # without telling charms via the identity-admin relation 138 | try: 139 | self.set_keystone_v2_client() 140 | self.api_version = '2' 141 | except keystoneauth1.exceptions.http.Unauthorized: 142 | self.set_keystone_v3_client() 143 | self.api_version = '3' 144 | self.kc.services.list() 145 | 146 | def admin_creds_base(self, api_version): 147 | return { 148 | 'username': self.keystone_info['service_username'], 149 | 'password': self.keystone_info['service_password'], 150 | 'auth_url': self.keystone_auth_url(api_version=api_version)} 151 | 152 | @property 153 | def admin_creds_v2(self): 154 | creds = self.admin_creds_base(api_version='2') 155 | creds['tenant_name'] = self.keystone_info['service_tenant_name'] 156 | creds['region_name'] = self.keystone_info['service_region'] 157 | return creds 158 | 159 | @property 160 | def admin_creds_v3(self): 161 | creds = self.admin_creds_base(api_version='3') 162 | creds['project_name'] = self.keystone_info.get( 163 | 'service_project_name', 164 | 'admin') 165 | creds['user_domain_name'] = self.keystone_info.get( 166 | 'service_user_domain_name', 167 | 'admin_domain') 168 | creds['project_domain_name'] = self.keystone_info.get( 169 | 'service_project_domain_name', 170 | 'Default') 171 | return creds 172 | 173 | @property 174 | def ec2_creds(self): 175 | """Generate EC2 style tokens or return existing EC2 tokens 176 | 177 | @returns {'access_token' token1, 'secret_token': token2} 178 | """ 179 | _ec2creds = {} 180 | if self.api_version == '2': 181 | current_creds = self.ks_client.ec2.list(self.ks_client.user_id) 182 | if current_creds: 183 | _ec2creds = current_creds[0] 184 | else: 185 | creds = self.ks_client.ec2.create( 186 | self.ks_client.user_id, 187 | self.ks_client.tenant_id) 188 | _ec2creds = { 189 | 'access_token': creds.access, 190 | 'secret_token': creds.secret} 191 | return _ec2creds 192 | 193 | @property 194 | def image_info(self): 195 | """Return image ids for the user-defined image names 196 | 197 | @returns {'image_id' id1, 'image_alt_id': id2} 198 | """ 199 | image_info = {} 200 | if self.service_present('glance'): 201 | if self.keystone_session: 202 | glance_client = glanceclient.Client( 203 | '2', session=self.keystone_session) 204 | else: 205 | glance_ep = self.resolve_endpoint('image', 'public') 206 | glance_client = glanceclient.Client( 207 | '2', glance_ep, token=self.ks_client.auth_token) 208 | for image in glance_client.images.list(): 209 | if self.uconfig.get('glance-image-name') == image.name: 210 | image_info['image_id'] = image.id 211 | if self.uconfig.get('image-ssh-user'): 212 | image_info['image_ssh_user'] = \ 213 | self.uconfig.get('image-ssh-user') 214 | if self.uconfig.get('glance-alt-image-name') == image.name: 215 | image_info['image_alt_id'] = image.id 216 | if self.uconfig.get('image-alt-ssh-user'): 217 | image_info['image_alt_ssh_user'] = \ 218 | self.uconfig.get('image-alt-ssh-user') 219 | return image_info 220 | 221 | @property 222 | def network_info(self): 223 | """Return public network and router ids for user-defined router and 224 | network names 225 | 226 | @returns {'public_network_id' id1, 'router_id': id2} 227 | """ 228 | network_info = {} 229 | if self.service_present('neutron'): 230 | if self.keystone_session: 231 | neutron_client = neutronclient.Client( 232 | session=self.keystone_session) 233 | else: 234 | neutron_ep = self.ks_client.service_catalog.url_for( 235 | service_type='network', 236 | endpoint_type='publicURL') 237 | neutron_client = neutronclient.Client( 238 | endpoint_url=neutron_ep, 239 | token=self.ks_client.auth_token) 240 | routers = neutron_client.list_routers( 241 | name=self.uconfig['router-name']) 242 | if len(routers['routers']) == 0: 243 | hookenv.log("Router not found") 244 | else: 245 | router = routers['routers'][0] 246 | network_info['router_id'] = router['id'] 247 | networks = neutron_client.list_networks( 248 | name=self.uconfig['network-name']) 249 | if len(networks['networks']) == 0: 250 | hookenv.log("network not found") 251 | else: 252 | network = networks['networks'][0] 253 | network_info['public_network_id'] = network['id'] 254 | networks = neutron_client.list_networks( 255 | name=self.uconfig['floating-network-name']) 256 | if len(networks['networks']) == 0: 257 | hookenv.log("Floating network name not found") 258 | else: 259 | network_info['floating_network_name'] = \ 260 | self.uconfig['floating-network-name'] 261 | return network_info 262 | 263 | def service_present(self, service): 264 | """Check if a given service type is registered in the catalogue 265 | 266 | :params service: string Service type 267 | @returns Boolean: True if service is registered 268 | """ 269 | return service in self.get_present_services() 270 | 271 | def get_nova_client(self): 272 | if not self.keystone_session: 273 | auth = keystoneauth1_v2.Password( 274 | auth_url=self.keystone_auth_url(), 275 | username=self.keystone_info['service_username'], 276 | password=self.keystone_info['service_password'], 277 | tenant_name=self.keystone_info['service_tenant_name']) 278 | self.keystone_session = keystoneauth1_session.Session(auth=auth) 279 | return novaclient_client.Client( 280 | 2, session=self.keystone_session) 281 | 282 | @property 283 | def compute_info(self): 284 | """Return flavor ids for user-defined flavors 285 | 286 | @returns {'flavor_id' id1, 'flavor_alt_id': id2} 287 | """ 288 | compute_info = {} 289 | if self.service_present('nova'): 290 | nova_client = self.get_nova_client() 291 | nova_ep = self.resolve_endpoint('compute', 'public') 292 | url = urllib.parse.urlparse(nova_ep) 293 | compute_info['nova_base'] = '{}://{}'.format( 294 | url.scheme, 295 | url.netloc.split(':')[0]) 296 | for flavor in nova_client.flavors.list(): 297 | if self.uconfig['flavor-name'] == flavor.name: 298 | compute_info['flavor_id'] = flavor.id 299 | if self.uconfig['flavor-alt-name'] == flavor.name: 300 | compute_info['flavor_alt_id'] = flavor.id 301 | return compute_info 302 | 303 | def get_present_services(self): 304 | """Query keystone catalogue for a list for registered services 305 | 306 | @returns [svc1, svc2, ...]: List of registered services 307 | """ 308 | services = [svc.name 309 | for svc in self.ks_client.services.list() 310 | if svc.enabled] 311 | return services 312 | 313 | @property 314 | def service_info(self): 315 | """Assemble a list of services tempest should tests 316 | 317 | Compare the list of keystone registered services with the services the 318 | user has requested be tested. If in 'auto' mode test all services 319 | registered in keystone. 320 | 321 | @returns [svc1, svc2, ...]: List of services to test 322 | """ 323 | service_info = {} 324 | tempest_candidates = ['ceilometer', 'cinder', 'glance', 'heat', 325 | 'horizon', 'ironic', 'neutron', 'nova', 326 | 'sahara', 'swift', 'trove', 'zaqar', 'neutron'] 327 | present_svcs = self.get_present_services() 328 | # If not running in an action context asssume auto mode 329 | try: 330 | action_args = hookenv.action_get() 331 | except Exception: 332 | action_args = {'service-whitelist': 'auto'} 333 | if action_args['service-whitelist'] == 'auto': 334 | white_list = [] 335 | for svc in present_svcs: 336 | if svc in tempest_candidates: 337 | white_list.append(svc) 338 | else: 339 | white_list = action_args['service-whitelist'] 340 | for svc in tempest_candidates: 341 | if svc in white_list: 342 | service_info[svc] = 'true' 343 | else: 344 | service_info[svc] = 'false' 345 | return service_info 346 | 347 | 348 | class TempestAdapters(adapters.OpenStackRelationAdapters): 349 | """ 350 | Adapters class for the Tempest charm. 351 | """ 352 | relation_adapters = { 353 | 'identity_admin': TempestAdminAdapter, 354 | } 355 | 356 | def __init__(self, relations): 357 | super(TempestAdapters, self).__init__( 358 | relations, 359 | options=TempestConfigurationAdapter) 360 | 361 | 362 | class TempestConfigurationAdapter(adapters.ConfigurationAdapter): 363 | """ 364 | Manipulate user supplied config as needed 365 | """ 366 | def __init__(self): 367 | super(TempestConfigurationAdapter, self).__init__() 368 | 369 | 370 | class TempestCharm(charm.OpenStackCharm): 371 | 372 | release = 'liberty' 373 | name = 'tempest' 374 | 375 | required_relations = ['identity-admin'] 376 | """Directories and files used for running tempest""" 377 | TEMPEST_ROOT = '/var/lib/tempest' 378 | TEMPEST_LOGDIR = TEMPEST_ROOT + '/logs' 379 | TEMPEST_CONF = TEMPEST_ROOT + '/tempest.conf' 380 | """pip.conf for proxy settings etc""" 381 | PIP_CONF = '/root/.pip/pip.conf' 382 | 383 | """List of packages charm should install 384 | XXX The install hook is currently installing most packages ahead of 385 | this because modules like keystoneclient are needed at load time 386 | """ 387 | packages = [ 388 | 'git', 'testrepository', 'subunit', 'python-nose', 'python-lxml', 389 | 'python-boto', 'python-junitxml', 'python-subunit', 390 | 'python-testresources', 'python-oslotest', 'python-stevedore', 391 | 'python-cinderclient', 'python-glanceclient', 'python-heatclient', 392 | 'python-keystoneclient', 'python-neutronclient', 'python-novaclient', 393 | 'python-swiftclient', 'python-ceilometerclient', 'openvswitch-test', 394 | 'python3-cinderclient', 'python3-glanceclient', 'python3-heatclient', 395 | 'python3-keystoneclient', 'python3-neutronclient', 396 | 'python3-novaclient', 'python3-swiftclient', 397 | 'python3-ceilometerclient', 'openvswitch-common', 'libffi-dev', 398 | 'libssl-dev', 'python-dev', 'python-cffi' 399 | ] 400 | 401 | """Use the Tempest specific adapters""" 402 | adapters_class = TempestAdapters 403 | """Tempest has no running services so no services need restarting on 404 | config file change 405 | """ 406 | restart_map = { 407 | TEMPEST_CONF: [], 408 | PIP_CONF: [], 409 | } 410 | 411 | @property 412 | def all_packages(self): 413 | _packages = self.packages[:] 414 | if host.lsb_release()['DISTRIB_RELEASE'] > '14.04': 415 | _packages.append('tox') 416 | else: 417 | _packages.append('python-tox') 418 | return _packages 419 | 420 | def setup_directories(self): 421 | for tempest_dir in [self.TEMPEST_ROOT, self.TEMPEST_LOGDIR]: 422 | if not os.path.exists(tempest_dir): 423 | os.mkdir(tempest_dir) 424 | 425 | def setup_git(self, branch, git_dir): 426 | """Clone tempest and symlink in rendered tempest.conf""" 427 | conf = hookenv.config() 428 | if not os.path.exists(git_dir): 429 | git_url = conf['tempest-source'] 430 | fetch.install_remote(str(git_url), dest=str(git_dir), 431 | branch=str(branch), depth=str(1)) 432 | conf_symlink = git_dir + '/tempest/etc/tempest.conf' 433 | if not os.path.exists(conf_symlink): 434 | os.symlink(self.TEMPEST_CONF, conf_symlink) 435 | 436 | def execute_tox(self, run_dir, logfile, tox_target): 437 | """Trigger tempest run through tox setting proxies if needed""" 438 | env = os.environ.copy() 439 | conf = hookenv.config() 440 | if conf.get('http-proxy'): 441 | env['http_proxy'] = conf['http-proxy'] 442 | if conf.get('https-proxy'): 443 | env['https_proxy'] = conf['https-proxy'] 444 | cmd = ['tox', '-e', tox_target] 445 | f = open(logfile, "w") 446 | subprocess.call(cmd, cwd=run_dir, stdout=f, stderr=f, env=env) 447 | 448 | def get_tempest_files(self, branch_name): 449 | """Prepare tempest files and directories 450 | 451 | @return git_dir, logfile, run_dir 452 | """ 453 | log_time_str = time.strftime("%Y%m%d%H%M%S", time.gmtime()) 454 | git_dir = '{}/tempest-{}'.format(self.TEMPEST_ROOT, branch_name) 455 | logfile = '{}/run_{}.log'.format(self.TEMPEST_LOGDIR, log_time_str) 456 | run_dir = '{}/tempest'.format(git_dir) 457 | return git_dir, logfile, run_dir 458 | 459 | def parse_tempest_log(self, logfile): 460 | """Read tempest logfile and return summary as dict 461 | 462 | @return dict: Dictonary of summary data 463 | """ 464 | summary = {} 465 | with open(logfile, 'r') as tempest_log: 466 | summary_line = False 467 | for line in tempest_log: 468 | if line.strip() == "Totals": 469 | summary_line = True 470 | if line.strip() == "Worker Balance": 471 | summary_line = False 472 | if summary_line: 473 | # Match lines like: ' - Unexpected Success: 0' 474 | matchObj = re.match( 475 | r'(.*)- (.*?):\s+(.*)', line, re.M | re.I) 476 | if matchObj: 477 | key = matchObj.group(2) 478 | key = key.replace(' ', '-').replace(':', '').lower() 479 | summary[key] = matchObj.group(3) 480 | return summary 481 | 482 | def run_test(self, tox_target): 483 | """Run smoke tests""" 484 | action_args = hookenv.action_get() 485 | branch_name = action_args['branch'] 486 | git_dir, logfile, run_dir = self.get_tempest_files(branch_name) 487 | self.setup_directories() 488 | self.setup_git(branch_name, git_dir) 489 | self.execute_tox(run_dir, logfile, tox_target) 490 | action_info = self.parse_tempest_log(logfile) 491 | action_info['tempest-logfile'] = logfile 492 | hookenv.action_set(action_info) 493 | 494 | 495 | class TempestCharmRocky(TempestCharm): 496 | 497 | release = 'rocky' 498 | 499 | packages = [ 500 | 'git', 'testrepository', 'subunit', 'python3-nose', 'python3-lxml', 501 | 'python3-boto', 'python3-junitxml', 'python3-subunit', 502 | 'python3-testresources', 'python3-oslotest', 'python3-stevedore', 503 | 'python3-cinderclient', 'python3-glanceclient', 'python3-heatclient', 504 | 'python3-keystoneclient', 'python3-neutronclient', 505 | 'python3-novaclient', 'python3-swiftclient', 506 | 'python3-ceilometerclient', 'openvswitch-test', 'openvswitch-common', 507 | 'libffi-dev', 'libssl-dev', 'python3-dev', 'python3-cffi' 508 | ] 509 | 510 | purge_packages = [ 511 | 'python-nose', 'python-lxml', 'python-boto', 'python-junitxml', 512 | 'python-subunit', 'python-testresources', 'python-oslotest', 513 | 'python-stevedore', 'python-cinderclient', 'python-glanceclient', 514 | 'python-heatclient', 'python-keystoneclient', 'python-neutronclient', 515 | 'python-novaclient', 'python-swiftclient', 'python-ceilometerclient', 516 | 'python-dev', 'python-cffi' 517 | ] 518 | 519 | python_version = 3 520 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 61 | 64 | 68 | 72 | 77 | 82 | 87 | 91 | 96 | 101 | 106 | 111 | 117 | 122 | 123 | 125 | 129 | 134 | 140 | 145 | 150 | 156 | 157 | 161 | 166 | 172 | 177 | 182 | 188 | 189 | 193 | 197 | 198 | 201 | 205 | 206 | 213 | 217 | 218 | 221 | 225 | 229 | 230 | 232 | 236 | 240 | 241 | 244 | 249 | 255 | 256 | 257 | 262 | 271 | 273 | 276 | 277 | 279 | 282 | 283 | 285 | 288 | 289 | 292 | 296 | 300 | 301 | 304 | 308 | 309 | 312 | 316 | 317 | 320 | 324 | 325 | 328 | 333 | 334 | 345 | 348 | 352 | 353 | 363 | 374 | 384 | 385 | 387 | 388 | 390 | image/svg+xml 391 | 393 | 394 | 395 | 396 | 397 | 403 | 409 | 413 | 420 | 427 | 433 | 434 | 439 | 446 | 451 | 456 | 461 | 466 | 471 | 476 | 484 | 489 | 494 | 499 | 505 | 510 | 511 | 516 | 587 | 588 | --------------------------------------------------------------------------------