Initialize repository
Import doc, code, various boilerplate elements from zuul. Add basic unit testing. Change-Id: I44b78cd9d2a31fb62ddf4ffd56546066c5db2689
This commit is contained in:
parent
2e7c14b411
commit
0bc9db6c89
0
.coveragerc
Normal file
0
.coveragerc
Normal file
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
*.egg
|
||||
*.egg-info
|
||||
*.pyc
|
||||
*.retry
|
||||
.idea
|
||||
.mypy_cache
|
||||
.test
|
||||
.testrepository
|
||||
.tox
|
||||
.venv
|
||||
.coverage
|
||||
.stestr
|
||||
AUTHORS
|
||||
build/*
|
||||
ChangeLog
|
||||
doc/build/*
|
||||
dist/
|
3
.stestr.conf
Normal file
3
.stestr.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
test_path=tests/unit
|
||||
top_dir=./
|
25
.zuul.yaml
Normal file
25
.zuul.yaml
Normal file
@ -0,0 +1,25 @@
|
||||
- project:
|
||||
check:
|
||||
jobs:
|
||||
- opendev-tox-docs
|
||||
- tox-linters:
|
||||
vars:
|
||||
tox_install_bindep: false
|
||||
- tox-py36:
|
||||
nodeset: ubuntu-bionic
|
||||
timeout: 3600
|
||||
- tox-py38:
|
||||
nodeset: ubuntu-bionic
|
||||
timeout: 3600
|
||||
gate:
|
||||
jobs:
|
||||
- opendev-tox-docs
|
||||
- tox-linters:
|
||||
vars:
|
||||
tox_install_bindep: false
|
||||
- tox-py36:
|
||||
nodeset: ubuntu-bionic
|
||||
timeout: 3600
|
||||
- tox-py38:
|
||||
nodeset: ubuntu-bionic
|
||||
timeout: 3600
|
202
LICENSE
Normal file
202
LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
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.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
7
MANIFEST.in
Normal file
7
MANIFEST.in
Normal file
@ -0,0 +1,7 @@
|
||||
include AUTHORS
|
||||
include ChangeLog
|
||||
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
||||
|
||||
global-exclude *.pyc
|
57
README.rst
Normal file
57
README.rst
Normal file
@ -0,0 +1,57 @@
|
||||
Zuul-client
|
||||
===========
|
||||
|
||||
Zuul-client is a CLI tool that can be used to interact with Zuul, the project
|
||||
gating system.
|
||||
|
||||
The latest documentation for Zuul and Zuul-client can be found at:
|
||||
https://zuul-ci.org/docs/zuul/
|
||||
|
||||
Getting Help
|
||||
------------
|
||||
|
||||
There are two Zuul-related mailing lists:
|
||||
|
||||
`zuul-announce <http://lists.zuul-ci.org/cgi-bin/mailman/listinfo/zuul-announce>`_
|
||||
A low-traffic announcement-only list to which every Zuul operator or
|
||||
power-user should subscribe.
|
||||
|
||||
`zuul-discuss <http://lists.zuul-ci.org/cgi-bin/mailman/listinfo/zuul-discuss>`_
|
||||
General discussion about Zuul, including questions about how to use
|
||||
it, and future development.
|
||||
|
||||
You will also find Zuul developers in the `#zuul` channel on Freenode
|
||||
IRC.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
To browse the latest code, see: https://opendev.org/zuul/zuul-client
|
||||
To clone the latest code, use `git clone https://opendev.org/zuul/zuul-client`
|
||||
|
||||
Bugs are handled at: https://storyboard.openstack.org/#!/project/zuul/zuul
|
||||
|
||||
Suspected security vulnerabilities are most appreciated if first
|
||||
reported privately following any of the supported mechanisms
|
||||
described at https://zuul-ci.org/docs/zuul/user/vulnerabilities.html
|
||||
|
||||
Code reviews are handled by gerrit at https://review.opendev.org
|
||||
|
||||
After creating a Gerrit account, use `git review` to submit patches.
|
||||
Example::
|
||||
|
||||
# Do your commits
|
||||
$ git review
|
||||
# Enter your username if prompted
|
||||
|
||||
Join `#zuul` on Freenode to discuss development or usage.
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
Zuul-client is free software, and licensed under the Apache License, version 2.0.
|
||||
|
||||
Python Version Support
|
||||
----------------------
|
||||
|
||||
Zuul-client requires Python 3. It does not support Python 2.
|
51
TESTING.rst
Normal file
51
TESTING.rst
Normal file
@ -0,0 +1,51 @@
|
||||
=================
|
||||
Testing Your Code
|
||||
=================
|
||||
------------
|
||||
A Quickstart
|
||||
------------
|
||||
|
||||
This is designed to be enough information for you to run your first tests.
|
||||
|
||||
*Install pip*::
|
||||
|
||||
[apt-get | yum] install python-pip
|
||||
|
||||
More information on pip here: http://www.pip-installer.org/en/latest/
|
||||
|
||||
*Use pip to install tox*::
|
||||
|
||||
pip install tox
|
||||
|
||||
Run The Tests
|
||||
-------------
|
||||
|
||||
*Navigate to the project's root directory and execute*::
|
||||
|
||||
tox
|
||||
|
||||
Information about tox can be found here: http://testrun.org/tox/latest/
|
||||
|
||||
|
||||
Run The Tests in One Environment
|
||||
--------------------------------
|
||||
|
||||
Tox will run your entire test suite in the environments specified in the project tox.ini::
|
||||
|
||||
[tox]
|
||||
|
||||
envlist = <list of available environments>
|
||||
|
||||
To run the test suite in just one of the environments in envlist execute::
|
||||
|
||||
tox -e <env>
|
||||
so for example, *run the test suite in py36*::
|
||||
|
||||
tox -e py36
|
||||
|
||||
Run One Test
|
||||
------------
|
||||
|
||||
To run individual tests with tox::
|
||||
|
||||
tox -e <env> -- path.to.module.Class.test
|
153
doc/Makefile
Normal file
153
doc/Makefile
Normal file
@ -0,0 +1,153 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS = -W
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Zuul.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Zuul.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/Zuul"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Zuul"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
5
doc/requirements.txt
Normal file
5
doc/requirements.txt
Normal file
@ -0,0 +1,5 @@
|
||||
sphinx>=1.6.1
|
||||
sphinxcontrib-programoutput
|
||||
sphinx-autodoc-typehints
|
||||
reno>=2.8.0 # Apache-2.0
|
||||
zuul-sphinx
|
167
doc/source/commands.rst
Normal file
167
doc/source/commands.rst
Normal file
@ -0,0 +1,167 @@
|
||||
:title: Commands
|
||||
|
||||
Commands
|
||||
========
|
||||
|
||||
Privileged commands
|
||||
-------------------
|
||||
|
||||
Some commands require a valid authentication token to be passed as the ``--auth-token``
|
||||
argument. Administrators can generate such a token for users as needed.
|
||||
|
||||
Usage
|
||||
-----
|
||||
The general options that apply to all subcommands are:
|
||||
|
||||
.. program-output:: zuul-client --help
|
||||
|
||||
The following subcommands are supported:
|
||||
|
||||
Autohold
|
||||
^^^^^^^^
|
||||
|
||||
.. note:: This command is only available with a valid authentication token.
|
||||
|
||||
.. program-output:: zuul-client autohold --help
|
||||
|
||||
Example::
|
||||
|
||||
zuul-client autohold --tenant openstack --project example_project --job example_job --reason "reason text" --count 1
|
||||
|
||||
Autohold Delete
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
.. note:: This command is only available with a valid authentication token.
|
||||
|
||||
.. program-output:: zuul-client autohold-delete --help
|
||||
|
||||
Example::
|
||||
|
||||
zuul-client autohold-delete --tenant openstack --id 0000000123
|
||||
|
||||
Autohold Info
|
||||
^^^^^^^^^^^^^
|
||||
.. program-output:: zuul-client autohold-info --help
|
||||
|
||||
Example::
|
||||
|
||||
zuul-client autohold-info --tenant openstack --id 0000000123
|
||||
|
||||
Autohold List
|
||||
^^^^^^^^^^^^^
|
||||
.. program-output:: zuul-client autohold-list --help
|
||||
|
||||
Example::
|
||||
|
||||
zuul-client autohold-list --tenant openstack
|
||||
|
||||
Dequeue
|
||||
^^^^^^^
|
||||
|
||||
.. note:: This command is only available with a valid authentication token.
|
||||
|
||||
.. program-output:: zuul-client dequeue --help
|
||||
|
||||
Examples::
|
||||
|
||||
zuul-client dequeue --tenant openstack --pipeline check --project example_project --change 5,1
|
||||
zuul-client dequeue --tenant openstack --pipeline periodic --project example_project --ref refs/heads/master
|
||||
|
||||
Enqueue
|
||||
^^^^^^^
|
||||
|
||||
.. note:: This command is only available with a valid authentication token.
|
||||
|
||||
.. program-output:: zuul-client enqueue --help
|
||||
|
||||
Example::
|
||||
|
||||
zuul-client enqueue --tenant openstack --trigger gerrit --pipeline check --project example_project --change 12345,1
|
||||
|
||||
Note that the format of change id is <number>,<patchset>.
|
||||
|
||||
Enqueue-ref
|
||||
^^^^^^^^^^^
|
||||
|
||||
.. note:: This command is only available with a valid authentication token.
|
||||
|
||||
.. program-output:: zuul-client enqueue-ref --help
|
||||
|
||||
This command is provided to manually simulate a trigger from an
|
||||
external source. It can be useful for testing or replaying a trigger
|
||||
that is difficult or impossible to recreate at the source. The
|
||||
arguments to ``enqueue-ref`` will vary depending on the source and
|
||||
type of trigger. Some familiarity with the arguments emitted by
|
||||
``gerrit`` `update hooks
|
||||
<https://gerrit-review.googlesource.com/admin/projects/plugins/hooks>`__
|
||||
such as ``patchset-created`` and ``ref-updated`` is recommended. Some
|
||||
examples of common operations are provided below.
|
||||
|
||||
Manual enqueue examples
|
||||
***********************
|
||||
|
||||
It is common to have a ``release`` pipeline that listens for new tags
|
||||
coming from ``gerrit`` and performs a range of code packaging jobs.
|
||||
If there is an unexpected issue in the release jobs, the same tag can
|
||||
not be recreated in ``gerrit`` and the user must either tag a new
|
||||
release or request a manual re-triggering of the jobs. To re-trigger
|
||||
the jobs, pass the failed tag as the ``ref`` argument and set
|
||||
``newrev`` to the change associated with the tag in the project
|
||||
repository (i.e. what you see from ``git show X.Y.Z``)::
|
||||
|
||||
zuul-client enqueue-ref --tenant openstack --trigger gerrit --pipeline release --project openstack/example_project --ref refs/tags/X.Y.Z --newrev abc123...
|
||||
|
||||
The command can also be used asynchronosly trigger a job in a
|
||||
``periodic`` pipeline that would usually be run at a specific time by
|
||||
the ``timer`` driver. For example, the following command would
|
||||
trigger the ``periodic`` jobs against the current ``master`` branch
|
||||
top-of-tree for a project::
|
||||
|
||||
zuul-client enqueue-ref --tenant openstack --trigger timer --pipeline periodic --project openstack/example_project --ref refs/heads/master
|
||||
|
||||
Another common pipeline is a ``post`` queue listening for ``gerrit``
|
||||
merge results. Triggering here is slightly more complicated as you
|
||||
wish to recreate the full ``ref-updated`` event from ``gerrit``. For
|
||||
a new commit on ``master``, the gerrit ``ref-updated`` trigger
|
||||
expresses "reset ``refs/heads/master`` for the project from ``oldrev``
|
||||
to ``newrev``" (``newrev`` being the committed change). Thus to
|
||||
replay the event, you could ``git log`` in the project and take the
|
||||
current ``HEAD`` and the prior change, then enqueue the event::
|
||||
|
||||
NEW_REF=$(git rev-parse HEAD)
|
||||
OLD_REF=$(git rev-parse HEAD~1)
|
||||
|
||||
zuul-client enqueue-ref --tenant openstack --trigger gerrit --pipeline post --project openstack/example_project --ref refs/heads/master --newrev $NEW_REF --oldrev $OLD_REF
|
||||
|
||||
Note that zero values for ``oldrev`` and ``newrev`` can indicate
|
||||
branch creation and deletion; the source code of Zuul is the best reference
|
||||
for these more advanced operations.
|
||||
|
||||
|
||||
Promote
|
||||
^^^^^^^
|
||||
|
||||
.. note:: This command is only available with a valid authentication token.
|
||||
|
||||
.. program-output:: zuul-client promote --help
|
||||
|
||||
This command will push the listed changes at the top of the chosen pipeline.
|
||||
|
||||
Example::
|
||||
|
||||
zuul-client promote --tenant openstack --pipeline check --changes 12345,1 13336,3
|
||||
|
||||
Note that the format of changes id is <number>,<patchset>.
|
||||
|
||||
The promote action is used to reorder the change queue in a pipeline, by putting
|
||||
the provided changes at the top of the queue; therefore this action makes the most
|
||||
sense when performed against a dependent pipeline.
|
||||
|
||||
The most common use case for the promote action is the need to merge an urgent fix
|
||||
when the gate pipeline has already several patches queued ahead. This is especially
|
||||
needed if there is concern that one or more changes ahead in the queue may fail,
|
||||
thus increasing the time to land for the fix; or concern that the fix may not
|
||||
pass validation if applied on top of the current patch queue in the gate.
|
||||
|
||||
If the queue of a dependent pipeline is targeted by the promote, all the ongoing
|
||||
jobs in that queue will be canceled and restarted on top of the promoted changes.
|
60
doc/source/conf.py
Normal file
60
doc/source/conf.py
Normal file
@ -0,0 +1,60 @@
|
||||
# Configuration file for the Sphinx documentation builder.
|
||||
#
|
||||
# This file only contains a selection of the most common options. For a full
|
||||
# list see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
# -- Path setup --------------------------------------------------------------
|
||||
|
||||
# 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.
|
||||
#
|
||||
# import os
|
||||
# import sys
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
project = 'Zuul-client'
|
||||
copyright = '2020, OpenStack'
|
||||
author = 'OpenStack'
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx_autodoc_typehints',
|
||||
'sphinxcontrib.programoutput',
|
||||
'zuul_sphinx',
|
||||
# 'zuul.sphinx.zuul',
|
||||
'reno.sphinxext',
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This pattern also affects html_static_path and html_extra_path.
|
||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# -- 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'
|
||||
|
||||
# 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']
|
12
doc/source/configuration.rst
Normal file
12
doc/source/configuration.rst
Normal file
@ -0,0 +1,12 @@
|
||||
:title: Configuration
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
The web client will look by default for a ``.zuul.conf`` file for its
|
||||
configuration. The file should consist of a ``[webclient]`` section with at least
|
||||
the ``url`` attribute set. The optional ``verify_ssl`` can be set to False to
|
||||
disable SSL verifications when connecting to Zuul (defaults to True).
|
||||
|
||||
It is also possible to run the web client without a configuration file, by using the
|
||||
``--zuul-url`` option to specify the base URL of the Zuul web server.
|
7
doc/source/examples/.zuul.conf
Normal file
7
doc/source/examples/.zuul.conf
Normal file
@ -0,0 +1,7 @@
|
||||
[opendev]
|
||||
url=https://zuul.opendev.org
|
||||
verify_ssl=True
|
||||
|
||||
[softwarefactory]
|
||||
url=https://softwarefactory-project.io/zuul/
|
||||
verify_ssl=True
|
16
doc/source/index.rst
Normal file
16
doc/source/index.rst
Normal file
@ -0,0 +1,16 @@
|
||||
Zuul-client - User CLI for the Zuul Project Gating System
|
||||
=========================================================
|
||||
|
||||
zuul-client is a simple command line client that may be used to query Zuul's
|
||||
state or affect its behavior, granted the user is allowed to do so. It must be
|
||||
run on a host with access to Zuul's web server.
|
||||
|
||||
Documentation
|
||||
-------------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
installation
|
||||
configuration
|
||||
commands
|
18
doc/source/installation.rst
Normal file
18
doc/source/installation.rst
Normal file
@ -0,0 +1,18 @@
|
||||
:title: Installation
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
*Install pip*::
|
||||
|
||||
[apt-get | yum] install python-pip
|
||||
|
||||
More information on pip here: http://www.pip-installer.org/en/latest/
|
||||
|
||||
*Install dependencies*::
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
*Install zuul-client*::
|
||||
|
||||
python setup.py install
|
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
pbr>=1.1.0
|
||||
requests==2.24.0
|
||||
urllib3!=1.25.4,!=1.25.5 # https://github.com/urllib3/urllib3/pull/1684
|
32
setup.cfg
Normal file
32
setup.cfg
Normal file
@ -0,0 +1,32 @@
|
||||
[metadata]
|
||||
name = zuul
|
||||
summary = A Project Gating System
|
||||
description-file =
|
||||
README.rst
|
||||
author = Zuul Team
|
||||
author-email = zuul-discuss@lists.zuul-ci.org
|
||||
home-page = https://zuul-ci.org/
|
||||
python-requires = >=3.6
|
||||
classifier =
|
||||
Intended Audience :: Information Technology
|
||||
Intended Audience :: System Administrators
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: POSIX :: Linux
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.6
|
||||
Programming Language :: Python :: 3.7
|
||||
Programming Language :: Python :: 3.8
|
||||
|
||||
[pbr]
|
||||
warnerrors = True
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
zuul-client = zuulclient.cmd:main
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
warning-is-error = 1
|
21
setup.py
Normal file
21
setup.py
Normal file
@ -0,0 +1,21 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import setuptools
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr'],
|
||||
pbr=True)
|
3
test-requirements.txt
Normal file
3
test-requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
coverage>=3.6
|
||||
stestr>=1.0.0 # Apache-2.0
|
||||
testtools>=0.9.32
|
13
tests/__init__.py
Normal file
13
tests/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
38
tests/unit/__init__.py
Normal file
38
tests/unit/__init__.py
Normal file
@ -0,0 +1,38 @@
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import logging
|
||||
import testtools
|
||||
|
||||
|
||||
class BaseTestCase(testtools.TestCase):
|
||||
log = logging.getLogger("zuulclient.test")
|
||||
|
||||
def setUp(self):
|
||||
super(BaseTestCase, self).setUp()
|
||||
|
||||
|
||||
class FakeRequestResponse(object):
|
||||
def __init__(self, status_code=None, json=None, exception_msg=None):
|
||||
self._json = json
|
||||
self.status_code = status_code
|
||||
self.exception_msg = exception_msg or 'Error'
|
||||
|
||||
def json(self):
|
||||
return self._json
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
raise Exception(self.exception_msg)
|
284
tests/unit/test_api.py
Normal file
284
tests/unit/test_api.py
Normal file
@ -0,0 +1,284 @@
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from tests.unit import BaseTestCase
|
||||
from tests.unit import FakeRequestResponse
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from zuulclient.api import ZuulRESTClient
|
||||
from zuulclient.api import ZuulRESTException
|
||||
|
||||
|
||||
class TestApi(BaseTestCase):
|
||||
|
||||
def test_client_init(self):
|
||||
"""Test initialization of a client"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
self.assertEqual('https://fake.zuul/', client.url)
|
||||
self.assertEqual('https://fake.zuul/api/', client.base_url)
|
||||
self.assertEqual(False, client.session.verify)
|
||||
self.assertFalse('Authorization' in client.session.headers)
|
||||
client = ZuulRESTClient(url='https://fake.zuul')
|
||||
self.assertEqual('https://fake.zuul/', client.url)
|
||||
self.assertEqual('https://fake.zuul/api/', client.base_url)
|
||||
client = ZuulRESTClient(url='https://fake.zuul/with/path/')
|
||||
self.assertEqual('https://fake.zuul/with/path/', client.url)
|
||||
self.assertEqual('https://fake.zuul/with/path/api/', client.base_url)
|
||||
token = 'aiaiaiai'
|
||||
client = ZuulRESTClient(url='https://fake.zuul/', verify=True,
|
||||
auth_token=token)
|
||||
self.assertEqual('https://fake.zuul/', client.url)
|
||||
self.assertEqual('https://fake.zuul/api/', client.base_url)
|
||||
self.assertEqual(True, client.session.verify)
|
||||
self.assertEqual('Bearer %s' % token,
|
||||
client.session.headers.get('Authorization'))
|
||||
|
||||
def _test_status_check(self, client, verb, func, *args, **kwargs):
|
||||
# validate request errors
|
||||
for error_code, regex in [(401, 'Unauthorized'),
|
||||
(403, 'Insufficient privileges'),
|
||||
(500, 'Unknown error')]:
|
||||
with self.assertRaisesRegex(ZuulRESTException,
|
||||
regex):
|
||||
req = FakeRequestResponse(error_code)
|
||||
if verb == 'post':
|
||||
client.session.post = MagicMock(return_value=req)
|
||||
elif verb == 'get':
|
||||
client.session.get = MagicMock(return_value=req)
|
||||
elif verb == 'delete':
|
||||
client.session.delete = MagicMock(return_value=req)
|
||||
else:
|
||||
raise Exception('Unknown HTTP "verb" %s' % verb)
|
||||
func(*args, **kwargs)
|
||||
|
||||
def test_autohold(self):
|
||||
"""Test autohold"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# token required
|
||||
with self.assertRaisesRegex(Exception, 'Auth Token required'):
|
||||
client.autohold(
|
||||
'tenant', 'project', 'job', 1, None, 'reason', 1, 3600)
|
||||
|
||||
client = ZuulRESTClient(url='https://fake.zuul/',
|
||||
auth_token='aiaiaiai')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'post', client.autohold,
|
||||
'tenant', 'project', 'job', 1, None, 'reason', 1, 3600)
|
||||
# test REST call
|
||||
req = FakeRequestResponse(200, True)
|
||||
client.session.post = MagicMock(return_value=req)
|
||||
ah = client.autohold(
|
||||
'tenant', 'project', 'job', 1, None, 'reason', 1, 3600)
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant/project/project/autohold',
|
||||
json={'reason': 'reason',
|
||||
'count': 1,
|
||||
'job': 'job',
|
||||
'change': 1,
|
||||
'ref': None,
|
||||
'node_hold_expiration': 3600}
|
||||
)
|
||||
self.assertEqual(True, ah)
|
||||
|
||||
def test_autohold_list(self):
|
||||
"""Test autohold-list"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'get', client.autohold_list, 'tenant1')
|
||||
|
||||
fakejson = [
|
||||
{'id': 123,
|
||||
'tenant': 'tenant1',
|
||||
'project': 'project1',
|
||||
'job': 'job1',
|
||||
'ref_filter': '.*',
|
||||
'max_count': 1,
|
||||
'current_count': 0,
|
||||
'reason': 'because',
|
||||
'nodes': ['node1', 'node2']}
|
||||
]
|
||||
req = FakeRequestResponse(200, fakejson)
|
||||
client.session.get = MagicMock(return_value=req)
|
||||
ahl = client.autohold_list('tenant1')
|
||||
client.session.get.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold')
|
||||
self.assertEqual(fakejson, ahl)
|
||||
|
||||
def test_autohold_delete(self):
|
||||
"""Test autohold-delete"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# token required
|
||||
with self.assertRaisesRegex(Exception, 'Auth Token required'):
|
||||
client.autohold_delete(123, 'tenant1')
|
||||
|
||||
client = ZuulRESTClient(url='https://fake.zuul/',
|
||||
auth_token='aiaiaiai')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'delete', client.autohold_delete,
|
||||
123, 'tenant1')
|
||||
|
||||
# test REST call
|
||||
req = FakeRequestResponse(204)
|
||||
client.session.delete = MagicMock(return_value=req)
|
||||
ahd = client.autohold_delete(123, 'tenant1')
|
||||
client.session.delete.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold/123'
|
||||
)
|
||||
self.assertEqual(True, ahd)
|
||||
|
||||
def test_autohold_info(self):
|
||||
"""Test autohold-info"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'get', client.autohold_info, 123, 'tenant1')
|
||||
|
||||
fakejson = {
|
||||
'id': 123,
|
||||
'tenant': 'tenant1',
|
||||
'project': 'project1',
|
||||
'job': 'job1',
|
||||
'ref_filter': '.*',
|
||||
'max_count': 1,
|
||||
'current_count': 0,
|
||||
'reason': 'because',
|
||||
'nodes': ['node1', 'node2']
|
||||
}
|
||||
req = FakeRequestResponse(200, fakejson)
|
||||
client.session.get = MagicMock(return_value=req)
|
||||
ahl = client.autohold_info(tenant='tenant1', id=123)
|
||||
client.session.get.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold/123')
|
||||
self.assertEqual(fakejson, ahl)
|
||||
|
||||
def test_enqueue(self):
|
||||
"""Test enqueue"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# token required
|
||||
with self.assertRaisesRegex(Exception, 'Auth Token required'):
|
||||
client.enqueue('tenant1', 'check', 'project1', '1,1')
|
||||
|
||||
client = ZuulRESTClient(url='https://fake.zuul/',
|
||||
auth_token='aiaiaiai')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'post', client.enqueue,
|
||||
'tenant1', 'check', 'project1', '1,1')
|
||||
|
||||
# test REST call
|
||||
req = FakeRequestResponse(200, True)
|
||||
client.session.post = MagicMock(return_value=req)
|
||||
enq = client.enqueue('tenant1', 'check', 'project1', '1,1')
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/project/project1/enqueue',
|
||||
json={'change': '1,1',
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, enq)
|
||||
|
||||
def test_enqueue_ref(self):
|
||||
"""Test enqueue ref"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# token required
|
||||
with self.assertRaisesRegex(Exception, 'Auth Token required'):
|
||||
client.enqueue_ref(
|
||||
'tenant1', 'check', 'project1', 'refs/heads/stable', '0', '0')
|
||||
|
||||
client = ZuulRESTClient(url='https://fake.zuul/',
|
||||
auth_token='aiaiaiai')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'post', client.enqueue_ref,
|
||||
'tenant1', 'check', 'project1', 'refs/heads/stable', '0', '0')
|
||||
|
||||
# test REST call
|
||||
req = FakeRequestResponse(200, True)
|
||||
client.session.post = MagicMock(return_value=req)
|
||||
enq_ref = client.enqueue_ref(
|
||||
'tenant1', 'check', 'project1', 'refs/heads/stable', '0', '0')
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/project/project1/enqueue',
|
||||
json={'ref': 'refs/heads/stable',
|
||||
'oldrev': '0',
|
||||
'newrev': '0',
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, enq_ref)
|
||||
|
||||
def test_dequeue(self):
|
||||
"""Test dequeue"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# token required
|
||||
with self.assertRaisesRegex(Exception, 'Auth Token required'):
|
||||
client.dequeue('tenant1', 'check', 'project1', '1,1')
|
||||
|
||||
client = ZuulRESTClient(url='https://fake.zuul/',
|
||||
auth_token='aiaiaiai')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'post', client.dequeue,
|
||||
'tenant1', 'check', 'project1', '1,1')
|
||||
|
||||
# test conditions on ref and change
|
||||
with self.assertRaisesRegex(Exception, 'need change OR ref'):
|
||||
client.dequeue(
|
||||
'tenant1', 'check', 'project1', '1,1', 'refs/heads/stable')
|
||||
|
||||
# test REST call
|
||||
req = FakeRequestResponse(200, True)
|
||||
client.session.post = MagicMock(return_value=req)
|
||||
deq = client.dequeue('tenant1', 'check', 'project1', change='1,1')
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/project/project1/dequeue',
|
||||
json={'change': '1,1',
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, deq)
|
||||
deq = client.dequeue(
|
||||
'tenant1', 'check', 'project1', ref='refs/heads/stable')
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/project/project1/dequeue',
|
||||
json={'ref': 'refs/heads/stable',
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, deq)
|
||||
|
||||
def test_promote(self):
|
||||
"""Test promote"""
|
||||
client = ZuulRESTClient(url='https://fake.zuul/')
|
||||
# token required
|
||||
with self.assertRaisesRegex(Exception, 'Auth Token required'):
|
||||
client.promote('tenant1', 'check', ['1,1', '2,1'])
|
||||
|
||||
client = ZuulRESTClient(url='https://fake.zuul/',
|
||||
auth_token='aiaiaiai')
|
||||
# test status checks
|
||||
self._test_status_check(
|
||||
client, 'post', client.promote,
|
||||
'tenant1', 'check', ['1,1', '2,1'])
|
||||
|
||||
# test REST call
|
||||
req = FakeRequestResponse(200, True)
|
||||
client.session.post = MagicMock(return_value=req)
|
||||
prom = client.promote('tenant1', 'check', ['1,1', '2,1'])
|
||||
client.session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/promote',
|
||||
json={'change_ids': ['1,1', '2,1'],
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(True, prom)
|
305
tests/unit/test_cmd.py
Normal file
305
tests/unit/test_cmd.py
Normal file
@ -0,0 +1,305 @@
|
||||
# Copyright 2020 Red Hat, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from tests.unit import BaseTestCase
|
||||
from tests.unit import FakeRequestResponse
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from zuulclient.cmd import ZuulClient
|
||||
|
||||
|
||||
class TestCmd(BaseTestCase):
|
||||
|
||||
def test_client_args_errors(self):
|
||||
"""Test bad CLI arguments when instantiating client"""
|
||||
ZC = ZuulClient()
|
||||
with self.assertRaisesRegex(Exception,
|
||||
'Either specify --zuul-url or '
|
||||
'use a config file'):
|
||||
ZC._main(['--zuul-url', 'https://fake.zuul',
|
||||
'--use-config', 'fakezuul',
|
||||
'autohold',
|
||||
'--tenant', 'tenant1', '--project', 'project1',
|
||||
'--job', 'job1', '--change', '3',
|
||||
'--reason', 'some reason',
|
||||
'--node-hold-expiration', '3600'])
|
||||
|
||||
def test_autohold(self):
|
||||
"""Test autohold via CLI"""
|
||||
ZC = ZuulClient()
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.post = MagicMock(
|
||||
return_value=FakeRequestResponse(200, True))
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'autohold',
|
||||
'--tenant', 'tenant1', '--project', 'project1',
|
||||
'--job', 'job1', '--change', '3', '--reason', 'some reason',
|
||||
'--node-hold-expiration', '3600'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/autohold',
|
||||
json={'reason': 'some reason',
|
||||
'count': 1,
|
||||
'job': 'job1',
|
||||
'change': '3',
|
||||
'ref': '',
|
||||
'node_hold_expiration': 3600}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
|
||||
def test_autohold_args_errors(self):
|
||||
"""Test wrong arguments for autohold"""
|
||||
ZC = ZuulClient()
|
||||
with self.assertRaisesRegex(Exception,
|
||||
"Change and ref can't be both used "
|
||||
"for the same request"):
|
||||
ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'autohold',
|
||||
'--tenant', 'tenant1', '--project', 'project1',
|
||||
'--job', 'job1', '--change', '3', '--reason', 'some reason',
|
||||
'--ref', '/refs/heads/master',
|
||||
'--node-hold-expiration', '3600'])
|
||||
with self.assertRaisesRegex(Exception,
|
||||
"Error: change argument can not "
|
||||
"contain any ','"):
|
||||
ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'autohold',
|
||||
'--tenant', 'tenant1', '--project', 'project1',
|
||||
'--job', 'job1', '--change', '3,2', '--reason', 'some reason',
|
||||
'--node-hold-expiration', '3600'])
|
||||
|
||||
def test_parse_arguments(self):
|
||||
""" Test wrong arguments in parseArguments precheck"""
|
||||
ZC = ZuulClient()
|
||||
with self.assertRaisesRegex(Exception,
|
||||
"The old and new revisions must "
|
||||
"not be the same."):
|
||||
ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'enqueue-ref',
|
||||
'--tenant', 'tenant1', '--project', 'project1',
|
||||
'--pipeline', 'check',
|
||||
'--ref', '/refs/heads/master',
|
||||
'--oldrev', '1234', '--newrev', '1234'])
|
||||
with self.assertRaisesRegex(Exception,
|
||||
"The 'change' and 'ref' arguments are "
|
||||
"mutually exclusive."):
|
||||
ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'dequeue',
|
||||
'--tenant', 'tenant1', '--project', 'project1',
|
||||
'--pipeline', 'post', '--change', '3,2',
|
||||
'--ref', '/refs/heads/master'])
|
||||
|
||||
def test_autohold_delete(self):
|
||||
"""Test autohold-delete via CLI"""
|
||||
ZC = ZuulClient()
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.delete = MagicMock(
|
||||
return_value=FakeRequestResponse(204))
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'autohold-delete',
|
||||
'--tenant', 'tenant1', '1234'])
|
||||
session.delete.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold/1234')
|
||||
self.assertEqual(0, exit_code)
|
||||
|
||||
def test_autohold_info(self):
|
||||
"""Test autohold-info via CLI"""
|
||||
ZC = ZuulClient()
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.get = MagicMock(
|
||||
return_value=FakeRequestResponse(
|
||||
200,
|
||||
json={'id': 1234,
|
||||
'tenant': 'tenant1',
|
||||
'project': 'project1',
|
||||
'job': 'job1',
|
||||
'ref_filter': '.*',
|
||||
'max_count': 1,
|
||||
'current_count': 0,
|
||||
'node_expiration': 0,
|
||||
'expired': 0,
|
||||
'reason': 'some_reason',
|
||||
'nodes': ['node1', 'node2']}))
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul', 'autohold-info',
|
||||
'--tenant', 'tenant1', '1234'])
|
||||
session.get.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/autohold/1234')
|
||||
self.assertEqual(0, exit_code)
|
||||
session.get = MagicMock(
|
||||
return_value=FakeRequestResponse(404,
|
||||
exception_msg='Not found'))
|
||||
with self.assertRaisesRegex(Exception, 'Not found'):
|
||||
ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul', 'autohold-info',
|
||||
'--tenant', 'tenant1', '1234'])
|
||||
|
||||
def test_enqueue(self):
|
||||
"""Test enqueue via CLI"""
|
||||
ZC = ZuulClient()
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.post = MagicMock(
|
||||
return_value=FakeRequestResponse(200, True))
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'enqueue',
|
||||
'--pipeline', 'check',
|
||||
'--tenant', 'tenant1', '--change', '3,1',
|
||||
'--project', 'project1'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/enqueue',
|
||||
json={'change': '3,1',
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
|
||||
def test_enqueue_ref(self):
|
||||
"""Test enqueue-ref via CLI"""
|
||||
ZC = ZuulClient()
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.post = MagicMock(
|
||||
return_value=FakeRequestResponse(200, True))
|
||||
# ensure default revs are set
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'enqueue-ref',
|
||||
'--pipeline', 'check',
|
||||
'--tenant', 'tenant1', '--ref', 'refs/heads/stable',
|
||||
'--project', 'project1'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/enqueue',
|
||||
json={'ref': 'refs/heads/stable',
|
||||
'pipeline': 'check',
|
||||
'oldrev': '0000000000000000000000000000000000000000',
|
||||
'newrev': '0000000000000000000000000000000000000000'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'enqueue-ref',
|
||||
'--pipeline', 'check',
|
||||
'--tenant', 'tenant1', '--ref', 'refs/heads/stable',
|
||||
'--project', 'project1',
|
||||
'--oldrev', 'ababababab'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/enqueue',
|
||||
json={'ref': 'refs/heads/stable',
|
||||
'pipeline': 'check',
|
||||
'oldrev': 'ababababab',
|
||||
'newrev': '0000000000000000000000000000000000000000'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'enqueue-ref',
|
||||
'--pipeline', 'check',
|
||||
'--tenant', 'tenant1', '--ref', 'refs/heads/stable',
|
||||
'--project', 'project1',
|
||||
'--newrev', 'ababababab'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/enqueue',
|
||||
json={'ref': 'refs/heads/stable',
|
||||
'pipeline': 'check',
|
||||
'newrev': 'ababababab',
|
||||
'oldrev': '0000000000000000000000000000000000000000'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'enqueue-ref',
|
||||
'--pipeline', 'check',
|
||||
'--tenant', 'tenant1', '--ref', 'refs/heads/stable',
|
||||
'--project', 'project1',
|
||||
'--oldrev', 'ababababab',
|
||||
'--newrev', 'bababababa'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/enqueue',
|
||||
json={'ref': 'refs/heads/stable',
|
||||
'pipeline': 'check',
|
||||
'oldrev': 'ababababab',
|
||||
'newrev': 'bababababa'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
|
||||
def test_dequeue(self):
|
||||
"""Test dequeue via CLI"""
|
||||
ZC = ZuulClient()
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.post = MagicMock(
|
||||
return_value=FakeRequestResponse(200, True))
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'dequeue',
|
||||
'--pipeline', 'tag',
|
||||
'--tenant', 'tenant1', '--ref', 'refs/heads/stable',
|
||||
'--project', 'project1'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/dequeue',
|
||||
json={'ref': 'refs/heads/stable',
|
||||
'pipeline': 'tag'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'dequeue',
|
||||
'--pipeline', 'check',
|
||||
'--tenant', 'tenant1', '--change', '3,3',
|
||||
'--project', 'project1'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/'
|
||||
'project/project1/dequeue',
|
||||
json={'change': '3,3',
|
||||
'pipeline': 'check'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
||||
|
||||
def test_promote(self):
|
||||
"""Test promote via CLI"""
|
||||
ZC = ZuulClient()
|
||||
with patch('requests.Session') as mock_sesh:
|
||||
session = mock_sesh.return_value
|
||||
session.post = MagicMock(
|
||||
return_value=FakeRequestResponse(200, True))
|
||||
exit_code = ZC._main(
|
||||
['--zuul-url', 'https://fake.zuul',
|
||||
'--auth-token', 'aiaiaiai', 'promote',
|
||||
'--pipeline', 'gate',
|
||||
'--tenant', 'tenant1',
|
||||
'--changes', '3,3', '4,1', '5,3'])
|
||||
session.post.assert_called_with(
|
||||
'https://fake.zuul/api/tenant/tenant1/promote',
|
||||
json={'change_ids': ['3,3', '4,1', '5,3'],
|
||||
'pipeline': 'gate'}
|
||||
)
|
||||
self.assertEqual(0, exit_code)
|
67
tox.ini
Normal file
67
tox.ini
Normal file
@ -0,0 +1,67 @@
|
||||
[tox]
|
||||
minversion = 3.2
|
||||
skipsdist = True
|
||||
envlist = linters,py3{-docker}
|
||||
ignore_basepython_conflict = True
|
||||
|
||||
[testenv]
|
||||
basepython = python3
|
||||
setenv =
|
||||
VIRTUAL_ENV={envdir}
|
||||
OS_TEST_TIMEOUT=360
|
||||
OS_STDOUT_CAPTURE={env:OS_STDOUT_CAPTURE:1}
|
||||
OS_STDERR_CAPTURE={env:OS_STDERR_CAPTURE:1}
|
||||
OS_LOG_CAPTURE={env:OS_LOG_CAPTURE:1}
|
||||
passenv =
|
||||
OS_LOG_CAPTURE
|
||||
OS_LOG_DEFAULTS
|
||||
OS_STDERR_CAPTURE
|
||||
OS_STDOUT_CAPTURE
|
||||
usedevelop = True
|
||||
whitelist_externals = bash
|
||||
deps =
|
||||
-r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
bash -c 'stestr run --slowest --concurrency=`python -c "import multiprocessing; print(int(multiprocessing.cpu_count()/2))"` {posargs}'
|
||||
|
||||
[testenv:linters]
|
||||
usedevelop = False
|
||||
install_command = pip install {opts} {packages}
|
||||
# --ignore-missing-imports tells mypy to not try to follow imported modules
|
||||
# out of the current tree. As you might expect, we don't want to run static
|
||||
# type checking on the world - just on ourselves.
|
||||
deps =
|
||||
flake8
|
||||
mypy<0.740
|
||||
commands =
|
||||
flake8 {posargs}
|
||||
mypy --ignore-missing-imports zuulclient
|
||||
|
||||
[testenv:cover]
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run --source zuulclient --parallel-mode
|
||||
commands =
|
||||
stestr run {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
|
||||
[testenv:docs]
|
||||
install_command = pip install {opts} {packages}
|
||||
deps =
|
||||
-r{toxinidir}/doc/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands =
|
||||
sphinx-build -E -W -d doc/build/doctrees -b html doc/source/ doc/build/html
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[flake8]
|
||||
# These are ignored intentionally in zuul projects;
|
||||
# please don't submit patches that solely correct them or enable them.
|
||||
ignore = E124,E125,E129,E252,E402,E741,H,W503,W504
|
||||
show-source = True
|
||||
exclude = .venv,.tox,dist,doc,build,*.egg,node_modules
|
154
zuulclient/api/__init__.py
Normal file
154
zuulclient/api/__init__.py
Normal file
@ -0,0 +1,154 @@
|
||||
# Copyright 2020 Red Hat, inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
import requests
|
||||
import urllib.parse
|
||||
|
||||
|
||||
class ZuulRESTException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ZuulRESTClient(object):
|
||||
"""Basic client for Zuul's REST API"""
|
||||
def __init__(self, url, verify=False, auth_token=None):
|
||||
self.url = url
|
||||
if not self.url.endswith('/'):
|
||||
self.url += '/'
|
||||
self.auth_token = auth_token
|
||||
self.verify = verify
|
||||
self.base_url = urllib.parse.urljoin(self.url, 'api/')
|
||||
self.session = requests.Session()
|
||||
self.session.verify = self.verify
|
||||
if self.auth_token:
|
||||
self.session.headers.update(
|
||||
dict(Authorization='Bearer %s' % self.auth_token))
|
||||
|
||||
def _check_request_status(self, req):
|
||||
try:
|
||||
req.raise_for_status()
|
||||
except Exception as e:
|
||||
if req.status_code == 401:
|
||||
raise ZuulRESTException(
|
||||
'Unauthorized - your token might be invalid or expired.')
|
||||
elif req.status_code == 403:
|
||||
raise ZuulRESTException(
|
||||
'Insufficient privileges to perform the action.')
|
||||
else:
|
||||
raise ZuulRESTException(
|
||||
'Unknown error code %s: "%s"' % (req.status_code, e))
|
||||
|
||||
def autohold(self, tenant, project, job, change, ref,
|
||||
reason, count, node_hold_expiration):
|
||||
if not self.auth_token:
|
||||
raise Exception('Auth Token required')
|
||||
args = {"reason": reason,
|
||||
"count": count,
|
||||
"job": job,
|
||||
"change": change,
|
||||
"ref": ref,
|
||||
"node_hold_expiration": node_hold_expiration}
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/project/%s/autohold' % (tenant, project))
|
||||
req = self.session.post(url, json=args)
|
||||
self._check_request_status(req)
|
||||
return req.json()
|
||||
|
||||
def autohold_list(self, tenant):
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/autohold' % tenant)
|
||||
# auth not needed here
|
||||
req = self.session.get(url)
|
||||
self._check_request_status(req)
|
||||
resp = req.json()
|
||||
return resp
|
||||
|
||||
def autohold_delete(self, id, tenant):
|
||||
if not self.auth_token:
|
||||
raise Exception('Auth Token required')
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/autohold/%s' % (tenant, id))
|
||||
req = self.session.delete(url)
|
||||
self._check_request_status(req)
|
||||
# DELETE doesn't return a body, just the HTTP code
|
||||
return (req.status_code == 204)
|
||||
|
||||
def autohold_info(self, id, tenant):
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/autohold/%s' % (tenant, id))
|
||||
# auth not needed here
|
||||
req = self.session.get(url)
|
||||
self._check_request_status(req)
|
||||
resp = req.json()
|
||||
return resp
|
||||
|
||||
def enqueue(self, tenant, pipeline, project, change):
|
||||
if not self.auth_token:
|
||||
raise Exception('Auth Token required')
|
||||
args = {"change": change,
|
||||
"pipeline": pipeline}
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/project/%s/enqueue' % (tenant, project))
|
||||
req = self.session.post(url, json=args)
|
||||
self._check_request_status(req)
|
||||
return req.json()
|
||||
|
||||
def enqueue_ref(self, tenant, pipeline, project, ref, oldrev, newrev):
|
||||
if not self.auth_token:
|
||||
raise Exception('Auth Token required')
|
||||
args = {"ref": ref,
|
||||
"oldrev": oldrev,
|
||||
"newrev": newrev,
|
||||
"pipeline": pipeline}
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/project/%s/enqueue' % (tenant, project))
|
||||
req = self.session.post(url, json=args)
|
||||
self._check_request_status(req)
|
||||
return req.json()
|
||||
|
||||
def dequeue(self, tenant, pipeline, project, change=None, ref=None):
|
||||
if not self.auth_token:
|
||||
raise Exception('Auth Token required')
|
||||
args = {"pipeline": pipeline}
|
||||
if change and not ref:
|
||||
args['change'] = change
|
||||
elif ref and not change:
|
||||
args['ref'] = ref
|
||||
else:
|
||||
raise Exception('need change OR ref')
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/project/%s/dequeue' % (tenant, project))
|
||||
req = self.session.post(url, json=args)
|
||||
self._check_request_status(req)
|
||||
return req.json()
|
||||
|
||||
def promote(self, tenant, pipeline, change_ids):
|
||||
if not self.auth_token:
|
||||
raise Exception('Auth Token required')
|
||||
args = {'pipeline': pipeline,
|
||||
'change_ids': change_ids}
|
||||
url = urllib.parse.urljoin(
|
||||
self.base_url,
|
||||
'tenant/%s/promote' % tenant)
|
||||
req = self.session.post(url, json=args)
|
||||
self._check_request_status(req)
|
||||
return req.json()
|
106
zuulclient/cmd/__init__.py
Normal file
106
zuulclient/cmd/__init__.py
Normal file
@ -0,0 +1,106 @@
|
||||
# Copyright 2020 Red Hat, inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from zuulclient.api import ZuulRESTClient
|
||||
# from zuulclient.api import ZuulRESTException
|
||||
from zuulclient.common.client import CLI
|
||||
from zuulclient.common import get_default
|
||||
|
||||
|
||||
class ZuulClient(CLI):
|
||||
app_name = 'zuul-client'
|
||||
app_description = 'Zuul User CLI'
|
||||
log = logging.getLogger("zuul-client")
|
||||
|
||||
def createParser(self):
|
||||
parser = super(ZuulClient, self).createParser()
|
||||
parser.add_argument('--auth-token', dest='auth_token',
|
||||
required=False,
|
||||
default=None,
|
||||
help='Authentication Token, required by '
|
||||
'admin commands')
|
||||
parser.add_argument('--zuul-url', dest='zuul_url',
|
||||
required=False,
|
||||
default=None,
|
||||
help='Zuul base URL, needed if using the '
|
||||
'client without a configuration file')
|
||||
parser.add_argument('--use-config', dest='zuul_config',
|
||||
required=False,
|
||||
default=None,
|
||||
help='A predefined configuration in .zuul.conf')
|
||||
parser.add_argument('--insecure', dest='verify_ssl',
|
||||
required=False,
|
||||
action='store_false',
|
||||
help='Do not verify SSL connection to Zuul '
|
||||
'(Defaults to False)')
|
||||
return parser
|
||||
|
||||
def createCommandParsers(self, parser):
|
||||
subparsers = super(ZuulClient, self).createCommandParsers(parser)
|
||||
# Add any specific zuul-client command subparser here
|
||||
return subparsers
|
||||
|
||||
def _main(self, args=None):
|
||||
self.parseArguments(args)
|
||||
if not self.args.zuul_url:
|
||||
self.readConfig()
|
||||
self.setup_logging()
|
||||
# TODO make func return specific return codes
|
||||
if self.args.func():
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def get_client(self):
|
||||
if self.args.zuul_url and self.args.zuul_config:
|
||||
raise Exception('Either specify --zuul-url or use a config file')
|
||||
if self.args.zuul_url:
|
||||
self.log.debug(
|
||||
'Using Zuul URL provided as argument to instantiate client')
|
||||
client = ZuulRESTClient(self.args.zuul_url,
|
||||
self.args.verify_ssl,
|
||||
self.args.auth_token)
|
||||
return client
|
||||
conf_sections = self.config.sections()
|
||||
if len(conf_sections) == 1 and self.args.zuul_config is None:
|
||||
zuul_conf = conf_sections[0]
|
||||
self.log.debug(
|
||||
'Using section "%s" found in '
|
||||
'config to instantiate client' % zuul_conf)
|
||||
elif self.args.zuul_config and self.args.zuul_config in conf_sections:
|
||||
zuul_conf = self.args.zuul_config
|
||||
else:
|
||||
raise Exception('Unable to find a way to connect to Zuul, '
|
||||
'provide the "--zuul-url" argument or set up a '
|
||||
'.zuul.conf file.')
|
||||
server = get_default(self.config,
|
||||
zuul_conf, 'url', None)
|
||||
verify = get_default(self.config, zuul_conf,
|
||||
'verify_ssl',
|
||||
self.args.verify_ssl)
|
||||
# Allow token override by CLI argument
|
||||
auth_token = self.args.auth_token or get_default(self.config,
|
||||
zuul_conf,
|
||||
'auth_token',
|
||||
None)
|
||||
if server is None:
|
||||
raise Exception('Missing "url" configuration value')
|
||||
client = ZuulRESTClient(server, verify, auth_token)
|
||||
return client
|
||||
|
||||
|
||||
def main():
|
||||
ZuulClient().main()
|
32
zuulclient/common/__init__.py
Normal file
32
zuulclient/common/__init__.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright 2020 Red Hat, inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def get_default(config, section, option, default=None, expand_user=False):
|
||||
if config.has_option(section, option):
|
||||
# Need to be ensured that we get suitable
|
||||
# type from config file by default value
|
||||
if isinstance(default, bool):
|
||||
value = config.getboolean(section, option)
|
||||
elif isinstance(default, int):
|
||||
value = config.getint(section, option)
|
||||
else:
|
||||
value = config.get(section, option)
|
||||
else:
|
||||
value = default
|
||||
if expand_user and value:
|
||||
return os.path.expanduser(value)
|
||||
return value
|
373
zuulclient/common/client.py
Normal file
373
zuulclient/common/client.py
Normal file
@ -0,0 +1,373 @@
|
||||
# Copyright 2020 Red Hat, inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
# TODO This is taken straight from zuul.cmd - Refactor so the boilerplate Code
|
||||
# lives in one place only.
|
||||
|
||||
import argparse
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
import prettytable
|
||||
import sys
|
||||
import textwrap
|
||||
import time
|
||||
|
||||
|
||||
class App(object):
|
||||
app_name = None # type: str
|
||||
app_description = None # type: str
|
||||
|
||||
def __init__(self):
|
||||
self.args = None
|
||||
self.config = None
|
||||
|
||||
def _get_version(self):
|
||||
from zuulclient.version import version_info
|
||||
return "Zuul-client version: %s" % version_info.release_string()
|
||||
|
||||
def createParser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
description=self.app_description,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument('-c', dest='config',
|
||||
help='specify the config file')
|
||||
parser.add_argument('--version', dest='version', action='version',
|
||||
version=self._get_version(),
|
||||
help='show zuul version')
|
||||
return parser
|
||||
|
||||
def parseArguments(self, args=None):
|
||||
parser = self.createParser()
|
||||
self.args = parser.parse_args(args)
|
||||
return parser
|
||||
|
||||
def readConfig(self):
|
||||
safe_env = {
|
||||
k: v for k, v in os.environ.items()
|
||||
if k.startswith('ZUUL_')
|
||||
}
|
||||
self.config = configparser.ConfigParser(safe_env)
|
||||
if self.args.config:
|
||||
locations = [self.args.config]
|
||||
else:
|
||||
locations = ['~/.zuul.conf']
|
||||
for fp in locations:
|
||||
if os.path.exists(os.path.expanduser(fp)):
|
||||
self.config.read(os.path.expanduser(fp))
|
||||
return
|
||||
raise Exception("Unable to locate config file in %s" % locations)
|
||||
|
||||
|
||||
class CLI(App):
|
||||
"""Common code used by the admin CLI and zuul-client."""
|
||||
|
||||
def createParser(self):
|
||||
parser = super(CLI, self).createParser()
|
||||
parser.add_argument('-v', dest='verbose', action='store_true',
|
||||
help='verbose output')
|
||||
self.createCommandParsers(parser)
|
||||
return parser
|
||||
|
||||
def createCommandParsers(self, parser):
|
||||
subparsers = parser.add_subparsers(title='commands',
|
||||
description='valid commands',
|
||||
help='additional help')
|
||||
# Add parsers that are common to RPC and REST clients
|
||||
self.add_autohold_subparser(subparsers)
|
||||
self.add_autohold_delete_subparser(subparsers)
|
||||
self.add_autohold_info_subparser(subparsers)
|
||||
self.add_autohold_list_subparser(subparsers)
|
||||
self.add_enqueue_subparser(subparsers)
|
||||
self.add_enqueue_ref_subparser(subparsers)
|
||||
self.add_dequeue_subparser(subparsers)
|
||||
self.add_promote_subparser(subparsers)
|
||||
|
||||
return subparsers
|
||||
|
||||
def parseArguments(self, args=None):
|
||||
parser = super(CLI, self).parseArguments(args)
|
||||
if not getattr(self.args, 'func', None):
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
if self.args.func == self.enqueue_ref:
|
||||
# if oldrev or newrev is set, ensure they're not the same
|
||||
if (self.args.oldrev is not None) or \
|
||||
(self.args.newrev is not None):
|
||||
if self.args.oldrev == self.args.newrev:
|
||||
raise Exception(
|
||||
"The old and new revisions must not be the same.")
|
||||
# if they're not set, we pad them out to zero
|
||||
if self.args.oldrev is None:
|
||||
self.args.oldrev = '0000000000000000000000000000000000000000'
|
||||
if self.args.newrev is None:
|
||||
self.args.newrev = '0000000000000000000000000000000000000000'
|
||||
if self.args.func == self.dequeue:
|
||||
if self.args.change is None and self.args.ref is None:
|
||||
raise Exception("Change or ref needed.")
|
||||
if self.args.change is not None and self.args.ref is not None:
|
||||
raise Exception(
|
||||
"The 'change' and 'ref' arguments are mutually exclusive.")
|
||||
|
||||
def setup_logging(self):
|
||||
"""Client logging does not rely on conf file"""
|
||||
if self.args.verbose:
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
def _main(self, args=None):
|
||||
self.parseArguments(args)
|
||||
self.readConfig()
|
||||
self.setup_logging()
|
||||
|
||||
if self.args.func():
|
||||
return 0
|
||||
else:
|
||||
return 1
|
||||
|
||||
def main(self):
|
||||
try:
|
||||
sys.exit(self._main())
|
||||
except Exception as e:
|
||||
self.log.error(e)
|
||||
sys.exit(1)
|
||||
|
||||
def get_client(self):
|
||||
raise NotImplementedError('No client defined')
|
||||
|
||||
def add_autohold_subparser(self, subparsers):
|
||||
cmd_autohold = subparsers.add_parser(
|
||||
'autohold', help='hold nodes for failed job')
|
||||
cmd_autohold.add_argument('--tenant', help='tenant name',
|
||||
required=True)
|
||||
cmd_autohold.add_argument('--project', help='project name',
|
||||
required=True)
|
||||
cmd_autohold.add_argument('--job', help='job name',
|
||||
required=True)
|
||||
cmd_autohold.add_argument('--change',
|
||||
help='specific change to hold nodes for',
|
||||
required=False, default='')
|
||||
cmd_autohold.add_argument('--ref', help='git ref to hold nodes for',
|
||||
required=False, default='')
|
||||
cmd_autohold.add_argument('--reason', help='reason for the hold',
|
||||
required=True)
|
||||
cmd_autohold.add_argument('--count',
|
||||
help='number of job runs (default: 1)',
|
||||
required=False, type=int, default=1)
|
||||
cmd_autohold.add_argument(
|
||||
'--node-hold-expiration',
|
||||
help=('how long in seconds should the node set be in HOLD status '
|
||||
'(default: scheduler\'s default_hold_expiration value)'),
|
||||
required=False, type=int)
|
||||
cmd_autohold.set_defaults(func=self.autohold)
|
||||
|
||||
def autohold(self):
|
||||
if self.args.change and self.args.ref:
|
||||
raise Exception(
|
||||
"Change and ref can't be both used for the same request")
|
||||
if "," in self.args.change:
|
||||
raise Exception("Error: change argument can not contain any ','")
|
||||
|
||||
node_hold_expiration = self.args.node_hold_expiration
|
||||
client = self.get_client()
|
||||
r = client.autohold(
|
||||
tenant=self.args.tenant,
|
||||
project=self.args.project,
|
||||
job=self.args.job,
|
||||
change=self.args.change,
|
||||
ref=self.args.ref,
|
||||
reason=self.args.reason,
|
||||
count=self.args.count,
|
||||
node_hold_expiration=node_hold_expiration)
|
||||
return r
|
||||
|
||||
def add_autohold_delete_subparser(self, subparsers):
|
||||
cmd_autohold_delete = subparsers.add_parser(
|
||||
'autohold-delete', help='delete autohold request')
|
||||
cmd_autohold_delete.set_defaults(func=self.autohold_delete)
|
||||
cmd_autohold_delete.add_argument('--tenant', help='tenant name',
|
||||
required=True, default=None)
|
||||
cmd_autohold_delete.add_argument('id', metavar='REQUEST_ID',
|
||||
help='the hold request ID')
|
||||
|
||||
def autohold_delete(self):
|
||||
client = self.get_client()
|
||||
return client.autohold_delete(self.args.id, self.args.tenant)
|
||||
|
||||
def add_autohold_info_subparser(self, subparsers):
|
||||
cmd_autohold_info = subparsers.add_parser(
|
||||
'autohold-info', help='retrieve autohold request detailed info')
|
||||
cmd_autohold_info.set_defaults(func=self.autohold_info)
|
||||
cmd_autohold_info.add_argument('--tenant', help='tenant name',
|
||||
required=True, default=None)
|
||||
cmd_autohold_info.add_argument('id', metavar='REQUEST_ID',
|
||||
help='the hold request ID')
|
||||
|
||||
def autohold_info(self):
|
||||
client = self.get_client()
|
||||
request = client.autohold_info(self.args.id, self.args.tenant)
|
||||
|
||||
if not request:
|
||||
print("Autohold request not found")
|
||||
return False
|
||||
|
||||
print("ID: %s" % request['id'])
|
||||
print("Tenant: %s" % request['tenant'])
|
||||
print("Project: %s" % request['project'])
|
||||
print("Job: %s" % request['job'])
|
||||
print("Ref Filter: %s" % request['ref_filter'])
|
||||
print("Max Count: %s" % request['max_count'])
|
||||
print("Current Count: %s" % request['current_count'])
|
||||
print("Node Expiration: %s" % request['node_expiration'])
|
||||
print("Request Expiration: %s" % time.ctime(request['expired']))
|
||||
print("Reason: %s" % request['reason'])
|
||||
print("Held Nodes: %s" % request['nodes'])
|
||||
|
||||
return True
|
||||
|
||||
def add_autohold_list_subparser(self, subparsers):
|
||||
cmd_autohold_list = subparsers.add_parser(
|
||||
'autohold-list', help='list autohold requests')
|
||||
cmd_autohold_list.add_argument('--tenant', help='tenant name',
|
||||
required=True)
|
||||
cmd_autohold_list.set_defaults(func=self.autohold_list)
|
||||
|
||||
def autohold_list(self):
|
||||
client = self.get_client()
|
||||
autohold_requests = client.autohold_list(tenant=self.args.tenant)
|
||||
|
||||
if not autohold_requests:
|
||||
print("No autohold requests found")
|
||||
return True
|
||||
|
||||
table = prettytable.PrettyTable(
|
||||
field_names=[
|
||||
'ID', 'Tenant', 'Project', 'Job', 'Ref Filter',
|
||||
'Max Count', 'Reason'
|
||||
])
|
||||
|
||||
for request in autohold_requests:
|
||||
table.add_row([
|
||||
request['id'],
|
||||
request['tenant'],
|
||||
request['project'],
|
||||
request['job'],
|
||||
request['ref_filter'],
|
||||
request['max_count'],
|
||||
request['reason'],
|
||||
])
|
||||
|
||||
print(table)
|
||||
return True
|
||||
|
||||
def add_enqueue_subparser(self, subparsers):
|
||||
cmd_enqueue = subparsers.add_parser('enqueue', help='enqueue a change')
|
||||
cmd_enqueue.add_argument('--tenant', help='tenant name',
|
||||
required=True)
|
||||
cmd_enqueue.add_argument('--pipeline', help='pipeline name',
|
||||
required=True)
|
||||
cmd_enqueue.add_argument('--project', help='project name',
|
||||
required=True)
|
||||
cmd_enqueue.add_argument('--change', help='change id',
|
||||
required=True)
|
||||
cmd_enqueue.set_defaults(func=self.enqueue)
|
||||
|
||||
def enqueue(self):
|
||||
client = self.get_client()
|
||||
r = client.enqueue(
|
||||
tenant=self.args.tenant,
|
||||
pipeline=self.args.pipeline,
|
||||
project=self.args.project,
|
||||
change=self.args.change)
|
||||
return r
|
||||
|
||||
def add_enqueue_ref_subparser(self, subparsers):
|
||||
cmd_enqueue = subparsers.add_parser(
|
||||
'enqueue-ref', help='enqueue a ref',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
description=textwrap.dedent('''\
|
||||
Submit a trigger event
|
||||
|
||||
Directly enqueue a trigger event. This is usually used
|
||||
to manually "replay" a trigger received from an external
|
||||
source such as gerrit.'''))
|
||||
cmd_enqueue.add_argument('--tenant', help='tenant name',
|
||||
required=True)
|
||||
cmd_enqueue.add_argument('--pipeline', help='pipeline name',
|
||||
required=True)
|
||||
cmd_enqueue.add_argument('--project', help='project name',
|
||||
required=True)
|
||||
cmd_enqueue.add_argument('--ref', help='ref name',
|
||||
required=True)
|
||||
cmd_enqueue.add_argument(
|
||||
'--oldrev', help='old revision', default=None)
|
||||
cmd_enqueue.add_argument(
|
||||
'--newrev', help='new revision', default=None)
|
||||
cmd_enqueue.set_defaults(func=self.enqueue_ref)
|
||||
|
||||
def enqueue_ref(self):
|
||||
client = self.get_client()
|
||||
r = client.enqueue_ref(
|
||||
tenant=self.args.tenant,
|
||||
pipeline=self.args.pipeline,
|
||||
project=self.args.project,
|
||||
ref=self.args.ref,
|
||||
oldrev=self.args.oldrev,
|
||||
newrev=self.args.newrev)
|
||||
return r
|
||||
|
||||
def add_dequeue_subparser(self, subparsers):
|
||||
cmd_dequeue = subparsers.add_parser('dequeue',
|
||||
help='dequeue a buildset by its '
|
||||
'change or ref')
|
||||
cmd_dequeue.add_argument('--tenant', help='tenant name',
|
||||
required=True)
|
||||
cmd_dequeue.add_argument('--pipeline', help='pipeline name',
|
||||
required=True)
|
||||
cmd_dequeue.add_argument('--project', help='project name',
|
||||
required=True)
|
||||
cmd_dequeue.add_argument('--change', help='change id',
|
||||
default=None)
|
||||
cmd_dequeue.add_argument('--ref', help='ref name',
|
||||
default=None)
|
||||
cmd_dequeue.set_defaults(func=self.dequeue)
|
||||
|
||||
def dequeue(self):
|
||||
client = self.get_client()
|
||||
r = client.dequeue(
|
||||
tenant=self.args.tenant,
|
||||
pipeline=self.args.pipeline,
|
||||
project=self.args.project,
|
||||
change=self.args.change,
|
||||
ref=self.args.ref)
|
||||
return r
|
||||
|
||||
def add_promote_subparser(self, subparsers):
|
||||
cmd_promote = subparsers.add_parser('promote',
|
||||
help='promote one or more changes')
|
||||
cmd_promote.add_argument('--tenant', help='tenant name',
|
||||
required=True)
|
||||
cmd_promote.add_argument('--pipeline', help='pipeline name',
|
||||
required=True)
|
||||
cmd_promote.add_argument('--changes', help='change ids',
|
||||
required=True, nargs='+')
|
||||
cmd_promote.set_defaults(func=self.promote)
|
||||
|
||||
def promote(self):
|
||||
client = self.get_client()
|
||||
r = client.promote(
|
||||
tenant=self.args.tenant,
|
||||
pipeline=self.args.pipeline,
|
||||
change_ids=self.args.changes)
|
||||
return r
|
32
zuulclient/version.py
Normal file
32
zuulclient/version.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright 2020 Red Hat, inc
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import json
|
||||
|
||||
import pbr.version
|
||||
import pkg_resources
|
||||
|
||||
version_info = pbr.version.VersionInfo('zuul-client')
|
||||
release_string = version_info.release_string()
|
||||
|
||||
is_release = None
|
||||
git_version = None
|
||||
try:
|
||||
_metadata = json.loads(
|
||||
pkg_resources.get_distribution('zuul-client').get_metadata('pbr.json'))
|
||||
if _metadata:
|
||||
is_release = _metadata['is_release']
|
||||
git_version = _metadata['git_version']
|
||||
except Exception:
|
||||
pass
|
Loading…
Reference in New Issue
Block a user