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:
Christian Berendt
2025-09-23 16:59:38 +02:00
parent b60a7e41f7
commit 2967eb389f
50 changed files with 5 additions and 5027 deletions

View File

@@ -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
View File

@@ -1,17 +0,0 @@
*.pyc
.stestr/
.tox
*.egg-info/
*.egg
.eggs/
.coverage
cover/*
.project
.pydevproject
build
.tempest/
.venv/
etc/
.tempestconf
tempest.log

View File

@@ -1,3 +0,0 @@
[DEFAULT]
test_path=./refstack_client/tests/unit
top_dir=./

View File

@@ -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

View File

@@ -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
View File

@@ -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.

View File

@@ -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".

View File

@@ -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/``.

View File

@@ -1,2 +0,0 @@
sphinx>=1.6.2 # BSD
openstackdocstheme>=1.11.0 # Apache-2.0

View File

@@ -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

View File

@@ -1,5 +0,0 @@
============
Contributing
============
.. include:: ../../CONTRIBUTING.rst

View File

@@ -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`

View File

@@ -1 +0,0 @@
.. include:: ../../README.rst

View File

@@ -1 +0,0 @@
../../specs/

View File

@@ -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}

View File

@@ -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 )

View File

@@ -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
}

View File

@@ -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'

View File

@@ -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

View File

@@ -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)())

View File

@@ -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)

View File

@@ -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)())

View File

@@ -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()

View File

@@ -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()

View File

@@ -1 +0,0 @@
__author__ = 'dlenwell'

View File

@@ -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"

View File

@@ -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()

View File

@@ -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

View File

@@ -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"}
]
}

View File

@@ -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

View File

@@ -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-----

View File

@@ -1,5 +0,0 @@
- username: 'admin'
tenant_name: 'tenant_name'
password: 'test'
roles:
- 'Member'

View File

@@ -1,3 +0,0 @@
tempest.api.test1[gate]
tempest.api.test2
tempest.api.test3[foo,bar](scenario)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View File

@@ -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