Retire refstack-client repository
Remove the content following https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project Change-Id: I85c44dc83727d035e95931f2e7528aaf5772b314 Signed-off-by: Christian Berendt <berendt@osism.tech>
This commit is contained in:
@@ -1,17 +0,0 @@
|
||||
*.egg*
|
||||
*.py[cod]
|
||||
.coverage
|
||||
.cache
|
||||
.tox/
|
||||
.venv/
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
build/
|
||||
cover/
|
||||
dist
|
||||
.git/
|
||||
venv/
|
||||
.venv/
|
||||
.tempest/
|
||||
htmlcov/
|
||||
test-reports/
|
||||
17
.gitignore
vendored
17
.gitignore
vendored
@@ -1,17 +0,0 @@
|
||||
*.pyc
|
||||
.stestr/
|
||||
.tox
|
||||
*.egg-info/
|
||||
*.egg
|
||||
.eggs/
|
||||
.coverage
|
||||
cover/*
|
||||
.project
|
||||
.pydevproject
|
||||
build
|
||||
.tempest/
|
||||
.venv/
|
||||
etc/
|
||||
.tempestconf
|
||||
tempest.log
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_path=./refstack_client/tests/unit
|
||||
top_dir=./
|
||||
47
.zuul.yaml
47
.zuul.yaml
@@ -1,47 +0,0 @@
|
||||
- project:
|
||||
templates:
|
||||
- openstack-cover-jobs
|
||||
check:
|
||||
jobs:
|
||||
- openstack-tox-pep8
|
||||
- openstack-tox-py38
|
||||
- openstack-tox-py39
|
||||
- openstack-tox-py310
|
||||
- openstack-tox-py311
|
||||
- refstack-client-devstack-master:
|
||||
# Define a list of irrelevant files to use everywhere else
|
||||
irrelevant-files: &refstack-client-irrelevant-files
|
||||
- ^.*\.rst$
|
||||
- ^doc/.*$
|
||||
- ^.gitignore$
|
||||
- ^.gitreview$
|
||||
- refstack-client-devstack-master-fips-centos9:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- refstack-client-devstack-2023-1:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- refstack-client-devstack-zed:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- refstack-client-devstack-yoga:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- opendev-tox-docs
|
||||
- openstack-tox-py39
|
||||
gate:
|
||||
jobs:
|
||||
- openstack-tox-pep8
|
||||
- openstack-tox-py38
|
||||
- openstack-tox-py39
|
||||
- openstack-tox-py310
|
||||
- openstack-tox-py311
|
||||
- refstack-client-devstack-master:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- refstack-client-devstack-2023-1:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- refstack-client-devstack-zed:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- refstack-client-devstack-yoga:
|
||||
irrelevant-files: *refstack-client-irrelevant-files
|
||||
- opendev-tox-docs
|
||||
- openstack-tox-py39
|
||||
promote:
|
||||
jobs:
|
||||
- opendev-promote-docs
|
||||
@@ -1,34 +0,0 @@
|
||||
The source repository for this project can be found at:
|
||||
|
||||
https://opendev.org/openinfra/refstack-client
|
||||
|
||||
To start contributing to OpenStack, follow the steps in the contribution guide
|
||||
to set up and use Gerrit:
|
||||
|
||||
https://docs.openstack.org/contributors/code-and-documentation/quick-start.html
|
||||
|
||||
Documentation of the project can be found at:
|
||||
|
||||
https://docs.opendev.org/openinfra/refstack-client/latest/
|
||||
|
||||
Bugs should be filed on Taiga:
|
||||
|
||||
https://tree.taiga.io/project/openstack-interop-working-group/issues
|
||||
|
||||
Patches against this project can be found at:
|
||||
|
||||
https://review.opendev.org/q/project:openinfra/refstack-client
|
||||
|
||||
To communicate with us you may use one of the following means:
|
||||
|
||||
**Mailing List:**
|
||||
Get in touch with us via `email <mailto:openstack-discuss@lists.openstack.org>`_.
|
||||
Use [refstack-client] in your subject.
|
||||
|
||||
**IRC:**
|
||||
We're at #refstack channel on OFTC network.
|
||||
`Setup IRC <https://docs.openstack.org/contributors/common/irc.html>`_
|
||||
|
||||
**Meetings:**
|
||||
`Visit this link <https://meetings.opendev.org/#Interop_Working_Group_Meeting>`_
|
||||
for the meeting information.
|
||||
176
LICENSE
176
LICENSE
@@ -1,176 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
229
README.rst
229
README.rst
@@ -1,225 +1,6 @@
|
||||
===============
|
||||
RefStack Client
|
||||
===============
|
||||
|
||||
Overview
|
||||
########
|
||||
|
||||
``refstack-client`` is a command line utility that allows you to execute Tempest
|
||||
test runs based on configurations you specify. When finished running Tempest
|
||||
it can send the passed test data to a RefStack API server.
|
||||
|
||||
Environment setup
|
||||
#################
|
||||
|
||||
We've created an "easy button" for Ubuntu, Centos, RHEL and openSUSE.
|
||||
|
||||
1. Make sure you have ``git`` installed
|
||||
2. Get the refstack client: ``git clone https://opendev.org/openinfra/refstack-client.git``
|
||||
3. Go into the ``refstack-client`` directory: ``cd refstack-client``
|
||||
4. Run the "easy button" setup: ``./setup_env``
|
||||
|
||||
**Options:**
|
||||
|
||||
a. -c option allows to specify SHA of commit or branch in Tempest repository
|
||||
which will be installed.
|
||||
|
||||
b. -t option allows to specify tag in Tempest repository which will be installed.
|
||||
For example: execute ``./setup_env -t tags/3`` to install Tempest tag-3.
|
||||
By default, Tempest will be installed from commit
|
||||
3c7eebaaf35c9e8a3f00c76cd1741457bdec9fab (April 2023).
|
||||
|
||||
c. -p option allows to specify python version - 3.8.10 (-p 3)
|
||||
or any equal or above 3.8.0. Default to python 3.8.10.
|
||||
|
||||
d. -q option makes ``refstack-client`` run quitely - if ``.tempest``
|
||||
directory exists ``refstack-client`` is considered as installed.
|
||||
|
||||
e. -s option makes ``refstack-client`` use ``python-tempestconf`` from the
|
||||
given source (path) - used when running f.e. in Zuul.
|
||||
|
||||
f. -l option makes ``refstack-client`` install ``python`` in directory ``./localpython``.
|
||||
If option -l is not used and version of ``python`` specified by option -p is equal to
|
||||
global version of python, script will use this version of ``python``.
|
||||
|
||||
Usage
|
||||
#####
|
||||
|
||||
1. Prepare a tempest configuration file (or let ``refstack-client`` generate it
|
||||
for you, see step #4) that is customized to your cloud environment.
|
||||
Samples of minimal Tempest configurations are provided in the ``etc``
|
||||
directory in ``tempest.conf.sample`` and ``accounts.yaml.sample``.
|
||||
Note that these samples will likely need changes or additional information
|
||||
to work with your cloud.
|
||||
|
||||
**Note**: Use Tempest Pre-Provisioned credentials_ to provide user test
|
||||
accounts.
|
||||
|
||||
.. _credentials: https://docs.openstack.org/tempest/latest/configuration.html#pre-provisioned-credentials
|
||||
|
||||
2. Go into the ``refstack-client`` directory::
|
||||
|
||||
cd ~/refstack-client
|
||||
|
||||
3. Source to use the correct Python environment::
|
||||
|
||||
source .venv/bin/activate
|
||||
|
||||
4. (optional) Generate tempest.conf using ``refstack-client``::
|
||||
|
||||
refstack-client config --use-test-accounts <path to account file>
|
||||
|
||||
The above command will create the tempest.conf in `etc` folder.
|
||||
|
||||
Note: If account file is not available, then:
|
||||
* Source the keystonerc file containing cloud credentials and run::
|
||||
|
||||
refstack-client config
|
||||
|
||||
It will create accounts.yaml and temepst.conf file in `etc` folder.
|
||||
|
||||
5. Validate your setup by running a short test::
|
||||
|
||||
refstack-client test \
|
||||
-c <Path of the tempest configuration file to use> -v -- \
|
||||
--regex tempest.api.identity.v3.test_tokens.TokensV3Test.test_create_token
|
||||
|
||||
6. Run tests.
|
||||
|
||||
To run the entire API test set::
|
||||
|
||||
refstack-client test -c <Path of the tempest configuration file to use> -v
|
||||
|
||||
To run only those tests specified in an OpenStack Powered (TM) Guideline::
|
||||
|
||||
refstack-client test -c <Path of the tempest configuration file to use> -v --test-list <Absolute path of test list>
|
||||
|
||||
For example::
|
||||
|
||||
refstack-client test \
|
||||
-c ~/tempest.conf -v \
|
||||
--test-list "https://refstack.openstack.org/api/v1/guidelines/2020.11/tests?target=platform&type=required&alias=true&flag=false"
|
||||
|
||||
This will run only the test cases required by the 2020.11 guidelines under
|
||||
Platform OpenStack Marketing Program that have not been flagged. More about
|
||||
the marketing programs at `Interop and OpenStack Marketing Programs`_.
|
||||
|
||||
For example tests under the compute program are available:
|
||||
https://refstack.openstack.org/api/v1/guidelines/2020.11/tests?target=compute&type=required&alias=true&flag=false
|
||||
Tests of add-on programs can be found similarly, f.e. tests under dns program:
|
||||
https://refstack.openstack.org/api/v1/guidelines/dns.2020.11/tests?target=dns&type=required&alias=true&flag=false
|
||||
or tests under orchestration program:
|
||||
https://refstack.openstack.org/api/v1/guidelines/orchestration.2020.11/tests?target=orchestration&type=required&alias=true&flag=false
|
||||
|
||||
**Note:**
|
||||
|
||||
a. Adding the ``-v`` option will show the Tempest test result output.
|
||||
b. Adding the ``--upload`` option will have your test results be uploaded to the
|
||||
default RefStack API server or the server specified by ``--url``.
|
||||
c. Adding the ``--test-list`` option will allow you to specify the file path or URL of
|
||||
a test list text file. This test list should contain specific test cases that
|
||||
should be tested. Tests lists passed in using this argument will be normalized
|
||||
with the current Tempest environment to eliminate any attribute mismatches.
|
||||
d. Adding the ``--url`` option will allow you to change where test results should
|
||||
be uploaded.
|
||||
e. Adding the ``-r`` option with a string will prefix the JSON result file with the
|
||||
given string (e.g. ``-r my-test`` will yield a result file like
|
||||
'my-test-0.json').
|
||||
f. Adding ``--`` enables you to pass arbitrary arguments to tempest run.
|
||||
After the first ``--``, all other subsequent arguments will be passed to
|
||||
tempest run as is. This is mainly used for quick verification of the
|
||||
target test cases. (e.g. ``-- --regex tempest.api.identity.v2.test_token``)
|
||||
g. If you have provisioned multiple user/project accounts you can run parallel
|
||||
test execution by enabling the ``--parallel`` flag.
|
||||
|
||||
Use ``refstack-client test --help`` for the full list of arguments.
|
||||
|
||||
6. Upload your results.
|
||||
|
||||
If you previously ran a test with ``refstack-client`` without the ``--upload``
|
||||
option, you can later upload your results to a RefStack API server
|
||||
with your digital signature. By default, the results are private and you can
|
||||
decide to share or delete the results later.
|
||||
|
||||
Following is the command to upload your result::
|
||||
|
||||
refstack-client upload <Path of results file> -i <path-to-private-key>
|
||||
|
||||
The results file is a JSON file generated by ``refstack-client`` when a test has
|
||||
completed. This is saved in .tempest/.stestr. When you use the
|
||||
``upload`` command, you can also override the RefStack API server uploaded to
|
||||
with the ``--url`` option.
|
||||
|
||||
Alternatively, you can use the ``upload-subunit`` command to upload results
|
||||
using an existing subunit file. This requires that you pass in the Keystone
|
||||
endpoint URL for the cloud that was tested to generate the subunit data::
|
||||
|
||||
refstack-client upload-subunit \
|
||||
--keystone-endpoint http://some.url:5000/v3 <Path of subunit file> \
|
||||
-i <path-to-private-key>
|
||||
|
||||
Intructions for uploading data with signature can be found at
|
||||
https://opendev.org/openinfra/refstack/src/branch/master/doc/source/uploading_private_results.rst
|
||||
|
||||
7. View uploaded test set.
|
||||
|
||||
You can list previously uploaded data from a RefStack API server by using
|
||||
the following command::
|
||||
|
||||
refstack-client list --url <URL of the RefStack API server> -i <path to private key>
|
||||
|
||||
Alternatively, if you uploaded the results to the official RefStack_ server
|
||||
you can view them by using RefStack_ page where all uploaded results
|
||||
associated with the particular account (the account private key used to
|
||||
upload the results belongs to) will be shown and may be further managed.
|
||||
|
||||
|
||||
Tempest hacking
|
||||
###############
|
||||
|
||||
By default, ``refstack-client`` installs Tempest into the ``.tempest`` directory.
|
||||
If you're interested in working with Tempest directly for debugging or
|
||||
configuration, you can activate a working Tempest environment by
|
||||
switching to that directory and using the installed dependencies.
|
||||
|
||||
1. ``cd .tempest``
|
||||
2. ``source ./.venv/bin/activate``
|
||||
and run tests manually with ``tempest run``.
|
||||
|
||||
This will make the entire Tempest environment available for you to run,
|
||||
including ``tempest run``. More about Tempest can be found at its documentation_.
|
||||
|
||||
.. _documentation: https://docs.openstack.org/tempest/latest/
|
||||
|
||||
|
||||
Interop and OpenStack Marketing Programs
|
||||
########################################
|
||||
|
||||
The tests ``refstack-client`` runs are defined within interop_ repository
|
||||
and divided into several OpenStack Marketing Programs, the list of the programs
|
||||
can be found at RefStack_ page.
|
||||
|
||||
.. _interop: https://opendev.org/openinfra/interop
|
||||
.. _RefStack: https://refstack.openstack.org/#/
|
||||
|
||||
|
||||
ansible-role-refstack-client
|
||||
############################
|
||||
|
||||
We have created an ansible role called ansible-role-refstack-client_ in order
|
||||
to simplify and automate running of ``refstack-client``. The role can be easily
|
||||
integrated to an automation machinery - f.e. we use the role for running
|
||||
``refstack-client`` on a devstack_ environment in Zuul where we run tests of
|
||||
every OpenStack Marketing Program of the current guideline. The latest builds
|
||||
can be found here__.
|
||||
|
||||
.. _ansible-role-refstack-client: https://opendev.org/openinfra/ansible-role-refstack-client
|
||||
.. _devstack: https://opendev.org/openstack/devstack/
|
||||
.. __builds: https://zuul.openstack.org/builds?project=openinfra%2Fansible-role-refstack-client
|
||||
|
||||
Get Involved
|
||||
############
|
||||
|
||||
See the `CONTRIBUTING <https://docs.opendev.org/openinfra/refstack-client/latest/contributing.html>`_
|
||||
guide on how to get involved.
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
Building Docs
|
||||
=============
|
||||
|
||||
Developer documentation is generated using Sphinx. To build this documentation,
|
||||
run the following from the root of the repository.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ tox -e docs
|
||||
|
||||
The documentation will be built at ``build/``.
|
||||
@@ -1,2 +0,0 @@
|
||||
sphinx>=1.6.2 # BSD
|
||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
||||
@@ -1,269 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# refstack-client documentation build configuration file, created by
|
||||
# sphinx-quickstart on Thu Sep 11 17:52:13 2014.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'openstackdocstheme'
|
||||
]
|
||||
|
||||
# openstackdocstheme options
|
||||
openstackdocs_repo_name = 'openinfra/refstack-client'
|
||||
openstackdocs_bug_project = '879'
|
||||
openstackdocs_bug_tag = ''
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
# templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'refstack-client'
|
||||
copyright = u'2014, OpenStack'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '0.1'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = '0.1'
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
#keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'alabaster'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
#html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
#html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
# So that we can enable "log-a-bug" links from each output HTML page, this
|
||||
# variable must be set to a format that includes year, month, day, hours and
|
||||
# minutes.
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'refstack-clientdoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'refstack-client.tex', u'refstack-client Documentation',
|
||||
u'OpenStack', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'refstack-client', u'refstack-client Documentation',
|
||||
[u'OpenStack'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
('index', 'refstack-client', u'refstack-client Documentation',
|
||||
u'OpenStack', 'refstack-client', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
#texinfo_no_detailmenu = False
|
||||
@@ -1,5 +0,0 @@
|
||||
============
|
||||
Contributing
|
||||
============
|
||||
|
||||
.. include:: ../../CONTRIBUTING.rst
|
||||
@@ -1,29 +0,0 @@
|
||||
===========================================
|
||||
Welcome to refstack-client's documentation!
|
||||
===========================================
|
||||
|
||||
---------------------
|
||||
About refstack-client
|
||||
---------------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
readme
|
||||
contributing
|
||||
|
||||
-----------------
|
||||
Approved Features
|
||||
-----------------
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
:glob:
|
||||
|
||||
specs/*/approved/*
|
||||
|
||||
------------------
|
||||
Indices and tables
|
||||
------------------
|
||||
|
||||
* :ref:`search`
|
||||
@@ -1 +0,0 @@
|
||||
.. include:: ../../README.rst
|
||||
@@ -1 +0,0 @@
|
||||
../../specs/
|
||||
@@ -1,17 +0,0 @@
|
||||
FROM ubuntu:16.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y sudo curl vim less tar
|
||||
|
||||
ARG UID
|
||||
ARG GID
|
||||
ENV DEV_USER=ubuntu COLUMNS=120
|
||||
|
||||
RUN [ ! $(grep ":${GID}:" /etc/group) ] && groupadd -g ${GID:-1000} ${DEV_USER}
|
||||
|
||||
RUN useradd -g ${DEV_USER} -u ${UID:-1000} -s /bin/bash -d /home/${DEV_USER} -m ${DEV_USER} && \
|
||||
( umask 226 && echo "${DEV_USER} ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/50_${DEV_USER} )
|
||||
|
||||
USER ${DEV_USER}
|
||||
WORKDIR /home/${DEV_USER}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash -x
|
||||
|
||||
if [ "$EUID" -eq 0 ]
|
||||
then echo "This script should not be runned with sudo!"
|
||||
exit
|
||||
fi
|
||||
|
||||
docker ps &> /dev/null; (( $? != 0 )) && echo 'Docker should be accessible without sudo '
|
||||
|
||||
|
||||
CONTAINER_NAME=refstack_client
|
||||
|
||||
if [ $( docker ps -a -q --filter name=${CONTAINER_NAME} ) ]; then
|
||||
docker rm -f $( docker ps -a -q --filter name=${CONTAINER_NAME} )
|
||||
fi
|
||||
|
||||
docker build -t ${CONTAINER_NAME} \
|
||||
--build-arg UID=$( id -u $USER ) \
|
||||
--build-arg GID=$( id -g $USER ) \
|
||||
--file $( git rev-parse --show-toplevel )/docker/Dockerfile \
|
||||
$( git rev-parse --show-toplevel )
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash -x
|
||||
|
||||
if [ "$EUID" -eq 0 ]
|
||||
then echo "This script should not be runned with sudo!"
|
||||
exit
|
||||
fi
|
||||
|
||||
docker ps &> /dev/null; (( $? != 0 )) && echo 'Docker should be accessible without sudo '
|
||||
|
||||
CONTAINER_NAME=refstack_client
|
||||
|
||||
if [ ! $( docker ps -q --filter name=${CONTAINER_NAME} ) ]; then
|
||||
ENV_CONTAINER=$( docker ps -a -q --filter name=${CONTAINER_NAME} )
|
||||
if [ ${ENV_CONTAINER} ]; then
|
||||
docker start -a -i $ENV_CONTAINER
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker run \
|
||||
--dns=8.8.8.8 \
|
||||
-i -t \
|
||||
--name ${CONTAINER_NAME}\
|
||||
-v $( git rev-parse --show-toplevel ):/home/ubuntu/refstack-client \
|
||||
-e REFSTACK_CLIENT_TEMPEST_DIR=/home/ubuntu/tempest \
|
||||
${CONTAINER_NAME} bash -c '~/refstack-client/setup_env -q && bash'
|
||||
fi
|
||||
|
||||
ENV_CONTAINER=$( docker ps -q --filter name=${CONTAINER_NAME} )
|
||||
[[ ! ${ENV_CONTAINER} ]] && exit 1
|
||||
|
||||
[[ $* ]] && {
|
||||
docker exec ${ENV_CONTAINER} $*
|
||||
} || {
|
||||
docker exec -i -t ${ENV_CONTAINER} bash
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
# A minimal accounts.yaml file
|
||||
# Will likely not work with swift, since additional
|
||||
# roles are required. For more documentation see:
|
||||
# https://opendev.org/openstack/tempest/src/branch/master/etc/accounts.yaml.sample
|
||||
|
||||
- username: '$USERNAME'
|
||||
project_name: '$PROJECTNAME'
|
||||
password: '$PASSWORD'
|
||||
@@ -1,51 +0,0 @@
|
||||
# This is a minimal example of what your tempest.conf file
|
||||
# can look like. You will need to supply your own values
|
||||
# and possibly add additional configurations for your cloud
|
||||
# You can use this as a starting point for configuring
|
||||
# tempest for the refstack client.
|
||||
|
||||
[auth]
|
||||
test_accounts_file = $ACCOUNTS_YAML
|
||||
use_dynamic_credentials = false
|
||||
|
||||
[compute]
|
||||
image_ref = $IMAGE_REF_1
|
||||
image_ref_alt = $IMAGE_REF_2
|
||||
flavor_ref = $FLAVOR_REF_1
|
||||
flavor_ref_alt = $FLAVOR_REF_2
|
||||
fixed_network_name = $FIXED_NETWORK_NAME
|
||||
|
||||
[compute-feature-enabled]
|
||||
resize = true
|
||||
|
||||
[identity]
|
||||
uri_v3 = $KEYSTONE_V3_ENDPOINT
|
||||
auth_version = v3
|
||||
|
||||
[identity-feature-enabled]
|
||||
api_v2 = false
|
||||
api_v3 = true
|
||||
|
||||
[network]
|
||||
public_network_id = $PUBLIC_NETWORK_ID
|
||||
floating_network_name = $FLOATING_NETWORK_ID
|
||||
|
||||
[object-storage]
|
||||
operator_role = SwiftOperator
|
||||
reseller_admin_role = ResellerAdmin
|
||||
|
||||
[oslo_concurrency]
|
||||
lock_path = /tmp/tempest
|
||||
|
||||
[service_available]
|
||||
cinder = true
|
||||
neutron = true
|
||||
glance = true
|
||||
swift = true
|
||||
nova = true
|
||||
heat = false
|
||||
|
||||
[validation]
|
||||
run_validation = true
|
||||
connect_method = floating
|
||||
auth_method = keypair
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2014 Piston Cloud Computing, Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
Run Tempest and upload results to RefStack.
|
||||
|
||||
This module runs the Tempest test suite on an OpenStack environment given a
|
||||
Tempest configuration file.
|
||||
|
||||
"""
|
||||
from refstack_client import refstack_client
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = refstack_client.parse_cli_args()
|
||||
test = refstack_client.RefstackClient(args)
|
||||
raise SystemExit(getattr(test, args.func)())
|
||||
@@ -1,210 +0,0 @@
|
||||
# Copyright (c) 2015 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
|
||||
class TestListParser(object):
|
||||
|
||||
"""This class is for normalizing test lists to match the tests in the
|
||||
current Tempest environment.
|
||||
"""
|
||||
|
||||
def __init__(self, tempest_dir, insecure=False):
|
||||
"""
|
||||
Initialize the TestListParser.
|
||||
|
||||
:param tempest_dir: Absolute path of the Tempest directory.
|
||||
:param insecure: Whether https requests, if any, should be insecure.
|
||||
"""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.tempest_dir = tempest_dir
|
||||
self.insecure = insecure
|
||||
|
||||
def _get_tempest_test_ids(self):
|
||||
"""This does a 'testr list-tests' or 'stestr list' according to
|
||||
Tempest version on the Tempest directory in order to get a list
|
||||
of full test IDs for the current Tempest environment. Test ID
|
||||
mappings are then formed for these tests.
|
||||
"""
|
||||
if os.path.exists(os.path.join(self.tempest_dir, '.stestr.conf')):
|
||||
init_cmd = (os.path.join(self.tempest_dir, 'tools/with_venv.sh'),
|
||||
'stestr', 'init')
|
||||
subprocess.Popen(init_cmd, stdout=subprocess.PIPE,
|
||||
cwd=self.tempest_dir)
|
||||
cmd = (os.path.join(self.tempest_dir, 'tools/with_venv.sh'),
|
||||
'stestr', 'list')
|
||||
else:
|
||||
cmd = (os.path.join(self.tempest_dir, 'tools/with_venv.sh'),
|
||||
'testr', 'list-tests')
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
|
||||
cwd=self.tempest_dir)
|
||||
(stdout, stderr) = process.communicate()
|
||||
|
||||
if process.returncode != 0:
|
||||
self.logger.error(stdout)
|
||||
self.logger.error(stderr)
|
||||
raise subprocess.CalledProcessError(process.returncode,
|
||||
' '.join(cmd))
|
||||
try:
|
||||
return self._form_test_id_mappings(stdout.split('\n'))
|
||||
except TypeError:
|
||||
return self._form_test_id_mappings(stdout.decode().split('\n'))
|
||||
|
||||
def _form_test_id_mappings(self, test_list):
|
||||
"""This takes in a list of full test IDs and forms a dict containing
|
||||
base test IDs mapped to their attributes. A full test ID also contains
|
||||
test attributes such as '[gate,smoke]'
|
||||
Ex:
|
||||
'tempest.api.test1': '[gate]'
|
||||
'tempest.api.test2': ''
|
||||
'tempest.api.test3(some_scenario)': '[smoke,gate]'
|
||||
|
||||
:param test_list: List of full test IDs
|
||||
"""
|
||||
test_mappings = {}
|
||||
for testcase in test_list:
|
||||
if testcase != "":
|
||||
# Search for any strings like '[smoke, gate]' in the test ID.
|
||||
match = re.search(r'(\[.*\])', testcase)
|
||||
|
||||
if match:
|
||||
testcase = re.sub(r'\[.*\]', '', testcase)
|
||||
test_mappings[testcase] = match.group(1)
|
||||
else:
|
||||
test_mappings[testcase] = ""
|
||||
return test_mappings
|
||||
|
||||
def _get_base_test_ids_from_list_file(self, list_location):
|
||||
"""This takes in a test list file and finds all the base test IDs
|
||||
for the tests listed.
|
||||
Ex:
|
||||
'tempest.test1[gate,id-2]' -> 'tempest.test1'
|
||||
'tempest.test2[gate,id-3](scenario)' -> 'tempest.test2(scenario)'
|
||||
|
||||
:param list_location: file path or URL location of list file
|
||||
"""
|
||||
try:
|
||||
response = requests.get(list_location,
|
||||
verify=not self.insecure)
|
||||
testcase_list = response.text.split('\n')
|
||||
test_mappings = self._form_test_id_mappings(testcase_list)
|
||||
# If the location isn't a valid URL, we assume it is a file path.
|
||||
except requests.exceptions.MissingSchema:
|
||||
try:
|
||||
with open(list_location) as data_file:
|
||||
testcase_list = [line.rstrip('\n') for line in data_file]
|
||||
test_mappings = self._form_test_id_mappings(testcase_list)
|
||||
except Exception:
|
||||
self.logger.error("Error reading the passed in test list " +
|
||||
"file.")
|
||||
raise
|
||||
except Exception:
|
||||
self.logger.error("Error reading the passed in test list file.")
|
||||
raise
|
||||
|
||||
return list(test_mappings.keys())
|
||||
|
||||
def _get_full_test_ids(self, tempest_ids, base_ids):
|
||||
"""This will remake the test ID list with the full IDs of the current
|
||||
Tempest environment. The Tempest test ID dict should have the correct
|
||||
mappings.
|
||||
|
||||
:param tempest_ids: dict containing test ID mappings
|
||||
:param base_ids: list containing base test IDs
|
||||
"""
|
||||
test_list = []
|
||||
for test_id in base_ids:
|
||||
try:
|
||||
attr = tempest_ids[test_id]
|
||||
# If the test has a scenario in the test ID, but also has some
|
||||
# additional attributes, the attributes need to go before the
|
||||
# scenario.
|
||||
if '(' in test_id and attr:
|
||||
components = test_id.split('(', 1)
|
||||
test_portion = components[0]
|
||||
scenario = "(" + components[1]
|
||||
test_list.append(test_portion + attr + scenario)
|
||||
else:
|
||||
test_list.append(test_id + attr)
|
||||
except KeyError:
|
||||
self.logger.debug("Test %s not found in Tempest list." %
|
||||
test_id)
|
||||
self.logger.debug("Number of tests: " + str(len(test_list)))
|
||||
return test_list
|
||||
|
||||
def _write_normalized_test_list(self, test_ids):
|
||||
"""Create a temporary file to pass into testr containing a list of test
|
||||
IDs that should be tested.
|
||||
|
||||
:param test_ids: list of full test IDs
|
||||
"""
|
||||
temp = tempfile.NamedTemporaryFile(delete=False)
|
||||
for test_id in test_ids:
|
||||
temp.write(("%s\n" % test_id).encode('utf-8'))
|
||||
temp.flush()
|
||||
|
||||
# Register the created file for cleanup.
|
||||
atexit.register(self._remove_test_list_file, temp.name)
|
||||
return temp.name
|
||||
|
||||
def _remove_test_list_file(self, file_path):
|
||||
"""Delete the given file.
|
||||
|
||||
:param file_path: string containing the location of the file
|
||||
"""
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
def get_normalized_test_list(self, list_location):
|
||||
"""This will take in the user's test list and will normalize it
|
||||
so that the test cases in the list map to actual full test IDS in
|
||||
the Tempest environment.
|
||||
|
||||
:param list_location: file path or URL of the test list
|
||||
"""
|
||||
tempest_test_ids = self._get_tempest_test_ids()
|
||||
if not tempest_test_ids:
|
||||
return None
|
||||
base_test_ids = self._get_base_test_ids_from_list_file(list_location)
|
||||
full_capability_test_ids = self._get_full_test_ids(tempest_test_ids,
|
||||
base_test_ids)
|
||||
list_file = self._write_normalized_test_list(full_capability_test_ids)
|
||||
return list_file
|
||||
|
||||
def create_include_list(self, list_location):
|
||||
"""This takes in a test list file, get normalized, and get list of
|
||||
include regexes using full qualified test names (one per line).
|
||||
Ex:
|
||||
'tempest.test1[id-2,gate]' -> tempest.test1\[ # noqa: W605
|
||||
'tempest.test2[id-3,smoke](scenario)' -> tempest.test2\[ # noqa: W605
|
||||
'tempest.test3[compute,id-4]' -> tempest.test3\[ # noqa: W605
|
||||
|
||||
:param list_location: file path or URL location of list file
|
||||
"""
|
||||
normalized_list = open(self.get_normalized_test_list(list_location),
|
||||
'r').read()
|
||||
# Keep the names
|
||||
tests_list = [re.sub(r"\[", r"\[", test)
|
||||
for test in re.findall(r".*\[", normalized_list)]
|
||||
|
||||
return self._write_normalized_test_list(tests_list)
|
||||
@@ -1,950 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright (c) 2014 Piston Cloud Computing, Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
Run Tempest and upload results to RefStack.
|
||||
|
||||
This module runs the Tempest test suite on an OpenStack environment given a
|
||||
Tempest configuration file.
|
||||
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import binascii
|
||||
import hashlib
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
from cryptography.exceptions import UnsupportedAlgorithm
|
||||
from cryptography.hazmat import backends
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
|
||||
from config_tempest import main
|
||||
from config_tempest import constants as C
|
||||
from keystoneauth1 import exceptions as KE
|
||||
from openstack import exceptions as OSE
|
||||
|
||||
import requests
|
||||
import requests.exceptions
|
||||
from six import moves
|
||||
from six.moves.urllib import parse
|
||||
from refstack_client.subunit_processor import SubunitProcessor
|
||||
from refstack_client.list_parser import TestListParser
|
||||
import yaml
|
||||
|
||||
|
||||
def get_input():
|
||||
"""
|
||||
Wrapper for raw_input. Necessary for testing.
|
||||
"""
|
||||
return moves.input().lower() # pragma: no cover
|
||||
|
||||
|
||||
def read_accounts_yaml(path):
|
||||
"""Reads a set of accounts from the specified file"""
|
||||
with open(path, 'r') as yaml_file:
|
||||
accounts = yaml.safe_load(yaml_file)
|
||||
return accounts
|
||||
|
||||
|
||||
class RefstackClient:
|
||||
log_format = "%(asctime)s %(name)s:%(lineno)d %(levelname)s %(message)s"
|
||||
|
||||
def __init__(self, args):
|
||||
'''Prepare a tempest test against a cloud.'''
|
||||
self.logger = logging.getLogger("refstack_client")
|
||||
self.console_log_handle = logging.StreamHandler()
|
||||
self.console_log_handle.setFormatter(
|
||||
logging.Formatter(self.log_format))
|
||||
self.logger.addHandler(self.console_log_handle)
|
||||
|
||||
self.args = args
|
||||
self.current_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
self.refstack_dir = os.path.dirname(self.current_dir)
|
||||
self.tempest_dir = os.path.join(self.refstack_dir, '.tempest')
|
||||
|
||||
# set default log level to INFO.
|
||||
if self.args.silent:
|
||||
self.logger.setLevel(logging.WARNING)
|
||||
elif self.args.verbose:
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
self.logger.setLevel(logging.INFO)
|
||||
|
||||
def _prep_test(self):
|
||||
'''Prepare a tempest test against a cloud.'''
|
||||
|
||||
# Check that the config file exists.
|
||||
if not os.path.isfile(self.args.conf_file):
|
||||
self.logger.error("Conf file not valid: %s" % self.args.conf_file)
|
||||
exit(1)
|
||||
|
||||
if not os.access(self.args.conf_file, os.R_OK):
|
||||
self.logger.error("You do not have read access to: %s"
|
||||
% self.args.conf_file)
|
||||
exit(1)
|
||||
|
||||
# Initialize environment variables with config file info
|
||||
os.environ["TEMPEST_CONFIG_DIR"] = os.path.abspath(
|
||||
os.path.dirname(self.args.conf_file))
|
||||
os.environ["TEMPEST_CONFIG"] = os.path.basename(self.args.conf_file)
|
||||
|
||||
# Check that the Tempest directory is an existing directory.
|
||||
if not os.path.isdir(self.tempest_dir):
|
||||
self.logger.error("Tempest directory given is not a directory or "
|
||||
"does not exist: %s" % self.tempest_dir)
|
||||
exit(1)
|
||||
|
||||
self.conf_file = self.args.conf_file
|
||||
# Note: SafeConfigParser deprecated on Python 3.2
|
||||
# Use ConfigParser directly
|
||||
self.conf = moves.configparser.ConfigParser()
|
||||
self.conf.read(self.args.conf_file)
|
||||
|
||||
def _prep_upload(self):
|
||||
'''Prepare an upload to the RefStack_api'''
|
||||
if not os.path.isfile(self.args.file):
|
||||
self.logger.error("File not valid: %s" % self.args.file)
|
||||
exit(1)
|
||||
|
||||
self.upload_file = self.args.file
|
||||
|
||||
def _get_next_stream_subunit_output_file(self, tempest_dir):
|
||||
'''This method reads from the next-stream file in the .testrepository
|
||||
or .stestr directory according to Tempest version of the given
|
||||
Tempest path. The integer here is the name of the file where subunit
|
||||
output will be saved to. It also checks if the repository is
|
||||
initialized and if not, initializes it'''
|
||||
if os.path.exists(os.path.join(tempest_dir, '.stestr.conf')):
|
||||
sub_dir = '.stestr'
|
||||
cmd = 'stestr'
|
||||
else:
|
||||
sub_dir = '.testrepository'
|
||||
cmd = 'testr'
|
||||
try:
|
||||
if not os.path.exists(os.path.join(tempest_dir, sub_dir)):
|
||||
self.logger.debug('No repository found, creating one.')
|
||||
os.chdir(tempest_dir)
|
||||
process = subprocess.Popen([cmd, 'init'])
|
||||
process.communicate()
|
||||
os.chdir(self.refstack_dir)
|
||||
|
||||
subunit_file = open(os.path.join(
|
||||
tempest_dir, sub_dir,
|
||||
'next-stream'), 'r').read().rstrip()
|
||||
except (IOError, OSError):
|
||||
self.logger.debug('The ' + sub_dir + '/next-stream file was not '
|
||||
'found. Assuming subunit results will be stored '
|
||||
'in file 0.')
|
||||
|
||||
# First test stream is saved into $sub_dir/0
|
||||
subunit_file = "0"
|
||||
|
||||
return os.path.join(tempest_dir, sub_dir, subunit_file)
|
||||
|
||||
def _get_keystone_config(self, conf_file):
|
||||
'''This will get and return the keystone configs from config file.'''
|
||||
try:
|
||||
# Prefer Keystone V3 API if it is enabled
|
||||
auth_version = 'v3'
|
||||
if conf_file.has_option('identity', 'auth_version'):
|
||||
auth_version = conf_file.get('identity', 'auth_version')
|
||||
|
||||
if auth_version == 'v2':
|
||||
auth_url = '%s/tokens' % (conf_file.get('identity', 'uri')
|
||||
.rstrip('/'))
|
||||
elif auth_version == 'v3':
|
||||
auth_url = '%s/auth/tokens' % (conf_file.get('identity',
|
||||
'uri_v3').rstrip('/'))
|
||||
|
||||
domain_name = 'Default'
|
||||
if conf_file.has_option('identity', 'domain_name'):
|
||||
domain_name = conf_file.get('identity', 'domain_name')
|
||||
if conf_file.has_option('auth', 'test_accounts_file'):
|
||||
account_file = os.path.expanduser(
|
||||
conf_file.get('auth', 'test_accounts_file'))
|
||||
if not os.path.isfile(account_file):
|
||||
self.logger.error(
|
||||
'Accounts file not found: %s' % account_file)
|
||||
exit(1)
|
||||
|
||||
accounts = read_accounts_yaml(account_file)
|
||||
if not accounts:
|
||||
self.logger.error('Accounts file %s found, '
|
||||
'but was empty.' % account_file)
|
||||
exit(1)
|
||||
account = accounts[0]
|
||||
username = account.get('username')
|
||||
password = account.get('password')
|
||||
project_id = (account.get('tenant_id') or
|
||||
account.get('project_id'))
|
||||
project_name = (account.get('tenant_name') or
|
||||
account.get('project_name'))
|
||||
return {'auth_version': auth_version,
|
||||
'auth_url': auth_url,
|
||||
'domain_name': domain_name,
|
||||
'username': username, 'password': password,
|
||||
'tenant_id': project_id, 'tenant_name': project_name,
|
||||
'project_id': project_id, 'project_name': project_name
|
||||
}
|
||||
elif conf_file.has_option('identity', 'username'):
|
||||
self.logger.error('Using identity section in tempest config '
|
||||
'file to specify user credentials is no '
|
||||
'longer supported. User credentials should '
|
||||
'be defined in the accounts file as '
|
||||
'described in the Tempest configuration '
|
||||
'guide (https://docs.openstack.org/tempest/'
|
||||
'latest/configuration.html).')
|
||||
exit(1)
|
||||
else:
|
||||
self.logger.error('User credentials cannot be found. '
|
||||
'User credentials should be defined in the '
|
||||
'accounts file as described in the Tempest '
|
||||
'configuration guide (http://docs.openstack.'
|
||||
'org/tempest/latest/configuration.html).')
|
||||
exit(1)
|
||||
except moves.configparser.Error as e:
|
||||
# Most likely a missing section or option in the config file.
|
||||
self.logger.error("Invalid Config File: %s" % e)
|
||||
exit(1)
|
||||
|
||||
def _generate_keystone_data(self, auth_config):
|
||||
'''This will generate data for http post to keystone
|
||||
API from auth_config.'''
|
||||
auth_version = auth_config['auth_version']
|
||||
auth_url = auth_config['auth_url']
|
||||
if auth_version == 'v2':
|
||||
password_credential = {'username': auth_config['username'],
|
||||
'password': auth_config['password']}
|
||||
if auth_config['tenant_id']:
|
||||
data = {
|
||||
'auth': {
|
||||
'tenantId': auth_config['tenant_id'],
|
||||
'passwordCredentials': password_credential
|
||||
}
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
'auth': {
|
||||
'tenantName': auth_config['tenant_name'],
|
||||
'passwordCredentials': password_credential
|
||||
}
|
||||
}
|
||||
return auth_version, auth_url, data
|
||||
elif auth_version == 'v3':
|
||||
identity = {
|
||||
'methods': ['password'],
|
||||
'password': {
|
||||
'user': {
|
||||
'name': auth_config['username'],
|
||||
'domain': {'name': auth_config['domain_name']},
|
||||
'password': auth_config['password']
|
||||
}}}
|
||||
|
||||
data = {
|
||||
'auth': {
|
||||
'identity': identity,
|
||||
'scope': {
|
||||
'project': {
|
||||
'name': auth_config['tenant_name'],
|
||||
'domain': {'name': auth_config['domain_name']}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return auth_version, auth_url, data
|
||||
|
||||
def _get_cpid_from_keystone(self, auth_version, auth_url, content):
|
||||
'''This will get the Keystone service ID which is used as the CPID.'''
|
||||
try:
|
||||
headers = {'content-type': 'application/json'}
|
||||
response = requests.post(auth_url,
|
||||
data=json.dumps(content),
|
||||
headers=headers,
|
||||
verify=not self.args.insecure)
|
||||
rsp = response.json()
|
||||
if response.status_code in (200, 203):
|
||||
# keystone API v2 response.
|
||||
access = rsp['access']
|
||||
for service in access['serviceCatalog']:
|
||||
if service['type'] == 'identity':
|
||||
if service['endpoints'][0]['id']:
|
||||
return service['endpoints'][0]['id']
|
||||
# Raise a key error if 'identity' was not found so that it
|
||||
# can be caught and have an appropriate error displayed.
|
||||
raise KeyError
|
||||
elif response.status_code == 201:
|
||||
# keystone API v3 response.
|
||||
token = rsp['token']
|
||||
for service in token['catalog']:
|
||||
if service['type'] == 'identity' and service['id']:
|
||||
return service['id']
|
||||
# Raise a key error if 'identity' was not found.
|
||||
# It will be caught below as well.
|
||||
raise KeyError
|
||||
else:
|
||||
message = ('Invalid request with error '
|
||||
'code: %s. Error message: %s'
|
||||
'' % (rsp['error']['code'],
|
||||
rsp['error']['message']))
|
||||
raise requests.exceptions.HTTPError(message)
|
||||
# If a Key or Index Error was raised, one of the expected keys or
|
||||
# indices for retrieving the identity service ID was not found.
|
||||
except (KeyError, IndexError):
|
||||
self.logger.warning('Unable to retrieve CPID from Keystone %s '
|
||||
'catalog. The catalog or the identity '
|
||||
'service endpoint was not '
|
||||
'found.' % auth_version)
|
||||
except Exception as e:
|
||||
self.logger.warning('Problems retrieving CPID from Keystone '
|
||||
'using %s endpoint (%s) with error (%s)'
|
||||
% (auth_version, auth_url, e))
|
||||
return self._generate_cpid_from_endpoint(auth_url)
|
||||
|
||||
def _generate_cpid_from_endpoint(self, endpoint):
|
||||
'''This method will md5 hash the hostname of a Keystone endpoint to
|
||||
generate a CPID.'''
|
||||
self.logger.info('Creating hash from endpoint to use as CPID.')
|
||||
url_parts = parse.urlparse(endpoint)
|
||||
if url_parts.scheme not in ('http', 'https'):
|
||||
raise ValueError('Invalid Keystone endpoint format. Make sure '
|
||||
'the endpoint (%s) includes the URL scheme '
|
||||
'(i.e. http/https).' % endpoint)
|
||||
return hashlib.md5(url_parts.hostname.encode('utf-8')).hexdigest()
|
||||
|
||||
def _form_result_content(self, cpid, duration, results):
|
||||
'''This method will create the content for the request. The spec at
|
||||
'https://opendev.org/openinfra/refstack/src/branch/master/specs/prior'
|
||||
'/implemented/api-v1.md'.
|
||||
defines the format expected by the API.'''
|
||||
content = {}
|
||||
content['cpid'] = cpid
|
||||
content['duration_seconds'] = duration
|
||||
content['results'] = results
|
||||
return content
|
||||
|
||||
def _save_json_results(self, results, path):
|
||||
'''Save the output results from the Tempest run as a JSON file'''
|
||||
file = open(path, "w+")
|
||||
file.write(json.dumps(results, indent=4, separators=(',', ': ')))
|
||||
file.close()
|
||||
|
||||
def _user_query(self, q):
|
||||
"""Ask user a query. Return true if user agreed (yes/y)"""
|
||||
if self.args.quiet:
|
||||
return True
|
||||
try:
|
||||
inp = moves.input(q + ' (yes/y): ')
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
else:
|
||||
return inp.lower() in ('yes', 'y')
|
||||
|
||||
def _upload_prompt(self, upload_content):
|
||||
if self._user_query('Test results will be uploaded to %s. '
|
||||
'Ok?' % self.args.url):
|
||||
self.post_results(self.args.url, upload_content,
|
||||
sign_with=self.args.priv_key)
|
||||
|
||||
def get_passed_tests(self, result_file):
|
||||
'''Get a list of tests IDs that passed Tempest from a subunit file.'''
|
||||
subunit_processor = SubunitProcessor(result_file)
|
||||
results = subunit_processor.process_stream()
|
||||
return results
|
||||
|
||||
def post_results(self, url, content, sign_with=None):
|
||||
'''Post the combined results back to the refstack server.'''
|
||||
endpoint = '%s/v1/results/' % url
|
||||
headers = {'Content-type': 'application/json'}
|
||||
data = json.dumps(content)
|
||||
self.logger.debug('API request content: %s ' % content)
|
||||
if sign_with:
|
||||
with open(sign_with) as private_key_file:
|
||||
try:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
private_key_file.read().encode('utf-8'),
|
||||
password=None,
|
||||
backend=backends.default_backend())
|
||||
except (IOError, UnsupportedAlgorithm, ValueError) as e:
|
||||
self.logger.info('Error during upload key pair %s' %
|
||||
private_key_file)
|
||||
self.logger.exception(e)
|
||||
return
|
||||
signature = private_key.sign(
|
||||
data.encode('utf-8'),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
pubkey = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.OpenSSH,
|
||||
serialization.PublicFormat.OpenSSH)
|
||||
|
||||
headers['X-Signature'] = binascii.b2a_hex(signature)
|
||||
headers['X-Public-Key'] = pubkey
|
||||
try:
|
||||
response = requests.post(endpoint,
|
||||
data=data,
|
||||
headers=headers,
|
||||
verify=not self.args.insecure)
|
||||
self.logger.info(endpoint + " Response: " + str(response.text))
|
||||
except Exception as e:
|
||||
self.logger.info('Failed to post %s - %s ' % (endpoint, e))
|
||||
self.logger.exception(e)
|
||||
return
|
||||
|
||||
if response.status_code == 201:
|
||||
resp = response.json()
|
||||
print('Test results uploaded!\nURL: %s' % resp.get('url', ''))
|
||||
|
||||
def generate_tempest_config(self):
|
||||
'''Generate tempest.conf for a deployed OpenStack Cloud.'''
|
||||
start_time = time.time()
|
||||
|
||||
# Write tempest.conf in refstack_client/etc folder
|
||||
if not self.args.out:
|
||||
config_path = os.path.join(self.refstack_dir, 'etc/tempest.conf')
|
||||
else:
|
||||
config_path = self.args.out
|
||||
self.logger.info("Generating in %s" % config_path)
|
||||
|
||||
# Generate Tempest configuration
|
||||
try:
|
||||
cloud_creds = main.get_cloud_creds(self.args)
|
||||
except KE.MissingRequiredOptions as e:
|
||||
self.logger.error("Credentials are not sourced - %s" % e)
|
||||
except OSE.ConfigException:
|
||||
self.logger.error("Named cloud %s was not found"
|
||||
% self.args.os_cloud)
|
||||
|
||||
# tempestconf arguments
|
||||
kwargs = {'non_admin': True,
|
||||
'test_accounts': self.args.test_accounts,
|
||||
'image_path': self.args.image,
|
||||
'network_id': self.args.network_id,
|
||||
'out': config_path,
|
||||
'cloud_creds': cloud_creds}
|
||||
|
||||
# Look for extra overrides to be replaced in tempest.conf
|
||||
# (TODO:chkumar246) volume-feature-enabled.api_v2=True is deprecated
|
||||
# in ROCKY release, but it is required for interop tests and it is out
|
||||
# of scope of python-tempestconf, adding it hardcoded here as a extra
|
||||
# overrides.
|
||||
cinder_overrides = "volume-feature-enabled.api_v2=True"
|
||||
overrides_format = cinder_overrides.replace('=', ',').split(',')
|
||||
overrides = []
|
||||
if self.args.overrides:
|
||||
overrides = self.args.overrides.replace('=', ',').split(',')
|
||||
if cinder_overrides not in self.args.overrides:
|
||||
overrides = overrides + overrides_format
|
||||
else:
|
||||
overrides = overrides_format
|
||||
kwargs.update({'overrides': main.parse_overrides(overrides)})
|
||||
|
||||
# Generate accounts.yaml if accounts.file is not given
|
||||
if not self.args.test_accounts:
|
||||
account_file = os.path.join(self.refstack_dir, 'etc/accounts.yaml')
|
||||
kwargs.update({'create_accounts_file': account_file})
|
||||
self.logger.info('Account file will be generated at %s.'
|
||||
% account_file)
|
||||
|
||||
# Generate tempest.conf
|
||||
main.config_tempest(**kwargs)
|
||||
|
||||
if os.path.isfile(config_path):
|
||||
end_time = time.time()
|
||||
elapsed = end_time - start_time
|
||||
duration = int(elapsed)
|
||||
self.logger.info('Tempest Configuration successfully generated '
|
||||
'in %s second at %s' % (duration, config_path))
|
||||
else:
|
||||
try:
|
||||
import config_tempest # noqa
|
||||
self.logging.warning('There is an error in syntax, please '
|
||||
'check $ refstack-client config -h')
|
||||
except ImportError:
|
||||
self.logger.warning('Please make sure python-tempestconf'
|
||||
'python package is installed')
|
||||
|
||||
def test(self):
|
||||
'''Execute Tempest test against the cloud.'''
|
||||
self._prep_test()
|
||||
results_file = self._get_next_stream_subunit_output_file(
|
||||
self.tempest_dir)
|
||||
keystone_config = self._get_keystone_config(self.conf)
|
||||
auth_version, auth_url, content = \
|
||||
self._generate_keystone_data(keystone_config)
|
||||
cpid = self._get_cpid_from_keystone(auth_version, auth_url, content)
|
||||
|
||||
self.logger.info("Starting Tempest test...")
|
||||
start_time = time.time()
|
||||
|
||||
# Run tempest run command, conf file specified at _prep_test method
|
||||
# Use virtual environment (wrapper script)
|
||||
# Run the tests serially if parallel not enabled (--serial).
|
||||
wrapper = os.path.join(self.tempest_dir, 'tools', 'with_venv.sh')
|
||||
cmd = [wrapper, 'tempest', 'run']
|
||||
if not self.args.parallel:
|
||||
cmd.append('--serial')
|
||||
# If a test list was specified, have it take precedence.
|
||||
if self.args.test_list:
|
||||
self.logger.info("Normalizing test list...")
|
||||
parser = TestListParser(os.path.abspath(self.tempest_dir),
|
||||
insecure=self.args.insecure)
|
||||
# get include list
|
||||
list_file = parser.create_include_list(self.args.test_list)
|
||||
if list_file:
|
||||
if os.path.getsize(list_file) > 0:
|
||||
# TODO(kopecmartin) rename the below argument when
|
||||
# refstack-client uses tempest which contains the following
|
||||
# change in its code:
|
||||
# https://review.opendev.org/c/openstack/tempest/+/768583
|
||||
cmd += ('--whitelist_file', list_file)
|
||||
else:
|
||||
self.logger.error("Test list is either empty or no valid "
|
||||
"test cases for the tempest "
|
||||
"environment were found.")
|
||||
exit(1)
|
||||
else:
|
||||
self.logger.error("Error normalizing passed in test list.")
|
||||
exit(1)
|
||||
elif 'arbitrary_args' in self.args:
|
||||
# Additional arguments for tempest run
|
||||
# otherwise run all Tempest API tests.
|
||||
# keep usage(-- testCaseName)
|
||||
tmp = self.args.arbitrary_args[1:]
|
||||
if tmp:
|
||||
cmd += (tmp if tmp[0].startswith('-') else ['--regex'] + tmp)
|
||||
else:
|
||||
cmd += ['--regex', "tempest.api"]
|
||||
|
||||
# If there were two verbose flags, show tempest results.
|
||||
if self.args.verbose > 0:
|
||||
stderr = None
|
||||
else:
|
||||
# Suppress tempest results output. Note that testr prints
|
||||
# results to stderr.
|
||||
stderr = open(os.devnull, 'w')
|
||||
|
||||
# Execute the tempest run command in a subprocess.
|
||||
os.chdir(self.tempest_dir)
|
||||
process = subprocess.Popen(cmd, stderr=stderr)
|
||||
process.communicate()
|
||||
os.chdir(self.refstack_dir)
|
||||
|
||||
# If the subunit file was created, then test cases were executed via
|
||||
# tempest run and there is test output to process.
|
||||
if os.path.isfile(results_file):
|
||||
end_time = time.time()
|
||||
elapsed = end_time - start_time
|
||||
duration = int(elapsed)
|
||||
|
||||
self.logger.info('Tempest test complete.')
|
||||
self.logger.info('Subunit results located in: %s' % results_file)
|
||||
|
||||
results = self.get_passed_tests(results_file)
|
||||
self.logger.info("Number of passed tests: %d" % len(results))
|
||||
|
||||
content = self._form_result_content(cpid, duration, results)
|
||||
|
||||
if self.args.result_tag:
|
||||
file_name = os.path.basename(results_file)
|
||||
directory = os.path.dirname(results_file)
|
||||
file_name = '-'.join([self.args.result_tag, file_name])
|
||||
results_file = os.path.join(directory, file_name)
|
||||
|
||||
json_path = results_file + ".json"
|
||||
self._save_json_results(content, json_path)
|
||||
self.logger.info('JSON results saved in: %s' % json_path)
|
||||
|
||||
# If the user specified the upload argument, then post
|
||||
# the results.
|
||||
if self.args.upload:
|
||||
self.post_results(self.args.url, content,
|
||||
sign_with=self.args.priv_key)
|
||||
else:
|
||||
msg1 = ("tempest run command did not generate a results file "
|
||||
"under the Refstack os.path.dirname(results_file) "
|
||||
"directory. Review command and try again.")
|
||||
msg2 = ("Problem executing tempest run command. Results file "
|
||||
"not generated hence no file to upload. Review "
|
||||
"arbitrary arguments.")
|
||||
if process.returncode != 0:
|
||||
self.logger.warning(msg1)
|
||||
if self.args.upload:
|
||||
self.logger.error(msg2)
|
||||
|
||||
return process.returncode
|
||||
|
||||
def upload(self):
|
||||
'''Perform upload to RefStack URL.'''
|
||||
self._prep_upload()
|
||||
json_file = open(self.upload_file)
|
||||
json_data = json.load(json_file)
|
||||
json_file.close()
|
||||
self._upload_prompt(json_data)
|
||||
|
||||
def upload_subunit(self):
|
||||
'''Perform upload to RefStack URL from a subunit file.'''
|
||||
self._prep_upload()
|
||||
|
||||
cpid = self._generate_cpid_from_endpoint(self.args.keystone_endpoint)
|
||||
# Forgo the duration for direct subunit uploads.
|
||||
duration = 0
|
||||
|
||||
# Formulate JSON from subunit
|
||||
results = self.get_passed_tests(self.upload_file)
|
||||
self.logger.info('Number of passed tests in given subunit '
|
||||
'file: %d ' % len(results))
|
||||
|
||||
content = self._form_result_content(cpid, duration, results)
|
||||
self._upload_prompt(content)
|
||||
|
||||
def yield_results(self, url, start_page=1,
|
||||
start_date='', end_date='', cpid=''):
|
||||
endpoint = '%s/v1/results/' % url
|
||||
headers = {'Content-type': 'application/json'}
|
||||
for page in itertools.count(start_page):
|
||||
params = {'page': page}
|
||||
for param in ('start_date', 'end_date', 'cpid'):
|
||||
if locals()[param]:
|
||||
params.update({param: locals()[param]})
|
||||
try:
|
||||
resp = requests.get(endpoint, headers=headers, params=params,
|
||||
verify=not self.args.insecure)
|
||||
resp.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
self.logger.info('Failed to list %s - %s ' % (endpoint, e))
|
||||
raise StopIteration
|
||||
else:
|
||||
resp = resp.json()
|
||||
results = resp.get('results', [])
|
||||
yield results
|
||||
if resp['pagination']['total_pages'] == page:
|
||||
raise StopIteration
|
||||
|
||||
def list(self):
|
||||
"""Retrieve list with last test results from RefStack."""
|
||||
results = self.yield_results(self.args.url,
|
||||
start_date=self.args.start_date,
|
||||
end_date=self.args.end_date)
|
||||
for page_of_results in results:
|
||||
for r in page_of_results:
|
||||
print('%s - %s' % (r['created_at'], r['url']))
|
||||
try:
|
||||
moves.input('Press Enter to go to next page...')
|
||||
except KeyboardInterrupt:
|
||||
return
|
||||
|
||||
def _sign_pubkey(self):
|
||||
"""Generate self signature for public key"""
|
||||
private_key_file = self.args.priv_key_to_sign
|
||||
try:
|
||||
with open(private_key_file) as pkf:
|
||||
private_key = serialization.load_pem_private_key(
|
||||
pkf.read().encode('utf-8'),
|
||||
password=None,
|
||||
backend=backends.default_backend())
|
||||
except (IOError, UnsupportedAlgorithm, ValueError) as e:
|
||||
self.logger.error('Error reading private key %s'
|
||||
'' % private_key_file)
|
||||
self.logger.exception(e)
|
||||
return
|
||||
|
||||
public_key_file = '.'.join((private_key_file, 'pub'))
|
||||
try:
|
||||
with open(public_key_file) as pkf:
|
||||
# Strip key comment at the end as it should not be included
|
||||
pub_key_elements = pkf.read().split(' ')
|
||||
pub_key = "%s %s" % (pub_key_elements[0], pub_key_elements[1])
|
||||
except IOError:
|
||||
self.logger.error('Public key file %s not found. '
|
||||
'Public key is generated from private one.'
|
||||
'' % public_key_file)
|
||||
pub_key = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.OpenSSH,
|
||||
serialization.PublicFormat.OpenSSH)
|
||||
|
||||
signature = private_key.sign(
|
||||
'signature'.encode('utf-8'),
|
||||
padding.PKCS1v15(),
|
||||
hashes.SHA256()
|
||||
)
|
||||
return pub_key, binascii.b2a_hex(signature)
|
||||
|
||||
def self_sign(self):
|
||||
"""Generate signature for public key."""
|
||||
pub_key, signature = self._sign_pubkey()
|
||||
print('Public key:\n%s\n' % pub_key)
|
||||
print('Self signature:\n%s\n' % str(signature, 'utf-8'))
|
||||
|
||||
|
||||
def parse_cli_args(args=None):
|
||||
|
||||
usage_string = ('refstack-client [-h] <ARG> ...\n\n'
|
||||
'To see help on specific argument, do:\n'
|
||||
'refstack-client <ARG> -h')
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='RefStack-client arguments',
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
usage=usage_string
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers(help='Available subcommands.')
|
||||
|
||||
# Arguments that go with all subcommands.
|
||||
shared_args = argparse.ArgumentParser(add_help=False)
|
||||
group = shared_args.add_mutually_exclusive_group()
|
||||
group.add_argument('-s', '--silent',
|
||||
action='store_true',
|
||||
help='Suppress output except warnings and errors.')
|
||||
|
||||
group.add_argument('-v', '--verbose',
|
||||
action='count',
|
||||
default=0,
|
||||
help='Show verbose output.')
|
||||
|
||||
shared_args.add_argument('-y',
|
||||
action='store_true',
|
||||
dest='quiet',
|
||||
required=False,
|
||||
help='Assume Yes to all prompt queries')
|
||||
|
||||
# Arguments that go with network-related subcommands (test, list, etc.).
|
||||
network_args = argparse.ArgumentParser(add_help=False)
|
||||
network_args.add_argument('--url',
|
||||
action='store',
|
||||
required=False,
|
||||
default=os.environ.get(
|
||||
'REFSTACK_URL',
|
||||
'https://refstack.openstack.org/api'),
|
||||
type=str,
|
||||
help='RefStack API URL to upload results to. '
|
||||
'Defaults to env[REFSTACK_URL] or '
|
||||
'https://refstack.openstack.org/'
|
||||
'api if it is not set '
|
||||
'(--url http://localhost:8000).')
|
||||
|
||||
network_args.add_argument('-k', '--insecure',
|
||||
action='store_true',
|
||||
dest='insecure',
|
||||
required=False,
|
||||
help='Skip SSL checks while interacting '
|
||||
'with RefStack API and Keystone endpoint')
|
||||
|
||||
network_args.add_argument('-i', '--sign',
|
||||
type=str,
|
||||
required=False,
|
||||
dest='priv_key',
|
||||
help='Path to private RSA key. '
|
||||
'OpenSSH RSA keys format supported')
|
||||
|
||||
# Upload command
|
||||
parser_upload = subparsers.add_parser(
|
||||
'upload', parents=[shared_args, network_args],
|
||||
help='Upload an existing result JSON file.'
|
||||
)
|
||||
|
||||
parser_upload.add_argument('file',
|
||||
type=str,
|
||||
help='Path of JSON results file.')
|
||||
|
||||
parser_upload.set_defaults(func="upload")
|
||||
|
||||
# Upload-subunit command
|
||||
parser_subunit_upload = subparsers.add_parser(
|
||||
'upload-subunit', parents=[shared_args, network_args],
|
||||
help='Upload results from a subunit file.'
|
||||
)
|
||||
|
||||
parser_subunit_upload.add_argument('file',
|
||||
type=str,
|
||||
help='Path of subunit file.')
|
||||
|
||||
parser_subunit_upload.add_argument('--keystone-endpoint',
|
||||
action='store',
|
||||
required=True,
|
||||
dest='keystone_endpoint',
|
||||
type=str,
|
||||
help='The Keystone URL of the cloud '
|
||||
'the subunit results belong to. '
|
||||
'This is used to generate a Cloud '
|
||||
'Provider ID.')
|
||||
|
||||
parser_subunit_upload.set_defaults(func="upload_subunit")
|
||||
|
||||
# Config Command
|
||||
parser_config = subparsers.add_parser(
|
||||
'config', parents=[shared_args, network_args],
|
||||
help='Generate tempest.conf for a cloud')
|
||||
|
||||
parser_config.add_argument('--use-test-accounts',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='test_accounts',
|
||||
type=str,
|
||||
help='Path of the accounts.yaml file.')
|
||||
|
||||
parser_config.add_argument('--network-id',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='network_id',
|
||||
help='The ID of an existing network in our '
|
||||
'openstack instance with external '
|
||||
'connectivity')
|
||||
|
||||
parser_config.add_argument('--image',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='image',
|
||||
default=C.DEFAULT_IMAGE,
|
||||
help='An image name chosen from `$ openstack '
|
||||
'image list` or a filepath/URL of an '
|
||||
'image to be uploaded to glance and set '
|
||||
'as a reference to be used by tests. The '
|
||||
'name of the image is the leaf name of '
|
||||
'the path. Default is %s'
|
||||
% C.DEFAULT_IMAGE)
|
||||
|
||||
parser_config.add_argument('--out',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='out',
|
||||
help='File path to write tempest.conf')
|
||||
|
||||
parser_config.add_argument('--os-cloud',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='os_cloud',
|
||||
help='Named cloud to connect to.')
|
||||
|
||||
parser_config.add_argument('--overrides',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='overrides',
|
||||
help='Comma seperated values which needs to be'
|
||||
'overridden in tempest.conf.'
|
||||
'Example --overrides'
|
||||
'compute.image_ref=<value>,'
|
||||
'compute.flavor_ref=<value>')
|
||||
|
||||
parser_config.set_defaults(func='generate_tempest_config')
|
||||
|
||||
# Test command
|
||||
parser_test = subparsers.add_parser(
|
||||
'test', parents=[shared_args, network_args],
|
||||
help='Run Tempest against a cloud.')
|
||||
|
||||
parser_test.add_argument('-c', '--conf-file',
|
||||
action='store',
|
||||
required=True,
|
||||
dest='conf_file',
|
||||
type=str,
|
||||
help='Path of the Tempest configuration file to '
|
||||
'use.')
|
||||
|
||||
parser_test.add_argument('-r', '--result-file-tag',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='result_tag',
|
||||
type=str,
|
||||
help='Specify a string to prefix the result '
|
||||
'file with to easier distinguish them. ')
|
||||
|
||||
parser_test.add_argument('--test-list',
|
||||
action='store',
|
||||
required=False,
|
||||
dest='test_list',
|
||||
type=str,
|
||||
help='Specify the file path or URL of a test '
|
||||
'list text file. This test list will '
|
||||
'contain specific test cases that should '
|
||||
'be tested.')
|
||||
|
||||
parser_test.add_argument('-u', '--upload',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='After running Tempest, upload the test '
|
||||
'results to the default RefStack API server '
|
||||
'or the server specified by --url.')
|
||||
|
||||
parser_test.add_argument('-p', '--parallel',
|
||||
action='store_true',
|
||||
required=False,
|
||||
help='Run the tests in parallel. Note this '
|
||||
'requires multiple users/projects in '
|
||||
'accounts.yaml.')
|
||||
|
||||
# This positional argument will allow arbitrary arguments to be passed in
|
||||
# with the usage of '--'.
|
||||
parser_test.add_argument('arbitrary_args',
|
||||
nargs=argparse.REMAINDER,
|
||||
help='After the first "--", you can pass '
|
||||
'arbitrary arguments to the tempest run '
|
||||
'runner. This can be used for running '
|
||||
'specific test cases or test lists. '
|
||||
'Some examples are: -- --regex '
|
||||
'tempest.api.compute.images.'
|
||||
'test_list_image_filters')
|
||||
parser_test.set_defaults(func="test")
|
||||
|
||||
# List command
|
||||
parser_list = subparsers.add_parser(
|
||||
'list', parents=[shared_args, network_args],
|
||||
help='List last results from RefStack')
|
||||
parser_list.add_argument('--start-date',
|
||||
required=False,
|
||||
dest='start_date',
|
||||
type=str,
|
||||
help='Specify a date for start listing of '
|
||||
'test results '
|
||||
'(e.g. --start-date "2015-04-24 01:23:56").')
|
||||
parser_list.add_argument('--end-date',
|
||||
required=False,
|
||||
dest='end_date',
|
||||
type=str,
|
||||
help='Specify a date for end listing of '
|
||||
'test results '
|
||||
'(e.g. --end-date "2015-04-24 01:23:56").')
|
||||
parser_list.set_defaults(func='list')
|
||||
|
||||
# Sign command
|
||||
parser_sign = subparsers.add_parser(
|
||||
'sign', parents=[shared_args],
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
help='Generate signature for public key.')
|
||||
parser_sign.add_argument('priv_key_to_sign',
|
||||
type=str,
|
||||
help='Path to private RSA key. '
|
||||
'OpenSSH RSA keys format supported')
|
||||
|
||||
parser_sign.set_defaults(func='self_sign')
|
||||
|
||||
return parser.parse_args(args=args)
|
||||
|
||||
|
||||
def entry_point():
|
||||
args = parse_cli_args()
|
||||
test = RefstackClient(args)
|
||||
raise SystemExit(getattr(test, args.func)())
|
||||
@@ -1,397 +0,0 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# This script aims to configure an initial OpenStack environment with all the
|
||||
# necessary configurations for tempest's run using nothing but OpenStack's
|
||||
# native API.
|
||||
# That includes, creating users, tenants, registering images (cirros),
|
||||
# configuring neutron and so on.
|
||||
#
|
||||
# ASSUMPTION: this script is run by an admin user as it is meant to configure
|
||||
# the OpenStack environment prior to actual use.
|
||||
|
||||
# Config
|
||||
import ConfigParser
|
||||
import os
|
||||
import tarfile
|
||||
|
||||
from six.moves.urllib import request as urlreq
|
||||
|
||||
# Default client libs
|
||||
import glanceclient as glance_client
|
||||
import keystoneclient.v2_0.client as keystone_client
|
||||
|
||||
# Import OpenStack exceptions
|
||||
import glanceclient.exc as glance_exception
|
||||
import keystoneclient.exceptions as keystone_exception
|
||||
|
||||
|
||||
TEMPEST_TEMP_DIR = os.getenv("TEMPEST_TEMP_DIR", "/tmp").rstrip('/')
|
||||
TEMPEST_ROOT_DIR = os.getenv("TEMPEST_ROOT_DIR", os.getenv("HOME")).rstrip('/')
|
||||
|
||||
# Environment variables override defaults
|
||||
TEMPEST_CONFIG_DIR = os.getenv("TEMPEST_CONFIG_DIR",
|
||||
"%s%s" % (TEMPEST_ROOT_DIR, "/etc")).rstrip('/')
|
||||
TEMPEST_CONFIG_FILE = os.getenv("TEMPEST_CONFIG_FILE",
|
||||
"%s%s" % (TEMPEST_CONFIG_DIR, "/tempest.conf"))
|
||||
TEMPEST_CONFIG_SAMPLE = os.getenv("TEMPEST_CONFIG_SAMPLE",
|
||||
"%s%s" % (TEMPEST_CONFIG_DIR,
|
||||
"/tempest.conf.sample"))
|
||||
# Image references
|
||||
IMAGE_DOWNLOAD_CHUNK_SIZE = 8 * 1024
|
||||
IMAGE_UEC_SOURCE_URL = os.getenv("IMAGE_UEC_SOURCE_URL",
|
||||
"http://download.cirros-cloud.net/0.3.1/"
|
||||
"cirros-0.3.1-x86_64-uec.tar.gz")
|
||||
TEMPEST_IMAGE_ID = os.getenv('IMAGE_ID')
|
||||
TEMPEST_IMAGE_ID_ALT = os.getenv('IMAGE_ID_ALT')
|
||||
IMAGE_STATUS_ACTIVE = 'active'
|
||||
|
||||
|
||||
class ClientManager(object):
|
||||
"""
|
||||
Manager that provides access to the official python clients for
|
||||
calling various OpenStack APIs.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.identity_client = None
|
||||
self.image_client = None
|
||||
self.network_client = None
|
||||
self.compute_client = None
|
||||
self.volume_client = None
|
||||
|
||||
def get_identity_client(self, **kwargs):
|
||||
"""
|
||||
Returns the openstack identity python client
|
||||
:param username: a string representing the username
|
||||
:param password: a string representing the user's password
|
||||
:param tenant_name: a string representing the tenant name of the user
|
||||
:param auth_url: a string representing the auth url of the identity
|
||||
:param insecure: True if we wish to disable ssl certificate validation,
|
||||
False otherwise
|
||||
:returns an instance of openstack identity python client
|
||||
"""
|
||||
if not self.identity_client:
|
||||
self.identity_client = keystone_client.Client(**kwargs)
|
||||
|
||||
return self.identity_client
|
||||
|
||||
def get_image_client(self, version="1", *args, **kwargs):
|
||||
"""
|
||||
This method returns OpenStack glance python client
|
||||
:param version: a string representing the version of the glance client
|
||||
to use.
|
||||
:param string endpoint: A user-supplied endpoint URL for the glance
|
||||
service.
|
||||
:param string token: Token for authentication.
|
||||
:param integer timeout: Allows customization of the timeout for client
|
||||
http requests. (optional)
|
||||
:return: a Client object representing the glance client
|
||||
"""
|
||||
if not self.image_client:
|
||||
self.image_client = glance_client.Client(version, *args, **kwargs)
|
||||
|
||||
return self.image_client
|
||||
|
||||
|
||||
def get_tempest_config(path_to_config):
|
||||
"""
|
||||
Gets the tempest configuration file as a ConfigParser object
|
||||
:param path_to_config: path to the config file
|
||||
:return: a ConfigParser object representing the tempest configuration file
|
||||
"""
|
||||
# get the sample config file from the sample
|
||||
config = ConfigParser.ConfigParser()
|
||||
config.readfp(open(path_to_config))
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def update_config_admin_credentials(config, config_section):
|
||||
"""
|
||||
Updates the tempest config with the admin credentials
|
||||
:param config: a ConfigParser object representing the tempest config file
|
||||
:param config_section: the section name where the admin credentials are
|
||||
"""
|
||||
# Check if credentials are present, default uses the config credentials
|
||||
OS_USERNAME = os.getenv('OS_USERNAME',
|
||||
config.get(config_section, "admin_username"))
|
||||
OS_PASSWORD = os.getenv('OS_PASSWORD',
|
||||
config.get(config_section, "admin_password"))
|
||||
OS_TENANT_NAME = os.getenv('OS_TENANT_NAME',
|
||||
config.get(config_section, "admin_tenant_name"))
|
||||
OS_AUTH_URL = os.getenv('OS_AUTH_URL', config.get(config_section, "uri"))
|
||||
|
||||
if not (OS_AUTH_URL and
|
||||
OS_USERNAME and
|
||||
OS_PASSWORD and
|
||||
OS_TENANT_NAME):
|
||||
raise Exception("Admin environment variables not found.")
|
||||
|
||||
# TODO(tkammer): Add support for uri_v3
|
||||
config_identity_params = {'uri': OS_AUTH_URL,
|
||||
'admin_username': OS_USERNAME,
|
||||
'admin_password': OS_PASSWORD,
|
||||
'admin_tenant_name': OS_TENANT_NAME}
|
||||
|
||||
update_config_section_with_params(config,
|
||||
config_section,
|
||||
config_identity_params)
|
||||
|
||||
|
||||
def update_config_section_with_params(config, config_section, params):
|
||||
"""
|
||||
Updates a given config object with given params
|
||||
:param config: a ConfigParser object representing the tempest config file
|
||||
:param config_section: the section we would like to update
|
||||
:param params: the parameters we wish to update for that section
|
||||
"""
|
||||
for option, value in params.items():
|
||||
config.set(config_section, option, value)
|
||||
|
||||
|
||||
def get_identity_client_kwargs(config, config_section):
|
||||
"""
|
||||
Get the required arguments for the identity python client
|
||||
:param config: a ConfigParser object representing the tempest config file
|
||||
:param config_section: the section name in the configuration where the
|
||||
arguments can be found
|
||||
:return: a dictionary representing the needed arguments for the identity
|
||||
client
|
||||
"""
|
||||
username = config.get(config_section, 'admin_username')
|
||||
password = config.get(config_section, 'admin_password')
|
||||
tenant_name = config.get(config_section, 'admin_tenant_name')
|
||||
auth_url = config.get(config_section, 'uri')
|
||||
dscv = config.get(config_section, 'disable_ssl_certificate_validation')
|
||||
kwargs = {'username': username,
|
||||
'password': password,
|
||||
'tenant_name': tenant_name,
|
||||
'auth_url': auth_url,
|
||||
'insecure': dscv}
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def create_user_with_tenant(identity_client, username, password, tenant_name):
|
||||
"""
|
||||
Creates a user using a given identity client
|
||||
:param identity_client: openstack identity python client
|
||||
:param username: a string representing the username
|
||||
:param password: a string representing the user's password
|
||||
:param tenant_name: a string representing the tenant name of the user
|
||||
"""
|
||||
# Try to create the necessary tenant
|
||||
tenant_id = None
|
||||
try:
|
||||
tenant_description = "Tenant for Tempest %s user" % username
|
||||
tenant = identity_client.tenants.create(tenant_name,
|
||||
tenant_description)
|
||||
tenant_id = tenant.id
|
||||
except keystone_exception.Conflict:
|
||||
|
||||
# if already exist, use existing tenant
|
||||
tenant_list = identity_client.tenants.list()
|
||||
for tenant in tenant_list:
|
||||
if tenant.name == tenant_name:
|
||||
tenant_id = tenant.id
|
||||
|
||||
# Try to create the user
|
||||
try:
|
||||
email = "%s@test.com" % username
|
||||
identity_client.users.create(name=username,
|
||||
password=password,
|
||||
email=email,
|
||||
tenant_id=tenant_id)
|
||||
except keystone_exception.Conflict:
|
||||
|
||||
# if already exist, use existing user
|
||||
pass
|
||||
|
||||
|
||||
def create_users_and_tenants(identity_client,
|
||||
config,
|
||||
config_section):
|
||||
"""
|
||||
Creates the two non admin users and tenants for tempest
|
||||
:param identity_client: openstack identity python client
|
||||
:param config: a ConfigParser object representing the tempest config file
|
||||
:param config_section: the section name of identity in the config
|
||||
"""
|
||||
# Get the necessary params from the config file
|
||||
tenant_name = config.get(config_section, 'tenant_name')
|
||||
username = config.get(config_section, 'username')
|
||||
password = config.get(config_section, 'password')
|
||||
|
||||
alt_tenant_name = config.get(config_section, 'alt_tenant_name')
|
||||
alt_username = config.get(config_section, 'alt_username')
|
||||
alt_password = config.get(config_section, 'alt_password')
|
||||
|
||||
# Create the necessary users for the test runs
|
||||
create_user_with_tenant(identity_client, username, password, tenant_name)
|
||||
create_user_with_tenant(identity_client, alt_username, alt_password,
|
||||
alt_tenant_name)
|
||||
|
||||
|
||||
def get_image_client_kwargs(identity_client, config, config_section):
|
||||
"""
|
||||
Get the required arguments for the image python client
|
||||
:param identity_client: openstack identity python client
|
||||
:param config: a ConfigParser object representing the tempest config file
|
||||
:param config_section: the section name of identity in the config
|
||||
:return: a dictionary representing the needed arguments for the image
|
||||
client
|
||||
"""
|
||||
|
||||
token = identity_client.auth_token
|
||||
endpoint = identity_client.\
|
||||
service_catalog.url_for(service_type='image', endpoint_type='publicURL'
|
||||
)
|
||||
dscv = config.get(config_section, 'disable_ssl_certificate_validation')
|
||||
kwargs = {'endpoint': endpoint,
|
||||
'token': token,
|
||||
'insecure': dscv}
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
def images_exist(image_client):
|
||||
"""
|
||||
Checks whether the images ID's located in the environment variable are
|
||||
indeed registered
|
||||
:param image_client: the openstack python client representing the image
|
||||
client
|
||||
"""
|
||||
exist = True
|
||||
if not TEMPEST_IMAGE_ID or not TEMPEST_IMAGE_ID_ALT:
|
||||
exist = False
|
||||
else:
|
||||
try:
|
||||
image_client.images.get(TEMPEST_IMAGE_ID)
|
||||
image_client.images.get(TEMPEST_IMAGE_ID_ALT)
|
||||
except glance_exception.HTTPNotFound:
|
||||
exist = False
|
||||
|
||||
return exist
|
||||
|
||||
|
||||
def download_and_register_uec_images(image_client, download_url,
|
||||
download_folder):
|
||||
"""
|
||||
Downloads and registered the UEC AKI/AMI/ARI images
|
||||
:param image_client:
|
||||
:param download_url: the url of the uec tar file
|
||||
:param download_folder: the destination folder we wish to save the file to
|
||||
"""
|
||||
basename = os.path.basename(download_url)
|
||||
path = os.path.join(download_folder, basename)
|
||||
|
||||
request = urlreq.urlopen(download_url)
|
||||
|
||||
# First, download the file
|
||||
with open(path, "wb") as fp:
|
||||
while True:
|
||||
chunk = request.read(IMAGE_DOWNLOAD_CHUNK_SIZE)
|
||||
if not chunk:
|
||||
break
|
||||
|
||||
fp.write(chunk)
|
||||
|
||||
# Then extract and register images
|
||||
tar = tarfile.open(path, "r")
|
||||
for name in tar.getnames():
|
||||
file_obj = tar.extractfile(name)
|
||||
format = "aki"
|
||||
|
||||
if file_obj.name.endswith(".img"):
|
||||
format = "ami"
|
||||
|
||||
if file_obj.name.endswith("initrd"):
|
||||
format = "ari"
|
||||
|
||||
# Register images in image client
|
||||
image_client.images.create(name=file_obj.name, disk_format=format,
|
||||
container_format=format, data=file_obj,
|
||||
is_public="true")
|
||||
|
||||
tar.close()
|
||||
|
||||
|
||||
def create_images(image_client, config, config_section,
|
||||
download_url=IMAGE_UEC_SOURCE_URL,
|
||||
download_folder=TEMPEST_TEMP_DIR):
|
||||
"""
|
||||
Creates images for tempest's use and registers the environment variables
|
||||
IMAGE_ID and IMAGE_ID_ALT with registered images
|
||||
:param image_client: OpenStack python image client
|
||||
:param config: a ConfigParser object representing the tempest config file
|
||||
:param config_section: the section name where the IMAGE ids are set
|
||||
:param download_url: the URL from which we should download the UEC tar
|
||||
:param download_folder: the place where we want to save the download file
|
||||
"""
|
||||
if not images_exist(image_client):
|
||||
# Falls down to the default uec images
|
||||
download_and_register_uec_images(image_client, download_url,
|
||||
download_folder)
|
||||
image_ids = []
|
||||
for image in image_client.images.list():
|
||||
image_ids.append(image.id)
|
||||
|
||||
os.environ["IMAGE_ID"] = image_ids[0]
|
||||
os.environ["IMAGE_ID_ALT"] = image_ids[1]
|
||||
|
||||
params = {'image_ref': os.getenv("IMAGE_ID"),
|
||||
'image_ref_alt': os.getenv("IMAGE_ID_ALT")}
|
||||
|
||||
update_config_section_with_params(config, config_section, params)
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
Main module to control the script
|
||||
"""
|
||||
# Check if config file exists or fall to the default sample otherwise
|
||||
path_to_config = TEMPEST_CONFIG_SAMPLE
|
||||
|
||||
if os.path.isfile(TEMPEST_CONFIG_FILE):
|
||||
path_to_config = TEMPEST_CONFIG_FILE
|
||||
|
||||
config = get_tempest_config(path_to_config)
|
||||
update_config_admin_credentials(config, 'identity')
|
||||
|
||||
client_manager = ClientManager()
|
||||
|
||||
# Set the identity related info for tempest
|
||||
identity_client_kwargs = get_identity_client_kwargs(config,
|
||||
'identity')
|
||||
identity_client = client_manager.get_identity_client(
|
||||
**identity_client_kwargs)
|
||||
|
||||
# Create the necessary users and tenants for tempest run
|
||||
create_users_and_tenants(identity_client, config, 'identity')
|
||||
|
||||
# Set the image related info for tempest
|
||||
image_client_kwargs = get_image_client_kwargs(identity_client,
|
||||
config,
|
||||
'identity')
|
||||
image_client = client_manager.get_image_client(**image_client_kwargs)
|
||||
|
||||
# Create the necessary users and tenants for tempest run
|
||||
create_images(image_client, config, 'compute')
|
||||
|
||||
# TODO(tkammer): add network implementation
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,80 +0,0 @@
|
||||
# Copyright 2014 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import os
|
||||
import re
|
||||
import subunit
|
||||
import testtools
|
||||
import unittest
|
||||
|
||||
|
||||
class TempestSubunitTestResultPassOnly(testtools.TestResult):
|
||||
"""Class to process subunit stream.
|
||||
|
||||
This class maintains a list of test IDs that pass.
|
||||
"""
|
||||
|
||||
def __init__(self, stream, descriptions, verbosity):
|
||||
"""Initialize with super class signature."""
|
||||
super(TempestSubunitTestResultPassOnly, self).__init__()
|
||||
self.results = []
|
||||
|
||||
@staticmethod
|
||||
def get_test_uuid(test):
|
||||
attrs = None
|
||||
try:
|
||||
attrs = test.split('[')[1].split(']')[0].split(',')
|
||||
except IndexError:
|
||||
pass
|
||||
if not attrs:
|
||||
return
|
||||
for attr in attrs:
|
||||
if attr.startswith('id-'):
|
||||
return '-'.join(attr.split('-')[1:])
|
||||
|
||||
def addSuccess(self, testcase):
|
||||
"""Overwrite super class method for additional data processing."""
|
||||
super(TempestSubunitTestResultPassOnly, self).addSuccess(testcase)
|
||||
# Remove any [] from the test ID before appending it.
|
||||
# Will leave in any () for now as they are the only thing discerning
|
||||
# certain test cases.
|
||||
test_result = {'name': str(re.sub(r'\[.*\]', '', testcase.id()))}
|
||||
uuid = self.get_test_uuid(str(testcase.id()))
|
||||
if uuid:
|
||||
test_result['uuid'] = uuid
|
||||
self.results.append(test_result)
|
||||
|
||||
def get_results(self):
|
||||
return self.results
|
||||
|
||||
|
||||
class SubunitProcessor():
|
||||
"""A class to replay subunit data from a stream."""
|
||||
|
||||
def __init__(self, in_stream,
|
||||
result_class=TempestSubunitTestResultPassOnly):
|
||||
self.in_stream = in_stream
|
||||
self.result_class = result_class
|
||||
|
||||
def process_stream(self):
|
||||
"""Read and process subunit data from a stream."""
|
||||
with open(self.in_stream, 'r') as fin:
|
||||
test = subunit.ProtocolTestCase(fin)
|
||||
runner = unittest.TextTestRunner(stream=open(os.devnull, 'w'),
|
||||
resultclass=self.result_class)
|
||||
|
||||
# Run (replay) the test from subunit stream.
|
||||
test_result = runner.run(test)
|
||||
return test_result.get_results()
|
||||
@@ -1 +0,0 @@
|
||||
__author__ = 'dlenwell'
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
cp -r /refstack-client /test
|
||||
cd /test
|
||||
./setup_env
|
||||
/bin/bash -c ". .venv/bin/activate; exec refstack-client test -vv -c tempest.conf -t tempest.api.identity.admin.test_users"
|
||||
@@ -1,61 +0,0 @@
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
|
||||
|
||||
def get_project_path():
|
||||
path = os.path.dirname(os.path.realpath(__file__))
|
||||
while 'setup.py' not in os.listdir(path):
|
||||
path = os.path.realpath(os.path.join(path, '..'))
|
||||
return path
|
||||
|
||||
|
||||
class TestSequenceFunctions(unittest.TestCase):
|
||||
scp_command = ('scp %s:/opt/stack/tempest/etc/'
|
||||
'tempest.conf %s')
|
||||
pull_command = ('docker pull %s')
|
||||
test_command = ('docker run -t -v %s:/refstack-client -w /refstack-client/'
|
||||
'refstack_client/tests/smoke --rm %s ./run_in_docker')
|
||||
|
||||
def run_test(self, distro):
|
||||
subprocess.check_call(self.pull_command % distro, shell=True)
|
||||
subprocess.check_call(self.test_command % (get_project_path(), distro),
|
||||
shell=True)
|
||||
|
||||
def setUp(self):
|
||||
devstack_host = os.environ.get('DEVSTACK_HOST', None)
|
||||
self.assertIsNotNone(devstack_host)
|
||||
subprocess.check_call(
|
||||
self.scp_command % (devstack_host, get_project_path()),
|
||||
shell=True
|
||||
)
|
||||
|
||||
def test_ubuntu_14(self):
|
||||
distro_image = 'ubuntu:12.04'
|
||||
self.run_test(distro_image)
|
||||
|
||||
def test_ubuntu_12(self):
|
||||
distro_image = 'ubuntu:14.04'
|
||||
self.run_test(distro_image)
|
||||
|
||||
def test_centos6(self):
|
||||
distro_image = 'centos:centos6'
|
||||
self.run_test(distro_image)
|
||||
|
||||
def test_centos7(self):
|
||||
distro_image = 'centos:centos7'
|
||||
self.run_test(distro_image)
|
||||
|
||||
def test_fedora_21(self):
|
||||
distro_image = 'fedora:21'
|
||||
self.run_test(distro_image)
|
||||
|
||||
def test_opensuse_13(self):
|
||||
# offcial opensuse image has outdated certificates
|
||||
# we can't use it while this issue isn't fixed
|
||||
distro_image = 'opensuse/13.2'
|
||||
self.run_test(distro_image)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,16 +0,0 @@
|
||||
time: 2014-08-15 10:34:49.010492Z
|
||||
tags: worker-0
|
||||
test: tempest.passed.test
|
||||
time: 2014-08-15 10:34:51.020584Z
|
||||
successful: tempest.passed.test [ multipart
|
||||
]
|
||||
tags: worker-0
|
||||
time: 2014-08-15 10:34:58.010492Z
|
||||
tags: worker-0
|
||||
test: tempest.tagged_passed.test[gate,id-0146f675-ffbd-4208-b3a4-60eb628dbc5e]
|
||||
time: 2014-08-15 10:34:58.020584Z
|
||||
successful: tempest.tagged_passed.test[gate,id-0146f675-ffbd-4208-b3a4-60eb628dbc5e] [ multipart
|
||||
]
|
||||
tags: -worker-0
|
||||
time: 2014-08-15 10:34:58.543400Z
|
||||
tags: worker-0
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"duration_seconds": 0,
|
||||
"cpid": "test-id",
|
||||
"results": [
|
||||
{"name": "tempest.passed.test"},
|
||||
{"name": "tempest.tagged_passed.test",
|
||||
"uuid": "0146f675-ffbd-4208-b3a4-60eb628dbc5e"}
|
||||
]
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
1
|
||||
@@ -1,11 +0,0 @@
|
||||
[identity]
|
||||
auth_version = v2
|
||||
uri = http://0.0.0.0:35357/v2.0
|
||||
uri_v3 = http://0.0.0.0:35357/v3
|
||||
username = admin
|
||||
password = test
|
||||
tenant_id = admin_project_id
|
||||
project_name = project_name
|
||||
|
||||
[identity-feature-enabled]
|
||||
api_v2 = true
|
||||
@@ -1,51 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKAIBAAKCAgEAlbeI0OM9UFFrt0kBOCeeExkptbWw7FofYyqtxBhCWr9T4Pa3
|
||||
PxsGItjlKDcShPZlQa+xbqfIBGB8Gl1Gb7c7W9A3U0EAb3cnfOVMi5c2IFjgPJPu
|
||||
RHkBxEnWi5LgXpugyZ34vJ2q2rV/wFk23JDxTgony6SU8mWJ/0xwz2yQ9JDMJ5F8
|
||||
DdADQ7MtuyhmmRj6yZw0PNzCfmpgXtzq3beeSrtLgptpnQ0UHUn3tJBtc8KWF/vS
|
||||
Gl1izyHM95pFDXys2lHht7OtcQyHS2gQtjei0U70ChyBYbmVtKACYsgonQWrbAd8
|
||||
T+S5Onffnm8K1lDOyeQOrj5rERr4NQUl6kqjW1LInF2TPd+/vk27z0x/jw/0JEoZ
|
||||
YrFXzh4rYXauMa+xO2XIVhppIsJr8LPhVehNNLc3shrWQnn5s2wQOfNJcqCoPCCg
|
||||
7Pn65GSsfeaKMfQc10AjAfAZx6gXo8/zUYiCjqGMITlFHoBfyT25SWEs0dl19hUC
|
||||
fRjW7JmrVWhCKYPiC91qrtHqZLk5U/6YDvh3q+x2a9umyojXRf1fM4gonlmC/poq
|
||||
CHLjlLi+3k3JbytTsV+fIx3h3Cg+CsTGzdZxQZHG2FhjZiCZL/e8UvXidibHAq3F
|
||||
gd2zZWHLPrFi44vQqaosxZQYqob5hVQwqIMGcg7fVZ0s3JkZKAzgj6NZeicCAwEA
|
||||
AQKCAgAXbMX9WPCo9nRSExwbuycieddq1Oi/skIi8/SIL/uB01m+Yxu8xe+p2CHS
|
||||
rvs4zox9QI4UcC/9b1M7lMiGhjbFYMinQA5fYldNVVsqpBgV65H6KHMpR32dRqhI
|
||||
4kw0wUjhAtR+PnUTDz7Ty6Gn1Q3MVg5v8GpVmsmCpmUoLyZm/ZjTwBGW36sDFq/b
|
||||
DrEu1xe2H7iUpT3RJbe6X/pngmlD7BYec06NAhTZmE8nc0mMyS9OxVnUQjRJkFJP
|
||||
k1WmjJFG/3S/l19Vxs4MYFXtDLtu4FmSk28y6SShRD/bUNH5738owesTXQgWO9dD
|
||||
JMCfU7lnIUWiwaVi4cNgGFQcWl1AmWkimg7YryVDTpYS8VEhyD+Nk+Gc3oOq8nZj
|
||||
5hZGH3tJ3ZxbuH3W8LWRrpUAT+jNOTXZ83gGY7wA9tirVW7xNozdrUJUAZ7ajhVs
|
||||
T3ObI7MmPDkMTYXoyFQ/mPOr/kTlB7ONMMPwX4TbLD6uizjLTVa4tTtjMxbGLpQx
|
||||
8qocrNfxVa1BXjFeFWzd3+zrBf4X6rQKJxfAiGSlUPc+Ny9EbaTVc/WzWEmofwRS
|
||||
97MEwiVcVcofXOgACO3OZTfYz8QPRzFEC3E5Xjk2CMwb0uNtr9Oi6j9zFSfuYKWS
|
||||
WHxbvifgMvz07hwNtmr3A6xkoyRr1Y5Kt5x76vhBlKsplaJ1kQKCAQEAxb/H4fG/
|
||||
EpiEWf1JrXVJlVCsDiF17Xl/a8+H4WKaGEecSHWBnofFOIIpP/5GqDVCzzw1afsf
|
||||
o/WRpPUvxq1/1v2qPzLhrMRXeQ5effDvSAQ1SSq2Yc0vI0hVOPuH5LvMga2qFRzB
|
||||
D7p0MQv0K38Uovi8SZ9oo7+F4Gfq/R2RJXtSVyz0A+cE+IBG51J9rdi2H0MNHBpo
|
||||
XSoV5YZ7QhgHeDe4cgW3s2bR+LhvV7IbPQtsGIqvYEclzsSRbrhS/HM6bcRgVONR
|
||||
JiNTnUZZyULWiFR/7Lh6Vf1gHtTZsl2B+OHHxl7uLyUT9ET2V5XgGweKEuswsbi8
|
||||
J9fwSZYv9rs2nQKCAQEAwdGntt0YZD97XdCPH5zLKWqNRQa8YMpZIRLwyh5mELYi
|
||||
HOsjdGbMEunE88uE93cJX7EcFwRVUq+Uf+mAwnKJ8ZwVf10iuIt63yKhtWVREeZa
|
||||
YNfTjj51HHoGgh1c1wi8UUlyY8rdR18cZkIcFxZIeBlPqm4BEm1NUxf2QX+WWP5D
|
||||
WHBiY7CuIDfx9Uxibssm6hy6y0jil2ZXjIXnTHgOvk9w4OkQVhs5eI1mOP//kSga
|
||||
Qfm1uYadwojrdFvsWyTZRaZp6k2UqueZS3pNSM2+/qH6MZaCGO/nMDB9gt+G9cHv
|
||||
WZBtaZ7bxEVGXrw6timSzR1UwbsKosheiWatv2Q2kwKCAQA7790NxtA7Oq8i93qV
|
||||
cK9U6pa70biEuga9DrIIxnIeWdYswDEBc/V7IziNhOy1ny8Y0Q7/iHYWpB/497f7
|
||||
aCsPZuNrNGjijMBWmNxbH+Pm2B+uhZuyGRbogswR8WtHEQTzaUfcDlMWCVWeaBkh
|
||||
9eqzWuD3D7IPr8VMNzMqdQPBcJeMhLuRUzxWdcsH4iDlyIGrCA+5LOflFRR99Tz2
|
||||
04GwFnN5W/JKFigeUwisc/d9kTC6X464h9gVy86o2IWOrv5Otu7by+qUvLBjQyeD
|
||||
sRaFS9daULAD0ECKF8nEHkN+xDBhF/TppTtfFmf0NCExEB/xjAe+VlfxW6ohI7x1
|
||||
9FihAoIBAFh/Rjj00wJTCh1X8UHZ8dnDUSXHYZRAUFoNr+xZ3PiccQ8LPnETzvKD
|
||||
0u4Oa3Qi4iDTWaQY0myixwdwst4WNm4feqFhAU2KQlxID9YnoNCvgWzenzY/xnFu
|
||||
NjKK/a0hy/rBsn1mT4sbHniCjxjrj8Nmqz2CZPLo/XmHY2WcwCV6U326MvKZ5afI
|
||||
Y65BZmB4WhhjbdcMPIosrKT5Lxd3aiPzWfMX9+GZJLCqv5YfLa41xWeCgTto//en
|
||||
VPsYTd9//8URqyLUsaEnhpM0EL3BVAgoJXkm49hHEiSqv2RWc+Ua3BLlI1AqvOXt
|
||||
S6hOAfDTIriNP/oFUWHqY2ARhhvxwgkCggEBALvTiIPceTBiTy+dfC3c28LT5pDO
|
||||
817YXjNlTsO+v/4HT4xkMbhfBDndBNEn4eeWIgTGRl7npja473lJa5PlF5ewGNQM
|
||||
arSI/RTm7gCjvMHoztpyI5RCH7Mg7q7vSUOYrklOGwGW5K8b4iNclixhr9uqrw5a
|
||||
aWaL4IqcnmTMdSKcmtUHg5AX6JN+1or/8UFnH7rFDteJFMVsmnaJT3Z2D/pbpMVN
|
||||
vLRo/sgo32FZCB3YlZwxbVuiNzQcWVacPB1YgUAAhwx13cr0tQubJZQox+hQ0a2P
|
||||
S9alfkakHGcVlDeVhf2ejcVk9hk8mbxAPBy2zsitsJdiabk4T7bSQddPxYI=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
@@ -1,5 +0,0 @@
|
||||
- username: 'admin'
|
||||
tenant_name: 'tenant_name'
|
||||
password: 'test'
|
||||
roles:
|
||||
- 'Member'
|
||||
@@ -1,3 +0,0 @@
|
||||
tempest.api.test1[gate]
|
||||
tempest.api.test2
|
||||
tempest.api.test3[foo,bar](scenario)
|
||||
@@ -1,940 +0,0 @@
|
||||
#
|
||||
# Copyright (c) 2014 Piston Cloud Computing, Inc. All Rights Reserved.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
import httmock
|
||||
from unittest import mock
|
||||
from unittest.mock import MagicMock
|
||||
import unittest
|
||||
|
||||
from refstack_client import refstack_client as rc
|
||||
|
||||
|
||||
class TestRefstackClient(unittest.TestCase):
|
||||
|
||||
test_path = os.path.dirname(os.path.realpath(__file__))
|
||||
conf_file_name = '%s/refstack-client.test.conf' % test_path
|
||||
|
||||
def patch(self, name, **kwargs):
|
||||
"""
|
||||
:param name: Name of class to be patched
|
||||
:param kwargs: directly passed to mock.patch
|
||||
:return: mock
|
||||
"""
|
||||
patcher = mock.patch(name, **kwargs)
|
||||
thing = patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
return thing
|
||||
|
||||
def mock_argv(self, command='test', **kwargs):
|
||||
"""
|
||||
Build argv for test.
|
||||
:param conf_file_name: Configuration file name
|
||||
:param verbose: verbosity level
|
||||
:return: argv
|
||||
"""
|
||||
argv = [command]
|
||||
if kwargs.get('verbose', None):
|
||||
argv.append(kwargs.get('verbose', None))
|
||||
if kwargs.get('silent', None):
|
||||
argv.append(kwargs.get('silent', None))
|
||||
argv.extend(['--url', 'http://127.0.0.1', '-y'])
|
||||
if kwargs.get('priv_key', None):
|
||||
argv.extend(('-i', kwargs.get('priv_key', None)))
|
||||
if command == 'test':
|
||||
argv.extend(
|
||||
('-c', kwargs.get('conf_file_name', self.conf_file_name)))
|
||||
if kwargs.get('test_cases', None):
|
||||
argv.extend(('--', kwargs.get('test_cases', None)))
|
||||
return argv
|
||||
|
||||
def mock_data(self):
|
||||
"""
|
||||
Mock the Keystone client methods.
|
||||
"""
|
||||
self.mock_identity_service_v2 = {'type': 'identity',
|
||||
'endpoints': [{'id': 'test-id'}]}
|
||||
self.mock_identity_service_v3 = {'type': 'identity',
|
||||
'id': 'test-id'}
|
||||
self.v2_config = {'auth_url': 'http://0.0.0.0:35357/v2.0/tokens',
|
||||
'auth_version': 'v2',
|
||||
'domain_name': 'Default',
|
||||
'password': 'test',
|
||||
'tenant_id': 'admin_project_id',
|
||||
'project_id': 'admin_project_id',
|
||||
'tenant_name': 'project_name',
|
||||
'project_name': 'project_name',
|
||||
'username': 'admin'}
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Test case setup
|
||||
"""
|
||||
logging.disable(logging.CRITICAL)
|
||||
|
||||
def test_verbose(self):
|
||||
"""
|
||||
Test different verbosity levels.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
self.assertEqual(client.logger.level, logging.INFO)
|
||||
|
||||
args = rc.parse_cli_args(self.mock_argv(verbose='-v'))
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
self.assertEqual(client.logger.level, logging.DEBUG)
|
||||
|
||||
args = rc.parse_cli_args(self.mock_argv(verbose='-vv'))
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
self.assertEqual(client.logger.level, logging.DEBUG)
|
||||
|
||||
args = rc.parse_cli_args(self.mock_argv(silent='-s'))
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
self.assertEqual(client.logger.level, logging.WARNING)
|
||||
|
||||
def test_get_next_stream_subunit_output_file(self):
|
||||
"""
|
||||
Test getting the subunit file from an existing .testrepository
|
||||
directory that has a next-stream file.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
output_file = client._get_next_stream_subunit_output_file(
|
||||
self.test_path)
|
||||
|
||||
# The next-stream file contains a "1".
|
||||
expected_file = expected_file = self.test_path + "/.testrepository/1"
|
||||
self.assertEqual(expected_file, output_file)
|
||||
|
||||
def test_get_next_stream_subunit_output_file_nonexistent(self):
|
||||
"""
|
||||
Test getting the subunit output file from a nonexistent
|
||||
.testrepository directory.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
output_file = client._get_next_stream_subunit_output_file(
|
||||
"/tempest/path")
|
||||
expected_file = "/tempest/path/.testrepository/0"
|
||||
self.assertEqual(expected_file, output_file)
|
||||
|
||||
def test_get_cpid_account_file_not_found(self):
|
||||
"""
|
||||
Test that the client will exit if an accounts file is specified,
|
||||
but does not exist.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
|
||||
client.conf.add_section('auth')
|
||||
client.conf.set('auth',
|
||||
'test_accounts_file',
|
||||
'%s/some-file.yaml' % self.test_path)
|
||||
|
||||
self.mock_data()
|
||||
with self.assertRaises(SystemExit):
|
||||
client._get_keystone_config(client.conf)
|
||||
|
||||
def test_get_keystone_config_account_file_empty(self):
|
||||
"""
|
||||
Test that the client will exit if an accounts file exists,
|
||||
but is empty.
|
||||
"""
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.read_accounts_yaml',
|
||||
return_value=None)
|
||||
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
|
||||
client.conf.add_section('auth')
|
||||
client.conf.set('auth',
|
||||
'test_accounts_file',
|
||||
'%s/some-file.yaml' % self.test_path)
|
||||
|
||||
self.mock_data()
|
||||
with self.assertRaises(SystemExit):
|
||||
client._get_keystone_config(client.conf)
|
||||
|
||||
def test_get_keystone_config_no_accounts_file(self):
|
||||
"""
|
||||
Test that the client will exit if accounts file
|
||||
is not specified.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
|
||||
self.mock_data()
|
||||
with self.assertRaises(SystemExit):
|
||||
client._get_keystone_config(client.conf)
|
||||
|
||||
def test_get_keystone_config(self):
|
||||
"""
|
||||
Test that keystone configs properly parsed.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
client.conf.add_section('auth')
|
||||
client.conf.set('auth',
|
||||
'test_accounts_file',
|
||||
'%s/test-accounts.yaml' % self.test_path)
|
||||
self.mock_data()
|
||||
accounts = [
|
||||
{
|
||||
'username': 'admin',
|
||||
'project_name': 'project_name',
|
||||
'project_id': 'admin_project_id',
|
||||
'password': 'test'
|
||||
}
|
||||
]
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.read_accounts_yaml',
|
||||
return_value=accounts)
|
||||
actual_result = client._get_keystone_config(client.conf)
|
||||
expected_result = self.v2_config
|
||||
self.assertEqual(expected_result, actual_result)
|
||||
|
||||
def test_get_cpid_from_keystone_by_tenant_name_from_account_file(self):
|
||||
"""
|
||||
Test getting a CPID from Keystone using an admin tenant name
|
||||
from an accounts file.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
client.conf.add_section('auth')
|
||||
client.conf.set('auth',
|
||||
'test_accounts_file',
|
||||
'%s/test-accounts.yaml' % self.test_path)
|
||||
self.mock_data()
|
||||
actual_result = client._get_keystone_config(client.conf)
|
||||
expected_result = None
|
||||
self.assertEqual(expected_result, actual_result['tenant_id'])
|
||||
accounts = [
|
||||
{
|
||||
'username': 'admin',
|
||||
'tenant_id': 'tenant_id',
|
||||
'password': 'test'
|
||||
}
|
||||
]
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.read_accounts_yaml',
|
||||
return_value=accounts)
|
||||
actual_result = client._get_keystone_config(client.conf)
|
||||
self.assertEqual('tenant_id', actual_result['tenant_id'])
|
||||
|
||||
def test_generate_keystone_data(self):
|
||||
"""Test that correct data is generated."""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
client.conf.add_section('auth')
|
||||
client.conf.set('auth',
|
||||
'test_accounts_file',
|
||||
'%s/test-accounts.yaml' % self.test_path)
|
||||
self.mock_data()
|
||||
accounts = [
|
||||
{
|
||||
'username': 'admin',
|
||||
'tenant_id': 'admin_tenant_id',
|
||||
'password': 'test'
|
||||
}
|
||||
]
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.read_accounts_yaml',
|
||||
return_value=accounts)
|
||||
configs = client._get_keystone_config(client.conf)
|
||||
actual_results = client._generate_keystone_data(configs)
|
||||
expected_results = ('v2', 'http://0.0.0.0:35357/v2.0/tokens',
|
||||
{'auth':
|
||||
{'passwordCredentials':
|
||||
{
|
||||
'username': 'admin', 'password': 'test'
|
||||
},
|
||||
'tenantId': 'admin_tenant_id'}})
|
||||
self.assertEqual(expected_results, actual_results)
|
||||
|
||||
def test_get_cpid_from_keystone_v3_varying_catalogs(self):
|
||||
"""
|
||||
Test getting the CPID from keystone API v3 with varying catalogs.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
client.conf.set('identity-feature-enabled', 'api_v3', 'true')
|
||||
client.conf.add_section('auth')
|
||||
client.conf.set('auth',
|
||||
'test_accounts_file',
|
||||
'%s/test-accounts.yaml' % self.test_path)
|
||||
self.mock_data()
|
||||
accounts = [
|
||||
{
|
||||
'tenant_name': 'tenant_name'
|
||||
}
|
||||
]
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.read_accounts_yaml',
|
||||
return_value=accounts)
|
||||
configs = client._get_keystone_config(client.conf)
|
||||
auth_version, auth_url, content = \
|
||||
client._generate_keystone_data(configs)
|
||||
client._generate_cpid_from_endpoint = MagicMock()
|
||||
|
||||
# Test when the identity ID is None.
|
||||
ks3_ID_None = {'token': {'catalog':
|
||||
[{'type': 'identity', 'id': None}]}}
|
||||
|
||||
@httmock.all_requests
|
||||
def keystone_api_v3_mock(url, request):
|
||||
return httmock.response(201, ks3_ID_None)
|
||||
with httmock.HTTMock(keystone_api_v3_mock):
|
||||
client._get_cpid_from_keystone(auth_version, auth_url, content)
|
||||
client._generate_cpid_from_endpoint.assert_called_with(auth_url)
|
||||
|
||||
# Test when the catalog is empty.
|
||||
ks3_catalog_empty = {'token': {'catalog': []}}
|
||||
client._generate_cpid_from_endpoint = MagicMock()
|
||||
|
||||
@httmock.all_requests
|
||||
def keystone_api_v3_mock2(url, request):
|
||||
return httmock.response(201, ks3_catalog_empty)
|
||||
with httmock.HTTMock(keystone_api_v3_mock2):
|
||||
client._get_cpid_from_keystone(auth_version, auth_url, content)
|
||||
client._generate_cpid_from_endpoint.assert_called_with(auth_url)
|
||||
|
||||
# Test when there is no service catalog.
|
||||
ks3_no_catalog = {'token': {}}
|
||||
client._generate_cpid_from_endpoint = MagicMock()
|
||||
|
||||
@httmock.all_requests
|
||||
def keystone_api_v3_mock3(url, request):
|
||||
return httmock.response(201, ks3_no_catalog)
|
||||
with httmock.HTTMock(keystone_api_v3_mock3):
|
||||
client._get_cpid_from_keystone(auth_version, auth_url, content)
|
||||
client._generate_cpid_from_endpoint.assert_called_with(auth_url)
|
||||
|
||||
# Test when catalog has other non-identity services.
|
||||
ks3_other_services = {'token': {
|
||||
'catalog': [{'type': 'compute',
|
||||
'id': 'test-id1'},
|
||||
{'type': 'identity',
|
||||
'id': 'test-id2'}]
|
||||
}}
|
||||
client._generate_cpid_from_endpoint = MagicMock()
|
||||
|
||||
@httmock.all_requests
|
||||
def keystone_api_v3_mock4(url, request):
|
||||
return httmock.response(201, ks3_other_services)
|
||||
with httmock.HTTMock(keystone_api_v3_mock4):
|
||||
cpid = client._get_cpid_from_keystone(auth_version,
|
||||
auth_url,
|
||||
content)
|
||||
self.assertFalse(client._generate_cpid_from_endpoint.called)
|
||||
self.assertEqual('test-id2', cpid)
|
||||
|
||||
def test_get_cpid_from_keystone_failure_handled(self):
|
||||
"""Test that get cpid from keystone API failure handled."""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
client.logger.warning = MagicMock()
|
||||
client._generate_cpid_from_endpoint = MagicMock()
|
||||
client.conf.add_section('auth')
|
||||
client.conf.set('auth',
|
||||
'test_accounts_file',
|
||||
'%s/test-accounts.yaml' % self.test_path)
|
||||
self.mock_data()
|
||||
accounts = [
|
||||
{
|
||||
'tenant_name': 'tenant_name',
|
||||
'tenant_id': 'admin_tenant_id',
|
||||
'password': 'test'
|
||||
}
|
||||
]
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.read_accounts_yaml',
|
||||
return_value=accounts)
|
||||
configs = client._get_keystone_config(client.conf)
|
||||
auth_version, url, content = client._generate_keystone_data(configs)
|
||||
|
||||
@httmock.urlmatch(netloc=r'(.*\.)?127.0.0.1$', path='/v2/tokens')
|
||||
def keystone_api_mock(auth_version, url, request):
|
||||
return None
|
||||
with httmock.HTTMock(keystone_api_mock):
|
||||
client._get_cpid_from_keystone(auth_version, url, content)
|
||||
client._generate_cpid_from_endpoint.assert_called_with(url)
|
||||
|
||||
def test_generate_cpid_from_endpoint(self):
|
||||
"""
|
||||
Test that an endpoint's hostname is properly hashed.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
cpid = client._generate_cpid_from_endpoint('http://some.url:5000/v2')
|
||||
expected = hashlib.md5('some.url'.encode('utf-8')).hexdigest()
|
||||
self.assertEqual(expected, cpid)
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
client._generate_cpid_from_endpoint('some.url:5000/v2')
|
||||
|
||||
def test_form_result_content(self):
|
||||
"""
|
||||
Test that the request content is formed into the expected format.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
content = client._form_result_content(1, 1, ['tempest.sample.test'])
|
||||
expected = {'cpid': 1,
|
||||
'duration_seconds': 1,
|
||||
'results': ['tempest.sample.test']}
|
||||
self.assertEqual(expected, content)
|
||||
|
||||
def test_save_json_result(self):
|
||||
"""
|
||||
Test that the results are properly written to a JSON file.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
results = {'cpid': 1,
|
||||
'duration_seconds': 1,
|
||||
'results': ['tempest.sample.test']}
|
||||
temp_file = tempfile.NamedTemporaryFile()
|
||||
client._save_json_results(results, temp_file.name)
|
||||
|
||||
# Get the JSON that was written to the file and make sure it
|
||||
# matches the expected value.
|
||||
json_file = open(temp_file.name)
|
||||
json_data = json.load(json_file)
|
||||
json_file.close()
|
||||
self.assertEqual(results, json_data)
|
||||
|
||||
def test_get_passed_tests(self):
|
||||
"""
|
||||
Test that only passing tests are retrieved from a subunit file.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
subunit_file = self.test_path + "/.testrepository/0"
|
||||
results = client.get_passed_tests(subunit_file)
|
||||
expected = [
|
||||
{'name': 'tempest.passed.test'},
|
||||
{'name': 'tempest.tagged_passed.test',
|
||||
'uuid': '0146f675-ffbd-4208-b3a4-60eb628dbc5e'}
|
||||
]
|
||||
self.assertEqual(expected, results)
|
||||
|
||||
@mock.patch('six.moves.input')
|
||||
def test_user_query(self, mock_input):
|
||||
client = rc.RefstackClient(rc.parse_cli_args(self.mock_argv()))
|
||||
self.assertTrue(client._user_query('42?'))
|
||||
|
||||
mock_input.return_value = 'n'
|
||||
cli_args = self.mock_argv()
|
||||
cli_args.remove('-y')
|
||||
client = rc.RefstackClient(rc.parse_cli_args(cli_args))
|
||||
self.assertFalse(client._user_query('42?'))
|
||||
mock_input.return_value = 'yes'
|
||||
self.assertTrue(client._user_query('42?'))
|
||||
|
||||
def test_upload_prompt(self):
|
||||
"""
|
||||
Test the _upload_prompt method.
|
||||
"""
|
||||
client = rc.RefstackClient(rc.parse_cli_args(self.mock_argv()))
|
||||
|
||||
# When user says yes.
|
||||
client._user_query = MagicMock(return_value=True)
|
||||
client.post_results = MagicMock()
|
||||
client._upload_prompt({'some': 'data'})
|
||||
client.post_results.assert_called_with(
|
||||
'http://127.0.0.1', {'some': 'data'}, sign_with=None
|
||||
)
|
||||
|
||||
# When user says no.
|
||||
client._user_query = MagicMock(return_value=False)
|
||||
client.post_results = MagicMock()
|
||||
client._upload_prompt({'some': 'data'})
|
||||
self.assertFalse(client.post_results.called)
|
||||
|
||||
def test_post_results(self):
|
||||
"""
|
||||
Test the post_results method, ensuring a requests call is made.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.logger.info = MagicMock()
|
||||
content = {'duration_seconds': 0,
|
||||
'cpid': 'test-id',
|
||||
'results': [{'name': 'tempest.passed.test', 'uid': None}]}
|
||||
expected_response = json.dumps({'test_id': 42})
|
||||
|
||||
@httmock.urlmatch(netloc=r'(.*\.)?127.0.0.1$', path='/v1/results/')
|
||||
def refstack_api_mock(url, request):
|
||||
return expected_response
|
||||
|
||||
with httmock.HTTMock(refstack_api_mock):
|
||||
client.post_results("http://127.0.0.1", content)
|
||||
client.logger.info.assert_called_with(
|
||||
'http://127.0.0.1/v1/results/ Response: '
|
||||
'%s' % expected_response)
|
||||
|
||||
def test_post_results_with_sign(self):
|
||||
"""
|
||||
Test the post_results method, ensuring a requests call is made.
|
||||
"""
|
||||
argv = self.mock_argv(command='upload', priv_key='rsa_key')
|
||||
argv.append('fake.json')
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
client.logger.info = MagicMock()
|
||||
content = {'duration_seconds': 0,
|
||||
'cpid': 'test-id',
|
||||
'results': [{'name': 'tempest.passed.test'}]}
|
||||
expected_response = json.dumps({'test_id': 42})
|
||||
|
||||
@httmock.urlmatch(netloc=r'(.*\.)?127.0.0.1$', path='/v1/results/')
|
||||
def refstack_api_mock(url, request):
|
||||
return expected_response
|
||||
|
||||
with httmock.HTTMock(refstack_api_mock):
|
||||
rsapath = os.path.join(self.test_path, 'rsa_key')
|
||||
client.post_results("http://127.0.0.1", content, sign_with=rsapath)
|
||||
client.logger.info.assert_called_with(
|
||||
'http://127.0.0.1/v1/results/ Response: %s' %
|
||||
expected_response)
|
||||
|
||||
def test_run_tempest(self):
|
||||
"""
|
||||
Test that the test command will run the tempest script using the
|
||||
default configuration.
|
||||
"""
|
||||
args = rc.parse_cli_args(
|
||||
self.mock_argv(verbose='-vv', test_cases='tempest.api.compute'))
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
mock_popen = self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0))
|
||||
self.patch("os.path.isfile", return_value=True)
|
||||
self.mock_data()
|
||||
client.get_passed_tests = MagicMock(return_value=[{'name': 'test'}])
|
||||
client.logger.info = MagicMock()
|
||||
client._save_json_results = MagicMock()
|
||||
client.post_results = MagicMock()
|
||||
client._get_keystone_config = MagicMock(
|
||||
return_value=self.v2_config)
|
||||
client.test()
|
||||
|
||||
mock_popen.assert_called_with(
|
||||
['%s/tools/with_venv.sh' % self.test_path, 'tempest', 'run',
|
||||
'--serial', '--regex', 'tempest.api.compute'],
|
||||
stderr=None
|
||||
)
|
||||
|
||||
self.assertFalse(client.post_results.called)
|
||||
|
||||
def test_run_tempest_upload(self):
|
||||
"""
|
||||
Test that the test command will run the tempest script and call
|
||||
post_results when the --upload argument is passed in.
|
||||
"""
|
||||
argv = self.mock_argv(verbose='-vv',
|
||||
test_cases='tempest.api.compute')
|
||||
argv.insert(2, '--upload')
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
mock_popen = self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0))
|
||||
self.patch("os.path.isfile", return_value=True)
|
||||
self.mock_data()
|
||||
client.get_passed_tests = MagicMock(return_value=['test'])
|
||||
client.post_results = MagicMock()
|
||||
client._save_json_results = MagicMock()
|
||||
client._get_keystone_config = MagicMock(
|
||||
return_value=self.v2_config)
|
||||
client._get_cpid_from_keystone = MagicMock()
|
||||
client.test()
|
||||
mock_popen.assert_called_with(
|
||||
['%s/tools/with_venv.sh' % self.test_path, 'tempest', 'run',
|
||||
'--serial', '--regex', 'tempest.api.compute'],
|
||||
stderr=None
|
||||
)
|
||||
|
||||
self.assertTrue(client.post_results.called)
|
||||
|
||||
def test_run_tempest_upload_with_sign(self):
|
||||
"""
|
||||
Test that the test command will run the tempest script and call
|
||||
post_results when the --upload argument is passed in.
|
||||
"""
|
||||
argv = self.mock_argv(verbose='-vv', priv_key='rsa_key',
|
||||
test_cases='tempest.api.compute')
|
||||
argv.insert(2, '--upload')
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
mock_popen = self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0)
|
||||
)
|
||||
self.patch("os.path.isfile", return_value=True)
|
||||
self.mock_data()
|
||||
client.get_passed_tests = MagicMock(return_value=['test'])
|
||||
client.post_results = MagicMock()
|
||||
client._save_json_results = MagicMock()
|
||||
client._get_keystone_config = MagicMock(
|
||||
return_value=self.v2_config)
|
||||
client._get_cpid_from_keystone = MagicMock(
|
||||
return_value='test-id')
|
||||
client.test()
|
||||
mock_popen.assert_called_with(
|
||||
['%s/tools/with_venv.sh' % self.test_path, 'tempest', 'run',
|
||||
'--serial', '--regex', 'tempest.api.compute'],
|
||||
stderr=None
|
||||
)
|
||||
|
||||
self.assertTrue(client.post_results.called)
|
||||
client.post_results.assert_called_with(
|
||||
'http://127.0.0.1',
|
||||
{'duration_seconds': 0,
|
||||
'cpid': 'test-id',
|
||||
'results': ['test']},
|
||||
sign_with='rsa_key'
|
||||
)
|
||||
|
||||
@mock.patch('refstack_client.list_parser.TestListParser.'
|
||||
'create_include_list')
|
||||
@mock.patch('refstack_client.list_parser.'
|
||||
'TestListParser.get_normalized_test_list')
|
||||
def test_run_tempest_with_test_list(self, mock_normalize,
|
||||
mock_include_list):
|
||||
"""Test that the Tempest script runs with a test list file."""
|
||||
argv = self.mock_argv(verbose='-vv')
|
||||
argv.extend(['--test-list', 'test-list.txt'])
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
mock_popen = self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0))
|
||||
self.patch("os.path.isfile", return_value=True)
|
||||
self.patch("os.path.getsize", return_value=4096)
|
||||
self.mock_data()
|
||||
client.get_passed_tests = MagicMock(return_value=[{'name': 'test'}])
|
||||
client._save_json_results = MagicMock()
|
||||
client.post_results = MagicMock()
|
||||
mock_normalize.return_value = '/tmp/some-list'
|
||||
mock_include_list.return_value = '/tmp/some-list'
|
||||
client._get_keystone_config = MagicMock(
|
||||
return_value=self.v2_config)
|
||||
client.test()
|
||||
|
||||
mock_include_list.assert_called_with('test-list.txt')
|
||||
# TODO(kopecmartin) rename the below argument when refstack-client
|
||||
# uses tempest which contains the following change in its code:
|
||||
# https://review.opendev.org/c/openstack/tempest/+/768583
|
||||
mock_popen.assert_called_with(
|
||||
['%s/tools/with_venv.sh' % self.test_path, 'tempest', 'run',
|
||||
'--serial', '--whitelist_file', '/tmp/some-list'],
|
||||
stderr=None
|
||||
)
|
||||
|
||||
def test_run_tempest_no_conf_file(self):
|
||||
"""
|
||||
Test when a nonexistent configuration file is passed in.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv(conf_file_name='ptn-khl'))
|
||||
client = rc.RefstackClient(args)
|
||||
self.assertRaises(SystemExit, client.test)
|
||||
|
||||
def test_forbidden_conf_file(self):
|
||||
"""
|
||||
Test when the user passes in a file that the user does not have
|
||||
read access to.
|
||||
"""
|
||||
file = tempfile.NamedTemporaryFile()
|
||||
# Remove read access
|
||||
os.chmod(file.name, 0o220)
|
||||
args = rc.parse_cli_args(self.mock_argv(conf_file_name=file.name))
|
||||
client = rc.RefstackClient(args)
|
||||
self.assertRaises(SystemExit, client.test)
|
||||
|
||||
def test_run_tempest_nonexisting_directory(self):
|
||||
"""
|
||||
Test when the Tempest directory does not exist.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = "/does/not/exist"
|
||||
self.assertRaises(SystemExit, client.test)
|
||||
|
||||
def test_run_tempest_result_tag(self):
|
||||
"""
|
||||
Check that the result JSON file is renamed with the result file tag
|
||||
when the --result-file-tag argument is passed in.
|
||||
"""
|
||||
argv = self.mock_argv(verbose='-vv',
|
||||
test_cases='tempest.api.compute')
|
||||
argv.insert(2, '--result-file-tag')
|
||||
argv.insert(3, 'my-test')
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
mock_popen = self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0))
|
||||
self.patch("os.path.isfile", return_value=True)
|
||||
self.mock_data()
|
||||
client.get_passed_tests = MagicMock(return_value=['test'])
|
||||
client._save_json_results = MagicMock()
|
||||
client._get_keystone_config = MagicMock(
|
||||
return_value=self.v2_config)
|
||||
client._get_cpid_from_keystone = MagicMock(
|
||||
return_value='test-id')
|
||||
client.test()
|
||||
|
||||
mock_popen.assert_called_with(
|
||||
['%s/tools/with_venv.sh' % self.test_path, 'tempest', 'run',
|
||||
'--serial', '--regex', 'tempest.api.compute'],
|
||||
stderr=None
|
||||
)
|
||||
# Since '1' is in the next-stream file, we expect the JSON output file
|
||||
# to be 'my-test-1.json'.
|
||||
expected_file = os.path.join(self.test_path, '.testrepository',
|
||||
'my-test-1.json')
|
||||
client._save_json_results.assert_called_with(mock.ANY, expected_file)
|
||||
|
||||
def test_failed_run(self):
|
||||
"""
|
||||
Test when the Tempest script returns a non-zero exit code.
|
||||
"""
|
||||
self.patch('refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=1))
|
||||
args = rc.parse_cli_args(self.mock_argv(verbose='-vv'))
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
self.mock_data()
|
||||
client.logger.warning = MagicMock()
|
||||
client._get_keystone_config = MagicMock(
|
||||
return_value=self.v2_config)
|
||||
client._get_cpid_from_keystone = MagicMock()
|
||||
client.test()
|
||||
self.assertTrue(client.logger.warning.called)
|
||||
|
||||
def test_upload(self):
|
||||
"""
|
||||
Test that the upload command runs as expected.
|
||||
"""
|
||||
upload_file_path = self.test_path + "/.testrepository/0.json"
|
||||
args = rc.parse_cli_args(
|
||||
self.mock_argv(command='upload', priv_key='rsa_key') +
|
||||
[upload_file_path])
|
||||
client = rc.RefstackClient(args)
|
||||
|
||||
client.post_results = MagicMock()
|
||||
client.upload()
|
||||
expected_json = {
|
||||
'duration_seconds': 0,
|
||||
'cpid': 'test-id',
|
||||
'results': [
|
||||
{'name': 'tempest.passed.test'},
|
||||
{'name': 'tempest.tagged_passed.test',
|
||||
'uuid': '0146f675-ffbd-4208-b3a4-60eb628dbc5e'}
|
||||
]
|
||||
}
|
||||
client.post_results.assert_called_with('http://127.0.0.1',
|
||||
expected_json,
|
||||
sign_with='rsa_key')
|
||||
|
||||
def test_subunit_upload(self):
|
||||
"""
|
||||
Test that the subunit upload command runs as expected.
|
||||
"""
|
||||
upload_file_path = self.test_path + "/.testrepository/0"
|
||||
args = rc.parse_cli_args(
|
||||
self.mock_argv(command='upload-subunit', priv_key='rsa_key') +
|
||||
['--keystone-endpoint', 'http://0.0.0.0:5000/v2.0'] +
|
||||
[upload_file_path])
|
||||
client = rc.RefstackClient(args)
|
||||
client.post_results = MagicMock()
|
||||
client.upload_subunit()
|
||||
expected_json = {
|
||||
'duration_seconds': 0,
|
||||
'cpid': hashlib.md5('0.0.0.0'.encode('utf-8')).hexdigest(),
|
||||
'results': [
|
||||
{'name': 'tempest.passed.test'},
|
||||
{'name': 'tempest.tagged_passed.test',
|
||||
'uuid': '0146f675-ffbd-4208-b3a4-60eb628dbc5e'}
|
||||
]
|
||||
}
|
||||
client.post_results.assert_called_with('http://127.0.0.1',
|
||||
expected_json,
|
||||
sign_with='rsa_key')
|
||||
|
||||
def test_upload_nonexisting_file(self):
|
||||
"""
|
||||
Test when the file to be uploaded does not exist.
|
||||
"""
|
||||
upload_file_path = self.test_path + "/.testrepository/foo.json"
|
||||
args = rc.parse_cli_args(['upload', upload_file_path,
|
||||
'--url', 'http://api.test.org'])
|
||||
client = rc.RefstackClient(args)
|
||||
self.assertRaises(SystemExit, client.upload)
|
||||
|
||||
def test_yield_results(self):
|
||||
"""
|
||||
Test the yield_results method, ensuring that results are retrieved.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv(command='list'))
|
||||
client = rc.RefstackClient(args)
|
||||
expected_response = {
|
||||
"pagination": {
|
||||
"current_page": 1,
|
||||
"total_pages": 1
|
||||
},
|
||||
"results": [
|
||||
{
|
||||
"cpid": "42",
|
||||
"created_at": "2015-04-28 13:57:05",
|
||||
"test_id": "1",
|
||||
"url": "http://127.0.0.1:8000/output.html?test_id=1"
|
||||
},
|
||||
{
|
||||
"cpid": "42",
|
||||
"created_at": "2015-04-28 13:57:05",
|
||||
"test_id": "2",
|
||||
"url": "http://127.0.0.1:8000/output.html?test_id=2"
|
||||
}]}
|
||||
|
||||
@httmock.urlmatch(netloc=r'(.*\.)?127.0.0.1$', path='/v1/results/')
|
||||
def refstack_api_mock(url, request):
|
||||
return json.dumps(expected_response)
|
||||
|
||||
with httmock.HTTMock(refstack_api_mock):
|
||||
results = client.yield_results("http://127.0.0.1")
|
||||
self.assertEqual(expected_response['results'], next(results))
|
||||
# Since Python3.7 StopIteration exceptions are transformed into
|
||||
# RuntimeError (PEP 479):
|
||||
# https://docs.python.org/3/whatsnew/3.7.html
|
||||
self.assertRaises((StopIteration, RuntimeError), next, results)
|
||||
|
||||
@mock.patch('six.moves.input', side_effect=KeyboardInterrupt)
|
||||
@mock.patch('sys.stdout', new_callable=MagicMock)
|
||||
def test_list(self, mock_stdout, mock_input):
|
||||
args = rc.parse_cli_args(self.mock_argv(command='list'))
|
||||
client = rc.RefstackClient(args)
|
||||
results = [[{"cpid": "42",
|
||||
"created_at": "2015-04-28 13:57:05",
|
||||
"test_id": "1",
|
||||
"url": "http://127.0.0.1:8000/output.html?test_id=1"},
|
||||
{"cpid": "42",
|
||||
"created_at": "2015-04-28 13:57:05",
|
||||
"test_id": "2",
|
||||
"url": "http://127.0.0.1:8000/output.html?test_id=2"}]]
|
||||
mock_results = MagicMock()
|
||||
mock_results.__iter__.return_value = results
|
||||
client.yield_results = MagicMock(return_value=mock_results)
|
||||
client.list()
|
||||
self.assertTrue(mock_stdout.write.called)
|
||||
|
||||
def test_sign_pubkey(self):
|
||||
"""
|
||||
Test that the test command will run the tempest script and call
|
||||
post_results when the --upload argument is passed in.
|
||||
"""
|
||||
args = rc.parse_cli_args(['sign',
|
||||
os.path.join(self.test_path, 'rsa_key')])
|
||||
client = rc.RefstackClient(args)
|
||||
pubkey, signature = client._sign_pubkey()
|
||||
self.assertTrue(pubkey.decode('utf8').startswith('ssh-rsa AAAA'))
|
||||
self.assertTrue(signature.decode('utf8').startswith('413cb954'))
|
||||
|
||||
def test_set_env_params(self):
|
||||
"""
|
||||
Test that the environment variables are correctly set.
|
||||
"""
|
||||
args = rc.parse_cli_args(self.mock_argv())
|
||||
client = rc.RefstackClient(args)
|
||||
client.tempest_dir = self.test_path
|
||||
client._prep_test()
|
||||
conf_dir = os.path.abspath(os.path.dirname(self.conf_file_name))
|
||||
conf_file = os.path.basename(self.conf_file_name)
|
||||
self.assertEqual(os.environ.get('TEMPEST_CONFIG_DIR'), conf_dir)
|
||||
self.assertEqual(os.environ.get('TEMPEST_CONFIG'), conf_file)
|
||||
|
||||
@mock.patch('refstack_client.list_parser.TestListParser.'
|
||||
'create_include_list')
|
||||
def test_run_tempest_with_empty_test_list(self, mock_include_list):
|
||||
"""Test that refstack-client can handle an empty test list file."""
|
||||
argv = self.mock_argv(verbose='-vv')
|
||||
argv.extend(['--test-list', 'foo.txt'])
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
self.mock_data()
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0))
|
||||
client._get_keystone_config = MagicMock(return_value=self.v2_config)
|
||||
client.tempest_dir = self.test_path
|
||||
self.patch("os.path.isfile", return_value=True)
|
||||
empty_file = tempfile.NamedTemporaryFile()
|
||||
mock_include_list.return_value = empty_file.name
|
||||
self.assertRaises(SystemExit, client.test)
|
||||
|
||||
def test_run_tempest_with_non_exist_test_list_file(self):
|
||||
"""Test that refstack-client runs with a nonexistent test list file."""
|
||||
argv = self.mock_argv(verbose='-vv')
|
||||
argv.extend(['--test-list', 'foo.txt'])
|
||||
args = rc.parse_cli_args(argv)
|
||||
client = rc.RefstackClient(args)
|
||||
self.mock_data()
|
||||
self.patch(
|
||||
'refstack_client.list_parser.TestListParser._get_tempest_test_ids',
|
||||
return_value={'foo': ''})
|
||||
self.patch(
|
||||
'refstack_client.refstack_client.subprocess.Popen',
|
||||
return_value=MagicMock(returncode=0))
|
||||
client._get_keystone_config = MagicMock(return_value=self.v2_config)
|
||||
client.tempest_dir = self.test_path
|
||||
self.assertRaises(IOError, client.test)
|
||||
@@ -1,192 +0,0 @@
|
||||
# Copyright 2015 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
import httmock
|
||||
from unittest import mock
|
||||
import unittest
|
||||
|
||||
import refstack_client.list_parser as parser
|
||||
|
||||
|
||||
class TestTestListParser(unittest.TestCase):
|
||||
|
||||
test_path = os.path.dirname(os.path.realpath(__file__))
|
||||
tempest_dir = "some_dir/.tempest"
|
||||
|
||||
def setUp(self):
|
||||
"""Test case setup"""
|
||||
logging.disable(logging.CRITICAL)
|
||||
self.parser = parser.TestListParser(self.tempest_dir)
|
||||
|
||||
def test_get_tempest_test_ids(self):
|
||||
"""Test that the tempest test-list is correctly parsed."""
|
||||
test_list = ("tempest.test.one[gate]\n"
|
||||
"tempest.test.two[gate,smoke]\n"
|
||||
"tempest.test.three(scenario)\n"
|
||||
"tempest.test.four[gate](another_scenario)\n"
|
||||
"tempest.test.five")
|
||||
process_mock = mock.Mock(returncode=0)
|
||||
attrs = {'communicate.return_value': (test_list, None)}
|
||||
process_mock.configure_mock(**attrs)
|
||||
subprocess.Popen = mock.Mock(return_value=process_mock)
|
||||
output = self.parser._get_tempest_test_ids()
|
||||
|
||||
subprocess.Popen.assert_called_with(
|
||||
("%s/tools/with_venv.sh" % self.tempest_dir, "testr",
|
||||
"list-tests"),
|
||||
stdout=subprocess.PIPE,
|
||||
cwd=self.tempest_dir)
|
||||
expected_output = {"tempest.test.one": "[gate]",
|
||||
"tempest.test.two": "[gate,smoke]",
|
||||
"tempest.test.three(scenario)": "",
|
||||
"tempest.test.four(another_scenario)": "[gate]",
|
||||
"tempest.test.five": ""}
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def test_get_tempest_test_ids_fail(self):
|
||||
"""Test when the test listing subprocess returns a non-zero exit
|
||||
status.
|
||||
"""
|
||||
process_mock = mock.Mock(returncode=1)
|
||||
attrs = {'communicate.return_value': (mock.ANY, None)}
|
||||
process_mock.configure_mock(**attrs)
|
||||
subprocess.Popen = mock.Mock(return_value=process_mock)
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
self.parser._get_tempest_test_ids()
|
||||
|
||||
def test_form_test_id_mappings(self):
|
||||
"""Test the test ID to attribute dict builder function."""
|
||||
test_list = ["tempest.test.one[gate]",
|
||||
"tempest.test.two[gate,smoke]",
|
||||
"tempest.test.three(scenario)",
|
||||
"tempest.test.four[gate](another_scenario)",
|
||||
"tempest.test.five"]
|
||||
|
||||
expected_output = {"tempest.test.one": "[gate]",
|
||||
"tempest.test.two": "[gate,smoke]",
|
||||
"tempest.test.three(scenario)": "",
|
||||
"tempest.test.four(another_scenario)": "[gate]",
|
||||
"tempest.test.five": ""}
|
||||
output = self.parser._form_test_id_mappings(test_list)
|
||||
self.assertEqual(expected_output, output)
|
||||
|
||||
def test_get_base_test_ids_from_list_file(self):
|
||||
"""test that we can get the base test IDs from a test list file."""
|
||||
list_file = self.test_path + "/test-list.txt"
|
||||
test_list = self.parser._get_base_test_ids_from_list_file(list_file)
|
||||
expected_list = ['tempest.api.test1',
|
||||
'tempest.api.test2',
|
||||
'tempest.api.test3(scenario)']
|
||||
self.assertEqual(expected_list, sorted(test_list))
|
||||
|
||||
def test_get_base_test_ids_from_list_files_invalid_file(self):
|
||||
"""Test that we get an exception when passing in a nonexistent file."""
|
||||
some_file = self.test_path + "/nonexistent.json"
|
||||
with self.assertRaises(Exception):
|
||||
self.parser._get_base_test_ids_from_list_file(some_file)
|
||||
|
||||
def test_get_base_test_ids_from_list_file_url(self):
|
||||
"""Test that we can parse the test cases from a test list URL."""
|
||||
list_file = self.test_path + "/test-list.txt"
|
||||
|
||||
with open(list_file, 'rb') as f:
|
||||
content = f.read()
|
||||
|
||||
@httmock.all_requests
|
||||
def request_mock(url, request):
|
||||
return {'status_code': 200,
|
||||
'content': content}
|
||||
|
||||
with httmock.HTTMock(request_mock):
|
||||
online_list = self.parser._get_base_test_ids_from_list_file(
|
||||
"http://127.0.0.1/test-list.txt")
|
||||
|
||||
expected_list = ['tempest.api.test1',
|
||||
'tempest.api.test2',
|
||||
'tempest.api.test3(scenario)']
|
||||
self.assertEqual(expected_list, sorted(online_list))
|
||||
|
||||
def test_get_base_test_ids_from_list_file_invalid_url(self):
|
||||
"""Test a case of an invalid URL schema."""
|
||||
with self.assertRaises(requests.exceptions.RequestException):
|
||||
self.parser._get_base_test_ids_from_list_file("foo://sasas.com")
|
||||
|
||||
def test_get_full_test_ids(self):
|
||||
"""Test that full test IDs can be formed."""
|
||||
tempest_ids = {"tempest.test.one": "[gate]",
|
||||
"tempest.test.two": "[gate,smoke]",
|
||||
"tempest.test.three(scenario)": "",
|
||||
"tempest.test.four(another_scenario)": "[gate]",
|
||||
"tempest.test.five": ""}
|
||||
|
||||
base_ids = ["tempest.test.one",
|
||||
"tempest.test.four(another_scenario)",
|
||||
"tempest.test.five"]
|
||||
|
||||
output_list = self.parser._get_full_test_ids(tempest_ids, base_ids)
|
||||
expected_list = ["tempest.test.one[gate]",
|
||||
"tempest.test.four[gate](another_scenario)",
|
||||
"tempest.test.five"]
|
||||
self.assertEqual(expected_list, output_list)
|
||||
|
||||
def test_get_full_test_ids_with_nonexistent_test(self):
|
||||
"""Test when a test ID doesn't exist in the Tempest environment."""
|
||||
tempest_ids = {"tempest.test.one": "[gate]",
|
||||
"tempest.test.two": "[gate,smoke]"}
|
||||
base_ids = ["tempest.test.one", "tempest.test.foo"]
|
||||
output_list = self.parser._get_full_test_ids(tempest_ids, base_ids)
|
||||
|
||||
self.assertEqual(["tempest.test.one[gate]"], output_list)
|
||||
|
||||
def test_write_normalized_test_list(self):
|
||||
"""Test that a normalized test list is written to disk."""
|
||||
test_ids = ["tempest.test.one[gate]", "tempest.test.five"]
|
||||
test_file = self.parser._write_normalized_test_list(test_ids)
|
||||
|
||||
# Check that the tempest IDs in the file match the expected test
|
||||
# ID list.
|
||||
with open(test_file, 'rb') as f:
|
||||
file_contents = f.read()
|
||||
testcase_list = list(filter(None,
|
||||
file_contents.decode('utf-8').split('\n')))
|
||||
|
||||
self.assertEqual(test_ids, testcase_list)
|
||||
|
||||
@mock.patch.object(parser.TestListParser, "get_normalized_test_list")
|
||||
def test_create_include_list(self, mock_get_normalized):
|
||||
"""Test whether a test list is properly parsed to extract test names"""
|
||||
test_list = [
|
||||
"tempest.test.one[id-11111111-2222-3333-4444-555555555555,gate]",
|
||||
"tempest.test.two[comp,id-22222222-3333-4444-5555-666666666666]",
|
||||
"tempest.test.three[id-33333333-4444-5555-6666-777777777777](gate)"
|
||||
]
|
||||
|
||||
expected_list = "tempest.test.one\[\n"\
|
||||
"tempest.test.two\[\n"\
|
||||
"tempest.test.three\[\n" # noqa W605
|
||||
|
||||
tmpfile = tempfile.mktemp()
|
||||
with open(tmpfile, 'w') as f:
|
||||
[f.write(item + "\n") for item in test_list]
|
||||
mock_get_normalized.return_value = tmpfile
|
||||
|
||||
result = open(self.parser.create_include_list(tmpfile)).read()
|
||||
self.assertEqual(result, expected_list)
|
||||
@@ -1,6 +0,0 @@
|
||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
python-subunit>=0.0.18 # BSD/Apache-2.0
|
||||
cryptography>=1.0,!=1.3.0 # BSD/Apache-2.0
|
||||
requests>=2.5.2
|
||||
PyYAML>=3.1.0
|
||||
six>=1.9.0
|
||||
37
setup.cfg
37
setup.cfg
@@ -1,37 +0,0 @@
|
||||
[metadata]
|
||||
name = refstack-client
|
||||
version = 0.1.1
|
||||
summary = Tempest test wrapper and result uploader for refstack
|
||||
description_file =
|
||||
README.rst
|
||||
author = OpenStack
|
||||
author_email = openstack-discuss@lists.openstack.org
|
||||
home_page = https://refstack.openstack.org
|
||||
classifier =
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Information Technology
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.8
|
||||
Programming Language :: Python :: 3.9
|
||||
Programming Language :: Python :: 3.10
|
||||
|
||||
[files]
|
||||
packages =
|
||||
refstack_client
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
refstack-client = refstack_client.refstack_client:entry_point
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
build-dir = doc/build
|
||||
source-dir = doc/source
|
||||
29
setup.py
29
setup.py
@@ -1,29 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2014 Piston Cloud Computing, inc. all rights reserved
|
||||
#
|
||||
# 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.
|
||||
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=2.0.0'],
|
||||
pbr=True)
|
||||
214
setup_env
214
setup_env
@@ -1,214 +0,0 @@
|
||||
#!/bin/bash -x
|
||||
|
||||
# Default Tempest commit:
|
||||
# SHA 3c7eebaaf35c9e8a3f00c76cd1741457bdec9fab (April 2023)
|
||||
CHECKOUT_POINT=3c7eebaaf35c9e8a3f00c76cd1741457bdec9fab
|
||||
PY_VERSION="3.8.10"
|
||||
UPPER_CONSTRAINTS_FILE="https://opendev.org/openstack/requirements/raw/branch/master/upper-constraints.txt"
|
||||
LOCAL_INTERPRETER=false
|
||||
|
||||
# Prints help
|
||||
function usage {
|
||||
set +x
|
||||
SCRIPT_NAME="basename ${BASH_SOURCE[0]}"
|
||||
echo "Usage: ${SCRIPT_NAME} [OPTION]..."
|
||||
echo "Setup RefStack client with test environment"
|
||||
echo ""
|
||||
echo " -h Print this usage message"
|
||||
echo " -c Tempest test runner commit. You can specify SHA or branch here"
|
||||
echo " If no commit or tag is specified, tempest will be install from commit"
|
||||
echo " -p [ 3 | 3.X.X ] - Uses python 3.8.10 (if -p 3)"
|
||||
echo " or given specific version (3.8 and higher, if -p 3.X.X). Default to python 3.8.10"
|
||||
echo " -q Run quietly. If .tempest folder exists, refstack-client is considered as installed"
|
||||
echo " -s Use python-tempestconf from the given source (path), used when running f.e. in Zuul"
|
||||
echo " -t Tempest test runner tag. You can specify tag here"
|
||||
echo " -l Force the installation of python version specified by -p (default 3.8.10) into"
|
||||
echo " the ./localpython file. If parameter -l is false and version of python specified by"
|
||||
echo " parameter -p is installed in the enviroment, script will use this interpreter."
|
||||
echo " ${CHECKOUT_POINT}"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check that parameter is a valid tag in tempest repository
|
||||
function check_tag {
|
||||
tags="$(git tag)"
|
||||
for tag in ${tags}; do
|
||||
[[ "${tag}" == "$1" ]] && return 0;
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# By default tempest uses commit ${CHECKOUT_POINT}
|
||||
|
||||
while getopts c:p:t:qs:hl FLAG; do
|
||||
case ${FLAG} in
|
||||
c)
|
||||
CHECKOUT_POINT=${OPTARG}
|
||||
;;
|
||||
p)
|
||||
if [ ${OPTARG} == '3' ]; then
|
||||
PY_VERSION=${PY_VERSION}
|
||||
elif [[ ${OPTARG} =~ ^3.([8-9]|[1-9][0-9]).([0-9]|[1-9][0-9])$ ]]; then
|
||||
# minimal version of python -> 3.8.0
|
||||
PY_VERSION=${OPTARG}
|
||||
else
|
||||
echo "Version of python-${OPTARG} no longer supported."
|
||||
usage
|
||||
fi
|
||||
;;
|
||||
t)
|
||||
CHECKOUT_POINT="-q ${OPTARG}"
|
||||
;;
|
||||
q) #show help
|
||||
QUIET_MODE=true
|
||||
;;
|
||||
s) #show help
|
||||
TEMPESTCONF_SOURCE=${OPTARG}
|
||||
;;
|
||||
h) #show help
|
||||
usage
|
||||
;;
|
||||
l) # use local python interpreter
|
||||
LOCAL_INTERPRETER=true
|
||||
;;
|
||||
\?) #unrecognized option - show help
|
||||
echo -e \\n"Option -$OPTARG not allowed."
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Install git
|
||||
WORKDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
TEMPEST_DIR=${REFSTACK_CLIENT_TEMPEST_DIR:-${WORKDIR}/.tempest}
|
||||
if [ -z ${TEMPESTCONF_SOURCE} ]; then
|
||||
TEMPESTCONF_DIR=${REFSTACK_CLIENT_TEMPEST_DIR:-${WORKDIR}/.tempestconf}
|
||||
else
|
||||
TEMPESTCONF_DIR=${TEMPESTCONF_SOURCE}
|
||||
fi
|
||||
|
||||
# Checkout tempest on specified tag
|
||||
if [ -d "${TEMPEST_DIR}" ]; then
|
||||
[ ${QUIET_MODE} ] && echo 'Looks like RefStack client is already installed' && exit 0
|
||||
while true; do
|
||||
read -p "Existing tempest installation found. We should remove it. All data from previous test runs will be deleted. Continue (y/n) ?" yn
|
||||
case ${yn} in
|
||||
[Yy]* ) rm -rf ${TEMPEST_DIR}; break;;
|
||||
[Nn]* ) exit 1;;
|
||||
* ) echo "Please answer yes or no.";;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
|
||||
if [ -n "$(command -v apt-get)" ]; then
|
||||
# For apt-get-based Linux distributions (Ubuntu, Debian)
|
||||
# If we run script in container we need sudo
|
||||
if [ ! -n "$(command -v sudo)" ]; then
|
||||
apt-get update || if [ $? -ne 0 ]; then exit 1; fi
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y install sudo
|
||||
else
|
||||
sudo apt-get update || if [ $? -ne 0 ]; then exit 1; fi
|
||||
fi
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install git
|
||||
elif [ -n "$(command -v yum)" ]; then
|
||||
# For yum-based distributions (RHEL, Centos)
|
||||
# If we run script in container we need sudo
|
||||
DNF_COMMAND=yum
|
||||
if [ ! -f sudo ]; then
|
||||
${DNF_COMMAND} -y install sudo
|
||||
fi
|
||||
sudo ${DNF_COMMAND} -y install git
|
||||
elif [ -n "$(command -v dnf)" ]; then
|
||||
# For dnf-based distributions (Centos>8, Fedora)
|
||||
# If we run script in container we need sudo
|
||||
DNF_COMMAND=dnf
|
||||
if [ ! -f sudo ]; then
|
||||
${DNF_COMMAND} -y install sudo
|
||||
fi
|
||||
sudo ${DNF_COMMAND} -y install git
|
||||
elif [ -n "$(command -v zypper)" ]; then
|
||||
# For zypper-based distributions (openSUSE, SELS)
|
||||
# If we run script in container we need sudo
|
||||
if [ ! -f sudo ]; then
|
||||
zypper --gpg-auto-import-keys --non-interactive refresh
|
||||
zypper --non-interactive install sudo
|
||||
else
|
||||
sudo zypper --gpg-auto-import-keys --non-interactive refresh
|
||||
fi
|
||||
sudo zypper --non-interactive install git
|
||||
else
|
||||
echo "Neither apt-get, nor yum, nor dnf, nor zypper found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z ${TEMPESTCONF_SOURCE} ]; then
|
||||
git clone https://opendev.org/openinfra/python-tempestconf.git ${TEMPESTCONF_DIR}
|
||||
fi
|
||||
|
||||
git clone https://opendev.org/openstack/tempest.git ${TEMPEST_DIR}
|
||||
cd ${TEMPEST_DIR}
|
||||
|
||||
git checkout $CHECKOUT_POINT || if [ $? -ne 0 ]; then exit 1; fi
|
||||
cd ${WORKDIR}
|
||||
|
||||
# Setup binary requirements
|
||||
if [ -n "$(command -v apt-get)" ]; then
|
||||
# For apt-get-based Linux distributions (Ubuntu, Debian)
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get -y install curl wget tar unzip python3-dev build-essential libssl-dev libxslt-dev libsasl2-dev libffi-dev libbz2-dev libyaml-dev
|
||||
elif [ -n "$DNF_COMMAND" -a -n "$(command -v ${DNF_COMMAND})" ]; then
|
||||
# For yum/dnf-based distributions (RHEL, Centos)
|
||||
sudo ${DNF_COMMAND} -y install curl wget tar unzip make gcc gcc-c++ libffi-devel libxml2-devel bzip2-devel libxslt-devel openssl-devel
|
||||
# python3 dependencies
|
||||
sudo ${DNF_COMMAND} -y install python3-devel
|
||||
elif [ -n "$(command -v zypper)" ]; then
|
||||
# For zypper-based distributions (openSUSE, SELS)
|
||||
sudo zypper --non-interactive install curl wget tar unzip make python-devel.x86_64 gcc gcc-c++ libffi-devel libxml2-devel zlib-devel libxslt-devel libopenssl-devel python-xml libyaml-devel
|
||||
else
|
||||
echo "Neither apt-get, nor yum, nor zypper found."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Build local python interpreter if needed
|
||||
sub_pystr="python$(echo $PY_VERSION | grep -o '3.[0-9]\+')"
|
||||
python_version=$($sub_pystr -V | cut -d " " -f 2)
|
||||
if [ $python_version == $PY_VERSION ] && [ ${LOCAL_INTERPRETER} == false ]; then
|
||||
echo "Python $PY_VERSION found!"
|
||||
PYPATH="$sub_pystr"
|
||||
else
|
||||
echo "Python $PY_VERSION not found. Building python ${PY_VERSION}..."
|
||||
mkdir ${WORKDIR}/.localpython
|
||||
mkdir ${WORKDIR}/.python_src
|
||||
cd ${WORKDIR}/.python_src
|
||||
wget http://www.python.org/ftp/python/${PY_VERSION}/Python-${PY_VERSION}.tgz
|
||||
tar zxvf Python-${PY_VERSION}.tgz
|
||||
cd Python-${PY_VERSION}
|
||||
|
||||
./configure --prefix=${WORKDIR}/.localpython --without-pymalloc
|
||||
make && make install
|
||||
cd ${WORKDIR}
|
||||
rm -rf ${WORKDIR}/.python_src
|
||||
PYPATH="${WORKDIR}/.localpython/bin/$sub_pystr"
|
||||
fi
|
||||
|
||||
mkdir ${WORKDIR}/.localvirtualenv
|
||||
VENV_VERSION='20.16.7'
|
||||
$PYPATH -m pip install --target=${WORKDIR}/.localvirtualenv virtualenv==${VENV_VERSION}
|
||||
export PYTHONPATH=$(realpath .localvirtualenv):$PYTHONPATH
|
||||
VIRTUALENV=${WORKDIR}/.localvirtualenv/bin/virtualenv
|
||||
|
||||
# Option -S disable import of modules installed in python which are causing errors when creating virtual enviroment
|
||||
$PYPATH -S $VIRTUALENV ${WORKDIR}/.venv --python="${PYPATH}"
|
||||
$PYPATH -S $VIRTUALENV ${TEMPEST_DIR}/.venv --python="${PYPATH}"
|
||||
|
||||
${WORKDIR}/.venv/bin/python -m pip install -c ${UPPER_CONSTRAINTS_FILE} -e .
|
||||
cd ${TEMPESTCONF_DIR}
|
||||
${WORKDIR}/.venv/bin/python -m pip install -c ${UPPER_CONSTRAINTS_FILE} -e .
|
||||
cd ..
|
||||
${TEMPEST_DIR}/.venv/bin/python -m pip install -c ${UPPER_CONSTRAINTS_FILE} ${TEMPEST_DIR}
|
||||
|
||||
# Add additional packages to find more tests by tempest
|
||||
# Note: Since there are no requirements in tempest-additional-requirements.txt by default,
|
||||
# this line is commented out to prevent errors from being returned. Uncomment this line if
|
||||
# there are requirements in tempest-additonal-requirements.txt.
|
||||
# ${TEMPEST_DIR}/.venv/bin/pip install -c ${UPPER_CONSTRAINTS_FILE} -r ${WORKDIR}/tempest-additional-requirements.txt
|
||||
@@ -1,54 +0,0 @@
|
||||
==============================
|
||||
Refstack-Client Specifications
|
||||
==============================
|
||||
|
||||
This folder is used to hold design specifications for additions
|
||||
to the refstack-client project. Reviews of the specs are done in gerrit, using a
|
||||
similar workflow to how we review and merge changes to the code itself.
|
||||
|
||||
The layout of this folder is as follows::
|
||||
|
||||
specs/<release>/
|
||||
specs/<release>/approved
|
||||
specs/<release>/implemented
|
||||
|
||||
The lifecycle of a specification
|
||||
--------------------------------
|
||||
|
||||
Specifications are proposed by adding an .rst file to the
|
||||
``specs/<release>/approved`` directory and posting it for review. You can
|
||||
find an example specification in ``/specs/template.rst``.
|
||||
|
||||
Once a specification has been fully implemented, meaning a patch has landed,
|
||||
it will be moved to the ``implemented`` directory and the corresponding
|
||||
blueprint will be marked as complete.
|
||||
|
||||
`Specifications are only approved for a single release`. If a specification
|
||||
was previously approved but not implemented (or not completely implemented),
|
||||
then the specification needs to be re-proposed by copying (not move) it to
|
||||
the right directory for the current release.
|
||||
|
||||
Previously approved specifications
|
||||
----------------------------------
|
||||
|
||||
The refstack-client specs directory was created during the Newton cycle.
|
||||
Therefore, the specs approved and implemented prior to the Newton cycle will
|
||||
be saved in the ``RefStack`` project.
|
||||
|
||||
Others
|
||||
------
|
||||
|
||||
Please note, Launchpad blueprints are still used for tracking the status of the
|
||||
blueprints. For more information, see::
|
||||
|
||||
https://wiki.openstack.org/wiki/Blueprints
|
||||
https://blueprints.launchpad.net/refstack
|
||||
|
||||
For more information about working with gerrit, see::
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
To validate that the specification is syntactically correct (i.e. get more
|
||||
confidence in the Jenkins result), please execute the following command::
|
||||
|
||||
$ tox -e docs
|
||||
@@ -1,264 +0,0 @@
|
||||
=============================================================
|
||||
Defcore waiver for additional properties on Nova API response
|
||||
=============================================================
|
||||
|
||||
Launchpad blueprint: https://blueprints.launchpad.net/refstack/+spec/refstack-waiver
|
||||
|
||||
Defcore waiver: https://review.openstack.org/#/c/333067/
|
||||
|
||||
Defcore committee approved above waiver which allows vendors who are using the
|
||||
Nova 2.0 API with additional properties to disable strict response checking
|
||||
when testing products for the OpenStack Powered program in 2016.
|
||||
|
||||
This spec defines the changes needed for refstack-client to optionally bypass
|
||||
Tempest strict validation.
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
Vendors need an automated way to apply the waiver. The proposed method is to
|
||||
run Tempest from the RefStack client, identify tests that fail because of
|
||||
strict response checking, and rerun those tests with strict checking disabled.
|
||||
|
||||
APIs and test cases using the waiver must be clearly identified.
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
1. Workflow
|
||||
|
||||
- Vendor run Tempest suite as usual: via refstack-client test, ostestr, testr
|
||||
or with any other test runner. Some test cases failed due to additional
|
||||
properties in Nova response.
|
||||
|
||||
- Vendor have the subunit test results file from the Tempest test execution.
|
||||
|
||||
- Vendor have the Tempest configuration file.
|
||||
|
||||
- Vendor rerun failed test cases by running ``refstack-client bypass-extras``
|
||||
command. Command identifies failed test cases, disables Tempest strict
|
||||
validations, rerun test cases, and enables strict validations again.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ refstack-client bypass-extras --subunit-file <results> --conf-file <tempest-conf-file>
|
||||
|
||||
- Output of bypass-extras command is a zip bundle containing the following
|
||||
files:
|
||||
|
||||
- tests_list - List of failed test cases due to additional properties.
|
||||
|
||||
- patched_schemas - List of tempest schemas which value was set to True
|
||||
(to allow additional properties).
|
||||
|
||||
- api_details - API call details from each failed test case (due to
|
||||
additional properties).
|
||||
|
||||
- rerun_test_results - The subunit result file for the re-run test cases.
|
||||
|
||||
- combined_test_results.json - The Refstack JSON file with the combined
|
||||
passed TCs from both initial and rerun subunit files.
|
||||
|
||||
2. Implement "bypass-extras" Refstack command:
|
||||
Assume Tempest test suite was run independently.
|
||||
Subunit test results and Tempest configuration files are available.
|
||||
|
||||
*bypass-extras* command is the helper tool for vendors to bypass the strict
|
||||
validation of additional properties in Tempest. Process steps and
|
||||
implementation details are explained on step 3.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ refstack-client --help
|
||||
|
||||
usage: refstack-client [-h] <ARG> ...
|
||||
...
|
||||
bypass-extras Apply Defcore waiver to identify additional properties
|
||||
on Nova API response. Re-runs failed test cases
|
||||
without Tempest strict response validations.
|
||||
|
||||
$ refstack-client bypass-extras --help
|
||||
|
||||
usage: refstack-client [-h] <ARG> ...
|
||||
|
||||
To see help on specific argument, do:
|
||||
refstack-client <ARG> -h waiver
|
||||
[-h] [-s | -v] [-y] [--url URL] [-k] [-i PRIV_KEY] file
|
||||
|
||||
optional arguments:
|
||||
-h, --help Show this help message and exit
|
||||
-s, --silent Suppress output except warnings and errors.
|
||||
-v, --verbose Show verbose output.
|
||||
-y Assume Yes to all prompt queries
|
||||
--subunit-file Path to subunit test result file.
|
||||
-c, --conf-file Path of the Tempest configuration file to use.
|
||||
|
||||
3. Flow for ``bypass-extras`` command.
|
||||
|
||||
Having as input a subunit test results file and a Tempest configuration file:
|
||||
|
||||
3.1 Find failed test cases and its details
|
||||
|
||||
Integrate code from find_additional_properties.py into Refstack-client to
|
||||
analyze subunit stream (from input results file). Find failed test cases
|
||||
due to additional properties in the response. Reconstruct the tempest schema
|
||||
causing the test case failure. Run subunit-describe-calls
|
||||
filter command to get test cases API call details.
|
||||
|
||||
Input: subunit-results
|
||||
|
||||
Output files:
|
||||
|
||||
- tests_list - List of failed test cases due to additional properties.
|
||||
|
||||
- patched_schemas - List of tempest schemas causing errors
|
||||
|
||||
- api_details - API calls from each test case.
|
||||
|
||||
3.2 Patch Tempest:
|
||||
|
||||
Create patch for .tempest virtual environment which lives under refstack-client
|
||||
installation.
|
||||
|
||||
- Modify tempest/lib/api_schema/response/compute/v2_1/__init__.py:
|
||||
|
||||
- Import module where schema lives.
|
||||
|
||||
- Set schema addtionalProperties key to True so that additional properties
|
||||
are accepted - bypass strict validation.
|
||||
|
||||
3.3 Rerun failed test cases using patched refstack-client .tempest environment
|
||||
|
||||
Use tests_list as withelist for ostestr in order to re-run failed test cases.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ostestr --serial -w test_list
|
||||
|
||||
Input: test_list and conf-file files.
|
||||
|
||||
Output: rerun_test_results subunit file
|
||||
|
||||
3.4 Remove Tempest patch
|
||||
|
||||
Regardless of previous steps outcome, unpatching Tempest step will be
|
||||
attempted.
|
||||
|
||||
Clean __init__.py by opening with access mode 'w'
|
||||
|
||||
3.5 Create refstack JSON format files
|
||||
|
||||
Transform subunit result files - The one provided as input and the rerun
|
||||
test results - into a combined refstack JSON format.
|
||||
|
||||
Input: initial_results_file, rerun_test_results files.
|
||||
|
||||
Output: combined_test_results.json files.
|
||||
|
||||
3.6 Create zip bundle
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
- Add additional property to Tempest config file
|
||||
|
||||
- For Tempest patch
|
||||
Comment the validate_response call by looking into the service_client.py file
|
||||
for the corresponding method (search through code files).
|
||||
|
||||
- Manual process
|
||||
Products applying for the OpenStack Powered Trademark in 2016 may
|
||||
request the waiver by submitting subunit data from their Tempest run
|
||||
that can be manually analyzed by the `find_additional_properties.py` script
|
||||
from the DefCore repository. This script will identify tests that
|
||||
failed because of additional properties. The vendor will then need
|
||||
to manually modify tempest-lib to remove additional checks on the impacted
|
||||
APIs.
|
||||
|
||||
Data model impact
|
||||
-----------------
|
||||
|
||||
None
|
||||
|
||||
REST API impact
|
||||
---------------
|
||||
|
||||
None
|
||||
|
||||
Security impact
|
||||
---------------
|
||||
|
||||
None
|
||||
|
||||
Notifications impact
|
||||
--------------------
|
||||
|
||||
None
|
||||
|
||||
Other end user impact
|
||||
---------------------
|
||||
|
||||
None
|
||||
|
||||
Performance Impact
|
||||
------------------
|
||||
|
||||
None
|
||||
|
||||
Other deployer impact
|
||||
---------------------
|
||||
|
||||
None
|
||||
|
||||
Developer impact
|
||||
----------------
|
||||
|
||||
None
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Primary assignee:
|
||||
|
||||
luz-cazares
|
||||
|
||||
Other contributors:
|
||||
|
||||
Chris Hoge
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
- Add *bypass-extras* command to refstack-client.
|
||||
- Integrate find_additional_properties code
|
||||
- Method to call subunit-describe-calls filter
|
||||
- Implement JSON schema gathering from test exception
|
||||
- Implement Tempest patch
|
||||
- Implement Tempest unpatch
|
||||
- Create zip bundle with file results
|
||||
- Add *bypass-extras* command usage documentation
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
None
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
Add unit testing for the new command, verify expected outcomes are met.
|
||||
|
||||
Documentation Impact
|
||||
====================
|
||||
|
||||
Add refstack-client bypass-extras usage information under refstack-client/
|
||||
README.rst
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
None
|
||||
@@ -1,113 +0,0 @@
|
||||
===============================
|
||||
Automatic Tempest Configuration
|
||||
===============================
|
||||
|
||||
Problem Description
|
||||
===================
|
||||
|
||||
A big barrier of entry to running the Interop tests is the fact that
|
||||
configuring tempest is done by the person running the tests and it requires
|
||||
knowledge of tempest that an end user of a cloud may not have.
|
||||
|
||||
Proposed solution
|
||||
=================
|
||||
|
||||
To make running the Interop tests easier for people that don't know anything
|
||||
about tempest, the tempest.conf can be created automatically by the
|
||||
refstack-client and provide an example tempest.conf populated with values from
|
||||
the target cloud.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ source openstackrc file
|
||||
$ refstack-client config -h
|
||||
--os-cloud <name of the cloud> # Using specific cloud.yaml files
|
||||
--use-test-accounts <Use accounts from accounts.yaml>
|
||||
$ # we can also use discover-tempest-config to generate tempest.conf
|
||||
$ discover-tempest-config --create
|
||||
|
||||
Data model impact
|
||||
-----------------
|
||||
|
||||
None
|
||||
|
||||
REST API impact
|
||||
---------------
|
||||
|
||||
None
|
||||
|
||||
Security impact
|
||||
---------------
|
||||
|
||||
* A basic refstack-client assumption is non-admin credentials. If a feature
|
||||
is not discovered by the tool due to lack of permissions, the tool should
|
||||
be able to handle proper exceptions by notifying proper message and continue
|
||||
processing.
|
||||
|
||||
Performance Impact
|
||||
------------------
|
||||
|
||||
None
|
||||
|
||||
Other Deployer Impact
|
||||
---------------------
|
||||
|
||||
One of the main goals of this project is to create a script that can be
|
||||
consumed by any project needing to configure tempest. It is not going to install
|
||||
any tempest plugins.
|
||||
|
||||
Developer impact
|
||||
----------------
|
||||
|
||||
The tool should be generic so that it can be used as a python dependency so
|
||||
that other projects can benefit from it.
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Primary assignee:
|
||||
* Chandan Kumar (chandankumar)
|
||||
* Martin Kopec (martinkopec)
|
||||
* Arx Cruz (arxcruz)
|
||||
* Luigi Toscano (tosky)
|
||||
|
||||
Other contributors:
|
||||
|
||||
TBD
|
||||
|
||||
Work Items
|
||||
==========
|
||||
|
||||
- Refactor the python-tempestconf code to auto-generate the required tempest.conf for running interop tests using non-admin accounts.
|
||||
- Implement `refstack-client config` in order to integrate with refstack.
|
||||
- Add respective CI jobs to test the `refstack-client config` by running Interop tests.
|
||||
- Add proper info message as a feature is not getting created on how to create it.
|
||||
For example: If glance image is not getting uploaded, provide proper commands on how to upload manually.
|
||||
- Add proper documentation stating what configurations are getting generated and how to use them.
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
- Existing script: https://opendev.org/openinfra/python-tempestconf
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
The python-tempestconf project has sufficient testing, and that the
|
||||
refstack-client can depend on upstream testing of the dependent product.
|
||||
|
||||
Documentation Impact
|
||||
====================
|
||||
|
||||
Documentation will be added to the client and readme files that describes
|
||||
how to use the configuration discovery.
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
- Launchpad blueprint: https://blueprints.launchpad.net/refstack/+spec/tempest-config-script
|
||||
- Pike PTG refstack etherpad: https://etherpad.openstack.org/p/refstack-pike-ptg
|
||||
- Queens PTG config script discussion etherpad: https://etherpad.openstack.org/p/InteropDenver2017PTG_TempestAutoconfig
|
||||
@@ -1,307 +0,0 @@
|
||||
==========================================
|
||||
Example Spec - The title of your blueprint
|
||||
==========================================
|
||||
|
||||
Include the URL of your launchpad blueprint:
|
||||
|
||||
https://blueprints.launchpad.net/refstack/+spec/example
|
||||
|
||||
Introduction paragraph -- why are we doing anything? A single paragraph of
|
||||
prose that operators can understand.
|
||||
|
||||
Some notes about using this template:
|
||||
|
||||
* Your spec should be in ReSTructured text, like this template.
|
||||
|
||||
* Please wrap text at 80 columns.
|
||||
|
||||
* The filename in the git repository should match the launchpad URL, for
|
||||
example a URL of:
|
||||
https://blueprints.launchpad.net/refstack/+spec/awesome-thing
|
||||
should be named awesome-thing.rst
|
||||
|
||||
* Please do not delete any of the sections in this template. If you have
|
||||
nothing to say for a whole section, just write: None
|
||||
|
||||
* For help with syntax, see http://sphinx-doc.org/rest.html
|
||||
|
||||
* To test out your formatting, build the docs using tox, or see:
|
||||
http://rst.ninjs.org
|
||||
|
||||
|
||||
Problem description
|
||||
===================
|
||||
|
||||
A detailed description of the problem:
|
||||
|
||||
* For a new feature this might be use cases. Ensure you are clear about the
|
||||
actors in each use case: End User vs Deployer
|
||||
|
||||
* For a major reworking of something existing it would describe the
|
||||
problems in that feature that are being addressed.
|
||||
|
||||
|
||||
Proposed change
|
||||
===============
|
||||
|
||||
Here is where you cover the change you propose to make in detail. How do you
|
||||
propose to solve this problem?
|
||||
|
||||
If this is one part of a larger effort make it clear where this piece ends. In
|
||||
other words, what's the scope of this effort?
|
||||
|
||||
Alternatives
|
||||
------------
|
||||
|
||||
What other ways could we do this thing? Why aren't we using those? This doesn't
|
||||
have to be a full literature review, but it should demonstrate that thought has
|
||||
been put into why the proposed solution is an appropriate one.
|
||||
|
||||
Data model impact
|
||||
-----------------
|
||||
|
||||
Changes which require modifications to the data model often have a wider impact
|
||||
on the system. The community often has strong opinions on how the data model
|
||||
should be evolved, from both a functional and performance perspective. It is
|
||||
therefore important to capture and gain agreement as early as possible on any
|
||||
proposed changes to the data model.
|
||||
|
||||
Questions which need to be addressed by this section include:
|
||||
|
||||
* What new data objects and/or database schema changes is this going to require?
|
||||
|
||||
* What database migrations will accompany this change.
|
||||
|
||||
* How will the initial set of new data objects be generated, for example if you
|
||||
need to take into account existing instances, or modify other existing data
|
||||
describe how that will work.
|
||||
|
||||
REST API impact
|
||||
---------------
|
||||
|
||||
Each API method which is either added or changed should have the following
|
||||
|
||||
* Specification for the method
|
||||
|
||||
* A description of what the method does suitable for use in
|
||||
user documentation
|
||||
|
||||
* Method type (POST/PUT/GET/DELETE)
|
||||
|
||||
* Normal http response code(s)
|
||||
|
||||
* Expected error http response code(s)
|
||||
|
||||
* A description for each possible error code should be included
|
||||
describing semantic errors which can cause it such as
|
||||
inconsistent parameters supplied to the method, or when an
|
||||
instance is not in an appropriate state for the request to
|
||||
succeed. Errors caused by syntactic problems covered by the JSON
|
||||
schema definition do not need to be included.
|
||||
|
||||
* URL for the resource
|
||||
|
||||
* Parameters which can be passed via the url
|
||||
|
||||
* JSON schema definition for the body data if allowed
|
||||
|
||||
* JSON schema definition for the response data if any
|
||||
|
||||
* Example use case including typical API samples for both data supplied
|
||||
by the caller and the response
|
||||
|
||||
* Discuss any policy changes, and discuss what things a deployer needs to
|
||||
think about when defining their policy.
|
||||
|
||||
Example JSON schema definitions can be found in the Nova tree
|
||||
http://git.openstack.org/cgit/openstack/nova/tree/nova/api/openstack/compute/schemas/v3
|
||||
|
||||
Note that the schema should be defined as restrictively as
|
||||
possible. Parameters which are required should be marked as such and
|
||||
only under exceptional circumstances should additional parameters
|
||||
which are not defined in the schema be permitted (eg
|
||||
additionaProperties should be False).
|
||||
|
||||
Reuse of existing predefined parameter types such as regexps for
|
||||
passwords and user defined names is highly encouraged.
|
||||
|
||||
Security impact
|
||||
---------------
|
||||
|
||||
Describe any potential security impact on the system. Some of the items to
|
||||
consider include:
|
||||
|
||||
* Does this change touch sensitive data such as tokens, keys, or user data?
|
||||
|
||||
* Does this change alter the API in a way that may impact security, such as
|
||||
a new way to access sensitive information or a new way to login?
|
||||
|
||||
* Does this change involve cryptography or hashing?
|
||||
|
||||
* Does this change require the use of sudo or any elevated privileges?
|
||||
|
||||
* Does this change involve using or parsing user-provided data? This could
|
||||
be directly at the API level or indirectly such as changes to a cache layer.
|
||||
|
||||
* Can this change enable a resource exhaustion attack, such as allowing a
|
||||
single API interaction to consume significant server resources? Some examples
|
||||
of this include launching subprocesses for each connection, or entity
|
||||
expansion attacks in XML.
|
||||
|
||||
For more detailed guidance, please see the OpenStack Security Guidelines as
|
||||
a reference (https://wiki.openstack.org/wiki/Security/Guidelines). These
|
||||
guidelines are a work in progress and are designed to help you identify
|
||||
security best practices. For further information, feel free to reach out
|
||||
to the OpenStack Security Group at openstack-security@lists.openstack.org.
|
||||
|
||||
Notifications impact
|
||||
--------------------
|
||||
|
||||
Please specify any changes to notifications. Be that an extra notification,
|
||||
changes to an existing notification, or removing a notification.
|
||||
|
||||
Other end user impact
|
||||
---------------------
|
||||
|
||||
Aside from the API, are there other ways a user will interact with this feature?
|
||||
|
||||
* Does this change have an impact on python-novaclient? What does the user
|
||||
interface there look like?
|
||||
|
||||
Performance Impact
|
||||
------------------
|
||||
|
||||
Describe any potential performance impact on the system, for example
|
||||
how often will new code be called, and is there a major change to the calling
|
||||
pattern of existing code.
|
||||
|
||||
Examples of things to consider here include:
|
||||
|
||||
* A periodic task might look like a small addition but if it calls conductor or
|
||||
another service the load is multiplied by the number of nodes in the system.
|
||||
|
||||
* Scheduler filters get called once per host for every instance being created, so
|
||||
any latency they introduce is linear with the size of the system.
|
||||
|
||||
* A small change in a utility function or a commonly used decorator can have a
|
||||
large impacts on performance.
|
||||
|
||||
* Calls which result in a database queries (whether direct or via conductor) can
|
||||
have a profound impact on performance when called in critical sections of the
|
||||
code.
|
||||
|
||||
* Will the change include any locking, and if so what considerations are there on
|
||||
holding the lock?
|
||||
|
||||
Other deployer impact
|
||||
---------------------
|
||||
|
||||
Discuss things that will affect how you deploy and configure OpenStack
|
||||
that have not already been mentioned, such as:
|
||||
|
||||
* What config options are being added? Should they be more generic than
|
||||
proposed (for example a flag that other hypervisor drivers might want to
|
||||
implement as well)? Are the default values ones which will work well in
|
||||
real deployments?
|
||||
|
||||
* Is this a change that takes immediate effect after its merged, or is it
|
||||
something that has to be explicitly enabled?
|
||||
|
||||
* If this change is a new binary, how would it be deployed?
|
||||
|
||||
* Please state anything that those doing continuous deployment, or those
|
||||
upgrading from the previous release, need to be aware of. Also describe
|
||||
any plans to deprecate configuration values or features. For example, if we
|
||||
change the directory name that instances are stored in, how do we handle
|
||||
instance directories created before the change landed? Do we move them? Do
|
||||
we have a special case in the code? Do we assume that the operator will
|
||||
recreate all the instances in their cloud?
|
||||
|
||||
Developer impact
|
||||
----------------
|
||||
|
||||
Discuss things that will affect other developers working on OpenStack,
|
||||
such as:
|
||||
|
||||
* If the blueprint proposes a change to the driver API, discussion of how
|
||||
other hypervisors would implement the feature is required.
|
||||
|
||||
|
||||
Implementation
|
||||
==============
|
||||
|
||||
Assignee(s)
|
||||
-----------
|
||||
|
||||
Who is leading the writing of the code? Or is this a blueprint where you're
|
||||
throwing it out there to see who picks it up?
|
||||
|
||||
If more than one person is working on the implementation, please designate the
|
||||
primary author and contact.
|
||||
|
||||
Primary assignee:
|
||||
<launchpad-id or None>
|
||||
|
||||
Other contributors:
|
||||
<launchpad-id or None>
|
||||
|
||||
Work Items
|
||||
----------
|
||||
|
||||
Work items or tasks -- break the feature up into the things that need to be
|
||||
done to implement it. Those parts might end up being done by different people,
|
||||
but we're mostly trying to understand the timeline for implementation.
|
||||
|
||||
|
||||
Dependencies
|
||||
============
|
||||
|
||||
* Include specific references to specs and/or blueprints in nova, or in other
|
||||
projects, that this one either depends on or is related to.
|
||||
|
||||
* If this requires functionality of another project that is not currently used
|
||||
by Nova (such as the glance v2 API when we previously only required v1),
|
||||
document that fact.
|
||||
|
||||
* Does this feature require any new library dependencies or code otherwise not
|
||||
included in OpenStack? Or does it depend on a specific version of library?
|
||||
|
||||
|
||||
Testing
|
||||
=======
|
||||
|
||||
Please discuss how the change will be tested. We especially want to know what
|
||||
tempest tests will be added. It is assumed that unit test coverage will be
|
||||
added so that doesn't need to be mentioned explicitly, but discussion of why
|
||||
you think unit tests are sufficient and we don't need to add more tempest
|
||||
tests would need to be included.
|
||||
|
||||
Is this untestable in gate given current limitations (specific hardware /
|
||||
software configurations available)? If so, are there mitigation plans (3rd
|
||||
party testing, gate enhancements, etc).
|
||||
|
||||
|
||||
Documentation Impact
|
||||
====================
|
||||
|
||||
What is the impact on the docs team of this change? Some changes might require
|
||||
donating resources to the docs team to have the documentation updated. Don't
|
||||
repeat details discussed above, but please reference them here.
|
||||
|
||||
|
||||
References
|
||||
==========
|
||||
|
||||
Please add any useful references here. You are not required to have any
|
||||
reference. Moreover, this specification should still make sense when your
|
||||
references are unavailable. Examples of what you could include are:
|
||||
|
||||
* Links to mailing list or IRC discussions
|
||||
|
||||
* Links to notes from a summit session
|
||||
|
||||
* Links to relevant research, if appropriate
|
||||
|
||||
* Related specifications as appropriate (e.g. if it's an EC2 thing, link the EC2 docs)
|
||||
|
||||
* Anything else you feel it is worthwhile to refer to
|
||||
@@ -1,6 +0,0 @@
|
||||
# This file is a placeholder for additional requirements for external test suites.
|
||||
# Add packages that should be installed in tempest venv
|
||||
# So tempest can find plugins with additional tests
|
||||
# Examples of how to define packages:
|
||||
#ec2-api>=1.0.0
|
||||
#git+https://opendev.org/openstack/swift.git@master#egg=swift
|
||||
@@ -1,8 +0,0 @@
|
||||
pep8==1.4.5
|
||||
pyflakes>=0.7.2,<2.2.0
|
||||
flake8<3.8.0
|
||||
docutils>=0.11 # OSI-Approved Open Source, Public Domain
|
||||
stestr>=1.1.0 # Apache-2.0
|
||||
testtools>=0.9.34
|
||||
coverage
|
||||
httmock
|
||||
58
tox.ini
58
tox.ini
@@ -1,58 +0,0 @@
|
||||
[tox]
|
||||
envlist = pep8,py3,py38
|
||||
minversion = 3.18
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install -U {opts} {packages}
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
OS_TEST_PATH=./refstack_client/tests/unit
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
git+https://opendev.org/openinfra/python-tempestconf@master#egg=python_tempestconf
|
||||
commands =
|
||||
stestr run {posargs}
|
||||
|
||||
allowlist_externals =
|
||||
bash
|
||||
find
|
||||
distribute = false
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python3
|
||||
commands = flake8
|
||||
distribute = false
|
||||
|
||||
[testenv:venv]
|
||||
basepython = python3
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
basepython = python3
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run --source refstack_client --parallel-mode
|
||||
commands =
|
||||
coverage erase
|
||||
find . -type f -name "*.pyc" -delete
|
||||
stestr run {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[testenv:docs]
|
||||
basepython = python3
|
||||
deps = -c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
|
||||
-r{toxinidir}/doc/requirements.txt
|
||||
commands = sphinx-build -b html doc/source doc/build/html
|
||||
|
||||
[flake8]
|
||||
# E125 continuation line does not distinguish itself from next logical line
|
||||
# H404 multi line docstring should start with a summary
|
||||
# W504 skipped because it is overeager and unnecessary
|
||||
ignore = E125,H404,W504
|
||||
show-source = true
|
||||
builtins = _
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools,build,.tempest
|
||||
Reference in New Issue
Block a user