Retire the Tuskar Client codebase
Change-Id: Iee7117358100a1b5cfb365103e6e2ac247ea6178 Depends-On: I904b2f27591333e104bf9080bb8c3876fcb3596c
This commit is contained in:
parent
8f9ccd7594
commit
c14ba08d32
@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
12
.travis.yml
12
.travis.yml
@ -1,12 +0,0 @@
|
||||
language: python
|
||||
env:
|
||||
- TOX_ENV=py27
|
||||
- TOX_ENV=pep8
|
||||
- TOX_ENV=cover
|
||||
before_install:
|
||||
- pip install tox --use-mirrors
|
||||
- if [ "x$TOX_ENV" = 'xcover' ]; then pip install coveralls --use-mirrors; fi
|
||||
script:
|
||||
- tox -e $TOX_ENV
|
||||
after_success:
|
||||
- if [ "x$TOX_ENV" = 'xcover' ]; then coveralls; fi
|
175
LICENSE
175
LICENSE
@ -1,175 +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.
|
@ -1,5 +0,0 @@
|
||||
include AUTHORS
|
||||
include ChangeLog
|
||||
|
||||
exclude .gitignore
|
||||
exclude .gitreview
|
10
README
Normal file
10
README
Normal file
@ -0,0 +1,10 @@
|
||||
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".
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev or #tripleo
|
||||
on Freenode.
|
58
README.rst
58
README.rst
@ -1,58 +0,0 @@
|
||||
===================
|
||||
python-tuskarclient
|
||||
===================
|
||||
|
||||
python-tuskarclient is a Python client and a command-line interface
|
||||
for `Tuskar <https://github.com/openstack/tuskar>`_.
|
||||
|
||||
|
||||
Getting Started
|
||||
===============
|
||||
|
||||
Clone the repo::
|
||||
|
||||
$ git clone https://git.openstack.org/openstack/python-tuskarclient
|
||||
|
||||
Then, use ``tox`` to set up a virtual environment and run tests::
|
||||
|
||||
$ cd python-tuskarclient
|
||||
$ tox
|
||||
|
||||
When this is done, activate your virtual environment::
|
||||
|
||||
$ source .tox/py27/bin/activate
|
||||
|
||||
Finally, use this script to build the wrapper script in your virtual
|
||||
environment for the CLI tools::
|
||||
|
||||
$ python setup.py develop
|
||||
|
||||
|
||||
Use from Python
|
||||
===============
|
||||
|
||||
For using ``python-tuskarclient`` within a Python application, `this
|
||||
wiki page <https://github.com/tuskar/python-tuskarclient/wiki/Usage>`_
|
||||
provides the most complete documentation.
|
||||
|
||||
Use from the CLI
|
||||
================
|
||||
|
||||
On the command line, ``python-tuskarclient`` implements the ``tuskar``
|
||||
command.
|
||||
|
||||
First, be sure to run all of the steps in the Getting Started section,
|
||||
above, and that you have not deactivated your virtual environment.
|
||||
|
||||
Then, export these two environment variables, customizing them if
|
||||
necessary::
|
||||
|
||||
$ export OS_AUTH_TOKEN=nopass
|
||||
$ export TUSKAR_URL=http://localhost:8585/v2
|
||||
|
||||
(Note that 'nopass' is the correct value in a default setup with no
|
||||
authentication.)
|
||||
|
||||
Now you may interact with Tuskar by using the ``tuskar``
|
||||
command. ``tuskar --help`` with list full usage details. You can use
|
||||
``tuskar rack-list`` as an example.
|
2
doc/.gitignore
vendored
2
doc/.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
build/
|
||||
source/ref/
|
@ -1,416 +0,0 @@
|
||||
/**
|
||||
* Sphinx stylesheet -- basic theme
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
|
||||
/* -- main layout ----------------------------------------------------------- */
|
||||
|
||||
div.clearer {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -- relbar ---------------------------------------------------------------- */
|
||||
|
||||
div.related {
|
||||
width: 100%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
div.related h3 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.related ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 10px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.related li {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.related li.right {
|
||||
float: right;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* -- sidebar --------------------------------------------------------------- */
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding: 10px 5px 0 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
float: left;
|
||||
width: 230px;
|
||||
margin-left: -100%;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul,
|
||||
div.sphinxsidebar ul.want-points {
|
||||
margin-left: 20px;
|
||||
list-style: square;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar form {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #98dbcc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* -- search page ----------------------------------------------------------- */
|
||||
|
||||
ul.search {
|
||||
margin: 10px 0 0 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ul.search li {
|
||||
padding: 5px 0 5px 20px;
|
||||
background-image: url(file.png);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 0 7px;
|
||||
}
|
||||
|
||||
ul.search li a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ul.search li div.context {
|
||||
color: #888;
|
||||
margin: 2px 0 0 30px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
ul.keywordmatches li.goodmatch a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* -- index page ------------------------------------------------------------ */
|
||||
|
||||
table.contentstable {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
table.contentstable p.biglink {
|
||||
line-height: 150%;
|
||||
}
|
||||
|
||||
a.biglink {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
span.linkdescr {
|
||||
font-style: italic;
|
||||
padding-top: 5px;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
/* -- general index --------------------------------------------------------- */
|
||||
|
||||
table.indextable td {
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
table.indextable dl, table.indextable dd {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
table.indextable tr.pcap {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
table.indextable tr.cap {
|
||||
margin-top: 10px;
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
|
||||
img.toggler {
|
||||
margin-right: 3px;
|
||||
margin-top: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* -- general body styles --------------------------------------------------- */
|
||||
|
||||
a.headerlink {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
h1:hover > a.headerlink,
|
||||
h2:hover > a.headerlink,
|
||||
h3:hover > a.headerlink,
|
||||
h4:hover > a.headerlink,
|
||||
h5:hover > a.headerlink,
|
||||
h6:hover > a.headerlink,
|
||||
dt:hover > a.headerlink {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
div.body p.caption {
|
||||
text-align: inherit;
|
||||
}
|
||||
|
||||
div.body td {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.field-list ul {
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.first {
|
||||
}
|
||||
|
||||
p.rubric {
|
||||
margin-top: 30px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* -- sidebars -------------------------------------------------------------- */
|
||||
|
||||
div.sidebar {
|
||||
margin: 0 0 0.5em 1em;
|
||||
border: 1px solid #ddb;
|
||||
padding: 7px 7px 0 7px;
|
||||
background-color: #ffe;
|
||||
width: 40%;
|
||||
float: right;
|
||||
}
|
||||
|
||||
p.sidebar-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* -- topics ---------------------------------------------------------------- */
|
||||
|
||||
div.topic {
|
||||
border: 1px solid #ccc;
|
||||
padding: 7px 7px 0 7px;
|
||||
margin: 10px 0 10px 0;
|
||||
}
|
||||
|
||||
p.topic-title {
|
||||
font-size: 1.1em;
|
||||
font-weight: bold;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* -- admonitions ----------------------------------------------------------- */
|
||||
|
||||
div.admonition {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
div.admonition dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.admonition dl {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
margin: 0px 10px 5px 0px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.body p.centered {
|
||||
text-align: center;
|
||||
margin-top: 25px;
|
||||
}
|
||||
|
||||
/* -- tables ---------------------------------------------------------------- */
|
||||
|
||||
table.docutils {
|
||||
border: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.docutils td, table.docutils th {
|
||||
padding: 1px 8px 1px 0;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 1px solid #aaa;
|
||||
}
|
||||
|
||||
table.field-list td, table.field-list th {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
table.footnote td, table.footnote th {
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
/* -- other body styles ----------------------------------------------------- */
|
||||
|
||||
dl {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
dd p {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
dd ul, dd table {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-top: 3px;
|
||||
margin-bottom: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
dt:target, .highlight {
|
||||
background-color: #fbe54e;
|
||||
}
|
||||
|
||||
dl.glossary dt {
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.field-list ul {
|
||||
margin: 0;
|
||||
padding-left: 1em;
|
||||
}
|
||||
|
||||
.field-list p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.refcount {
|
||||
color: #060;
|
||||
}
|
||||
|
||||
.optional {
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.versionmodified {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.system-message {
|
||||
background-color: #fda;
|
||||
padding: 5px;
|
||||
border: 3px solid red;
|
||||
}
|
||||
|
||||
.footnote:target {
|
||||
background-color: #ffa
|
||||
}
|
||||
|
||||
.line-block {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.line-block .line-block {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
margin-left: 1.5em;
|
||||
}
|
||||
|
||||
/* -- code displays --------------------------------------------------------- */
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
td.linenos pre {
|
||||
padding: 5px 0px;
|
||||
border: 0;
|
||||
background-color: transparent;
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
table.highlighttable {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
|
||||
table.highlighttable td {
|
||||
padding: 0 0.5em 0 0.5em;
|
||||
}
|
||||
|
||||
tt.descname {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
tt.descclassname {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
tt.xref, a tt {
|
||||
background-color: transparent;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* -- math display ---------------------------------------------------------- */
|
||||
|
||||
img.math {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
div.body div.math p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
span.eqno {
|
||||
float: right;
|
||||
}
|
||||
|
||||
/* -- printout stylesheet --------------------------------------------------- */
|
||||
|
||||
@media print {
|
||||
div.document,
|
||||
div.documentwrapper,
|
||||
div.bodywrapper {
|
||||
margin: 0 !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.sphinxsidebar,
|
||||
div.related,
|
||||
div.footer,
|
||||
#top-link {
|
||||
display: none;
|
||||
}
|
||||
}
|
@ -1,230 +0,0 @@
|
||||
/**
|
||||
* Sphinx stylesheet -- default theme
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
font-size: 100%;
|
||||
background-color: #11303d;
|
||||
color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: #1c4e63;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 230px;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #000000;
|
||||
padding: 0 20px 30px 20px;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
color: #ffffff;
|
||||
width: 100%;
|
||||
padding: 9px 0 9px 0;
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #ffffff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.related {
|
||||
background-color: #133f52;
|
||||
line-height: 30px;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 {
|
||||
font-family: 'Trebuchet MS', sans-serif;
|
||||
color: #ffffff;
|
||||
font-size: 1.4em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: 'Trebuchet MS', sans-serif;
|
||||
color: #ffffff;
|
||||
font-size: 1.3em;
|
||||
font-weight: normal;
|
||||
margin: 5px 0 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.topless {
|
||||
margin: 5px 10px 10px 10px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px;
|
||||
padding: 0;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #98dbcc;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #98dbcc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #355f7c;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
text-align: left;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: 'Trebuchet MS', sans-serif;
|
||||
background-color: #f2f2f2;
|
||||
font-weight: normal;
|
||||
color: #20435c;
|
||||
border-bottom: 1px solid #ccc;
|
||||
margin: 20px -20px 10px -20px;
|
||||
padding: 3px 0 3px 10px;
|
||||
}
|
||||
|
||||
div.body h1 { margin-top: 0; font-size: 200%; }
|
||||
div.body h2 { font-size: 160%; }
|
||||
div.body h3 { font-size: 140%; }
|
||||
div.body h4 { font-size: 120%; }
|
||||
div.body h5 { font-size: 110%; }
|
||||
div.body h6 { font-size: 100%; }
|
||||
|
||||
a.headerlink {
|
||||
color: #c60f0f;
|
||||
font-size: 0.8em;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
background-color: #c60f0f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
text-align: left;
|
||||
line-height: 130%;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.admonition p {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.admonition pre {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.admonition ul, div.admonition ol {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: #ffe4e4;
|
||||
border: 1px solid #f66;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 5px;
|
||||
background-color: #eeffcc;
|
||||
color: #333333;
|
||||
line-height: 120%;
|
||||
border: 1px solid #ac9;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
padding: 0 1px 0 1px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.warning tt {
|
||||
background: #efc2c2;
|
||||
}
|
||||
|
||||
.note tt {
|
||||
background: #d6d6d6;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 48 B |
Binary file not shown.
Before Width: | Height: | Size: 3.7 KiB |
@ -1,154 +0,0 @@
|
||||
(function($) {
|
||||
|
||||
$.fn.tweet = function(o){
|
||||
var s = {
|
||||
username: ["seaofclouds"], // [string] required, unless you want to display our tweets. :) it can be an array, just do ["username1","username2","etc"]
|
||||
list: null, //[string] optional name of list belonging to username
|
||||
avatar_size: null, // [integer] height and width of avatar if displayed (48px max)
|
||||
count: 3, // [integer] how many tweets to display?
|
||||
intro_text: null, // [string] do you want text BEFORE your your tweets?
|
||||
outro_text: null, // [string] do you want text AFTER your tweets?
|
||||
join_text: null, // [string] optional text in between date and tweet, try setting to "auto"
|
||||
auto_join_text_default: "i said,", // [string] auto text for non verb: "i said" bullocks
|
||||
auto_join_text_ed: "i", // [string] auto text for past tense: "i" surfed
|
||||
auto_join_text_ing: "i am", // [string] auto tense for present tense: "i was" surfing
|
||||
auto_join_text_reply: "i replied to", // [string] auto tense for replies: "i replied to" @someone "with"
|
||||
auto_join_text_url: "i was looking at", // [string] auto tense for urls: "i was looking at" http:...
|
||||
loading_text: null, // [string] optional loading text, displayed while tweets load
|
||||
query: null // [string] optional search query
|
||||
};
|
||||
|
||||
if(o) $.extend(s, o);
|
||||
|
||||
$.fn.extend({
|
||||
linkUrl: function() {
|
||||
var returning = [];
|
||||
var regexp = /((ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?)/gi;
|
||||
this.each(function() {
|
||||
returning.push(this.replace(regexp,"<a href=\"$1\">$1</a>"));
|
||||
});
|
||||
return $(returning);
|
||||
},
|
||||
linkUser: function() {
|
||||
var returning = [];
|
||||
var regexp = /[\@]+([A-Za-z0-9-_]+)/gi;
|
||||
this.each(function() {
|
||||
returning.push(this.replace(regexp,"<a href=\"http://twitter.com/$1\">@$1</a>"));
|
||||
});
|
||||
return $(returning);
|
||||
},
|
||||
linkHash: function() {
|
||||
var returning = [];
|
||||
var regexp = / [\#]+([A-Za-z0-9-_]+)/gi;
|
||||
this.each(function() {
|
||||
returning.push(this.replace(regexp, ' <a href="http://search.twitter.com/search?q=&tag=$1&lang=all&from='+s.username.join("%2BOR%2B")+'">#$1</a>'));
|
||||
});
|
||||
return $(returning);
|
||||
},
|
||||
capAwesome: function() {
|
||||
var returning = [];
|
||||
this.each(function() {
|
||||
returning.push(this.replace(/\b(awesome)\b/gi, '<span class="awesome">$1</span>'));
|
||||
});
|
||||
return $(returning);
|
||||
},
|
||||
capEpic: function() {
|
||||
var returning = [];
|
||||
this.each(function() {
|
||||
returning.push(this.replace(/\b(epic)\b/gi, '<span class="epic">$1</span>'));
|
||||
});
|
||||
return $(returning);
|
||||
},
|
||||
makeHeart: function() {
|
||||
var returning = [];
|
||||
this.each(function() {
|
||||
returning.push(this.replace(/(<)+[3]/gi, "<tt class='heart'>♥</tt>"));
|
||||
});
|
||||
return $(returning);
|
||||
}
|
||||
});
|
||||
|
||||
function relative_time(time_value) {
|
||||
var parsed_date = Date.parse(time_value);
|
||||
var relative_to = (arguments.length > 1) ? arguments[1] : new Date();
|
||||
var delta = parseInt((relative_to.getTime() - parsed_date) / 1000);
|
||||
var pluralize = function (singular, n) {
|
||||
return '' + n + ' ' + singular + (n == 1 ? '' : 's');
|
||||
};
|
||||
if(delta < 60) {
|
||||
return 'less than a minute ago';
|
||||
} else if(delta < (45*60)) {
|
||||
return 'about ' + pluralize("minute", parseInt(delta / 60)) + ' ago';
|
||||
} else if(delta < (24*60*60)) {
|
||||
return 'about ' + pluralize("hour", parseInt(delta / 3600)) + ' ago';
|
||||
} else {
|
||||
return 'about ' + pluralize("day", parseInt(delta / 86400)) + ' ago';
|
||||
}
|
||||
}
|
||||
|
||||
function build_url() {
|
||||
var proto = ('https:' == document.location.protocol ? 'https:' : 'http:');
|
||||
if (s.list) {
|
||||
return proto+"//api.twitter.com/1/"+s.username[0]+"/lists/"+s.list+"/statuses.json?per_page="+s.count+"&callback=?";
|
||||
} else if (s.query == null && s.username.length == 1) {
|
||||
return proto+'//twitter.com/status/user_timeline/'+s.username[0]+'.json?count='+s.count+'&callback=?';
|
||||
} else {
|
||||
var query = (s.query || 'from:'+s.username.join('%20OR%20from:'));
|
||||
return proto+'//search.twitter.com/search.json?&q='+query+'&rpp='+s.count+'&callback=?';
|
||||
}
|
||||
}
|
||||
|
||||
return this.each(function(){
|
||||
var list = $('<ul class="tweet_list">').appendTo(this);
|
||||
var intro = '<p class="tweet_intro">'+s.intro_text+'</p>';
|
||||
var outro = '<p class="tweet_outro">'+s.outro_text+'</p>';
|
||||
var loading = $('<p class="loading">'+s.loading_text+'</p>');
|
||||
|
||||
if(typeof(s.username) == "string"){
|
||||
s.username = [s.username];
|
||||
}
|
||||
|
||||
if (s.loading_text) $(this).append(loading);
|
||||
$.getJSON(build_url(), function(data){
|
||||
if (s.loading_text) loading.remove();
|
||||
if (s.intro_text) list.before(intro);
|
||||
$.each((data.results || data), function(i,item){
|
||||
// auto join text based on verb tense and content
|
||||
if (s.join_text == "auto") {
|
||||
if (item.text.match(/^(@([A-Za-z0-9-_]+)) .*/i)) {
|
||||
var join_text = s.auto_join_text_reply;
|
||||
} else if (item.text.match(/(^\w+:\/\/[A-Za-z0-9-_]+\.[A-Za-z0-9-_:%&\?\/.=]+) .*/i)) {
|
||||
var join_text = s.auto_join_text_url;
|
||||
} else if (item.text.match(/^((\w+ed)|just) .*/im)) {
|
||||
var join_text = s.auto_join_text_ed;
|
||||
} else if (item.text.match(/^(\w*ing) .*/i)) {
|
||||
var join_text = s.auto_join_text_ing;
|
||||
} else {
|
||||
var join_text = s.auto_join_text_default;
|
||||
}
|
||||
} else {
|
||||
var join_text = s.join_text;
|
||||
};
|
||||
|
||||
var from_user = item.from_user || item.user.screen_name;
|
||||
var profile_image_url = item.profile_image_url || item.user.profile_image_url;
|
||||
var join_template = '<span class="tweet_join"> '+join_text+' </span>';
|
||||
var join = ((s.join_text) ? join_template : ' ');
|
||||
var avatar_template = '<a class="tweet_avatar" href="http://twitter.com/'+from_user+'"><img src="'+profile_image_url+'" height="'+s.avatar_size+'" width="'+s.avatar_size+'" alt="'+from_user+'\'s avatar" title="'+from_user+'\'s avatar" border="0"/></a>';
|
||||
var avatar = (s.avatar_size ? avatar_template : '');
|
||||
var date = '<a href="http://twitter.com/'+from_user+'/statuses/'+item.id+'" title="view tweet on twitter">'+relative_time(item.created_at)+'</a>';
|
||||
var text = '<span class="tweet_text">' +$([item.text]).linkUrl().linkUser().linkHash().makeHeart().capAwesome().capEpic()[0]+ '</span>';
|
||||
|
||||
// until we create a template option, arrange the items below to alter a tweet's display.
|
||||
list.append('<li>' + avatar + date + join + text + '</li>');
|
||||
|
||||
list.children('li:first').addClass('tweet_first');
|
||||
list.children('li:odd').addClass('tweet_even');
|
||||
list.children('li:even').addClass('tweet_odd');
|
||||
});
|
||||
if (s.outro_text) list.after(outro);
|
||||
});
|
||||
|
||||
});
|
||||
};
|
||||
})(jQuery);
|
@ -1,245 +0,0 @@
|
||||
/*
|
||||
* nature.css_t
|
||||
* ~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- nature theme.
|
||||
*
|
||||
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 100%;
|
||||
background-color: #111;
|
||||
color: #555;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 {{ theme_sidebarwidth|toint }}px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 30px 30px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
color: #555;
|
||||
width: 100%;
|
||||
padding: 13px 0;
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #444;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.related {
|
||||
background-color: #6BA81E;
|
||||
line-height: 32px;
|
||||
color: #fff;
|
||||
text-shadow: 0px 1px 0 #444;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: #E2F3CC;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
font-size: 0.75em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper{
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #222;
|
||||
font-size: 1.2em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
background-color: #ddd;
|
||||
text-shadow: 1px 1px 0 white
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4{
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #888;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.topless {
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 20px;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #ccc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input[type=text]{
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #005B81;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #E32E00;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #BED4EB;
|
||||
font-weight: normal;
|
||||
color: #212224;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 5px 0 5px 10px;
|
||||
text-shadow: 0px 1px 0 white
|
||||
}
|
||||
|
||||
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
|
||||
div.body h2 { font-size: 150%; background-color: #C8D5E3; }
|
||||
div.body h3 { font-size: 120%; background-color: #D8DEE3; }
|
||||
div.body h4 { font-size: 110%; background-color: #D8DEE3; }
|
||||
div.body h5 { font-size: 100%; background-color: #D8DEE3; }
|
||||
div.body h6 { font-size: 100%; background-color: #D8DEE3; }
|
||||
|
||||
a.headerlink {
|
||||
color: #c60f0f;
|
||||
font-size: 0.8em;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
background-color: #c60f0f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.highlight{
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: #ffe4e4;
|
||||
border: 1px solid #f66;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
background-color: White;
|
||||
color: #222;
|
||||
line-height: 1.2em;
|
||||
border: 1px solid #C6C9CB;
|
||||
font-size: 1.1em;
|
||||
margin: 1.5em 0 1.5em 0;
|
||||
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
|
||||
-moz-box-shadow: 1px 1px 1px #d8d8d8;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
/* padding: 1px 2px; */
|
||||
font-size: 1.1em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background-color: #f4debf;
|
||||
border-top: 1px solid #ac9;
|
||||
border-bottom: 1px solid #ac9;
|
||||
}
|
Binary file not shown.
Before Width: | Height: | Size: 3.6 KiB |
@ -1,62 +0,0 @@
|
||||
.highlight .hll { background-color: #ffffcc }
|
||||
.highlight { background: #eeffcc; }
|
||||
.highlight .c { color: #408090; font-style: italic } /* Comment */
|
||||
.highlight .err { border: 1px solid #FF0000 } /* Error */
|
||||
.highlight .k { color: #007020; font-weight: bold } /* Keyword */
|
||||
.highlight .o { color: #666666 } /* Operator */
|
||||
.highlight .cm { color: #408090; font-style: italic } /* Comment.Multiline */
|
||||
.highlight .cp { color: #007020 } /* Comment.Preproc */
|
||||
.highlight .c1 { color: #408090; font-style: italic } /* Comment.Single */
|
||||
.highlight .cs { color: #408090; background-color: #fff0f0 } /* Comment.Special */
|
||||
.highlight .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.highlight .ge { font-style: italic } /* Generic.Emph */
|
||||
.highlight .gr { color: #FF0000 } /* Generic.Error */
|
||||
.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.highlight .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.highlight .go { color: #333333 } /* Generic.Output */
|
||||
.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
|
||||
.highlight .gs { font-weight: bold } /* Generic.Strong */
|
||||
.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.highlight .gt { color: #0044DD } /* Generic.Traceback */
|
||||
.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */
|
||||
.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */
|
||||
.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */
|
||||
.highlight .kp { color: #007020 } /* Keyword.Pseudo */
|
||||
.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */
|
||||
.highlight .kt { color: #902000 } /* Keyword.Type */
|
||||
.highlight .m { color: #208050 } /* Literal.Number */
|
||||
.highlight .s { color: #4070a0 } /* Literal.String */
|
||||
.highlight .na { color: #4070a0 } /* Name.Attribute */
|
||||
.highlight .nb { color: #007020 } /* Name.Builtin */
|
||||
.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */
|
||||
.highlight .no { color: #60add5 } /* Name.Constant */
|
||||
.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */
|
||||
.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */
|
||||
.highlight .ne { color: #007020 } /* Name.Exception */
|
||||
.highlight .nf { color: #06287e } /* Name.Function */
|
||||
.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */
|
||||
.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
|
||||
.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */
|
||||
.highlight .nv { color: #bb60d5 } /* Name.Variable */
|
||||
.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */
|
||||
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
|
||||
.highlight .mf { color: #208050 } /* Literal.Number.Float */
|
||||
.highlight .mh { color: #208050 } /* Literal.Number.Hex */
|
||||
.highlight .mi { color: #208050 } /* Literal.Number.Integer */
|
||||
.highlight .mo { color: #208050 } /* Literal.Number.Oct */
|
||||
.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */
|
||||
.highlight .sc { color: #4070a0 } /* Literal.String.Char */
|
||||
.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */
|
||||
.highlight .s2 { color: #4070a0 } /* Literal.String.Double */
|
||||
.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */
|
||||
.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */
|
||||
.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */
|
||||
.highlight .sx { color: #c65d09 } /* Literal.String.Other */
|
||||
.highlight .sr { color: #235388 } /* Literal.String.Regex */
|
||||
.highlight .s1 { color: #4070a0 } /* Literal.String.Single */
|
||||
.highlight .ss { color: #517918 } /* Literal.String.Symbol */
|
||||
.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */
|
||||
.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */
|
||||
.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */
|
||||
.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */
|
||||
.highlight .il { color: #208050 } /* Literal.Number.Integer.Long */
|
@ -1,94 +0,0 @@
|
||||
body {
|
||||
background: #fff url(../_static/header_bg.jpg) top left no-repeat;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 950px;
|
||||
margin: 0 auto;
|
||||
height: 102px;
|
||||
}
|
||||
|
||||
#header h1#logo {
|
||||
background: url(../_static/openstack_logo.png) top left no-repeat;
|
||||
display: block;
|
||||
float: left;
|
||||
text-indent: -9999px;
|
||||
width: 175px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
background: url(../_static/header-line.gif) repeat-x 0 bottom;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 27px 0 0 25px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#navigation li{
|
||||
float: left;
|
||||
display: block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
#navigation li a {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
background-position: 50% 0;
|
||||
padding: 20px 0 5px;
|
||||
color: #353535;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#navigation li a.current, #navigation li a.section {
|
||||
border-bottom: 3px solid #cf2f19;
|
||||
color: #cf2f19;
|
||||
}
|
||||
|
||||
div.related {
|
||||
background-color: #cde2f8;
|
||||
border: 1px solid #b0d3f8;
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: #4078ba;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
div.documentwrapper h1, div.documentwrapper h2, div.documentwrapper h3, div.documentwrapper h4, div.documentwrapper h5, div.documentwrapper h6 {
|
||||
font-family: 'PT Sans', sans-serif !important;
|
||||
color: #264D69;
|
||||
border-bottom: 1px dotted #C5E2EA;
|
||||
padding: 0;
|
||||
background: none;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div.documentwrapper h3 {
|
||||
color: #CF2F19;
|
||||
}
|
||||
|
||||
a.headerlink {
|
||||
color: #fff !important;
|
||||
margin-left: 5px;
|
||||
background: #CF2F19 !important;
|
||||
}
|
||||
|
||||
div.body {
|
||||
margin-top: -25px;
|
||||
margin-left: 230px;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
{% extends "basic/layout.html" %}
|
||||
{% set css_files = css_files + ['_static/tweaks.css'] %}
|
||||
{% set script_files = script_files + ['_static/jquery.tweet.js'] %}
|
||||
|
||||
{%- macro sidebar() %}
|
||||
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
|
||||
<div class="sphinxsidebar">
|
||||
<div class="sphinxsidebarwrapper">
|
||||
{%- block sidebarlogo %}
|
||||
{%- if logo %}
|
||||
<p class="logo"><a href="{{ pathto(master_doc) }}">
|
||||
<img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/>
|
||||
</a></p>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- block sidebartoc %}
|
||||
{%- if display_toc %}
|
||||
<h3><a href="{{ pathto(master_doc) }}">{{ _('Table Of Contents') }}</a></h3>
|
||||
{{ toc }}
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- block sidebarrel %}
|
||||
{%- if prev %}
|
||||
<h4>{{ _('Previous topic') }}</h4>
|
||||
<p class="topless"><a href="{{ prev.link|e }}"
|
||||
title="{{ _('previous chapter') }}">{{ prev.title }}</a></p>
|
||||
{%- endif %}
|
||||
{%- if next %}
|
||||
<h4>{{ _('Next topic') }}</h4>
|
||||
<p class="topless"><a href="{{ next.link|e }}"
|
||||
title="{{ _('next chapter') }}">{{ next.title }}</a></p>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- block sidebarsourcelink %}
|
||||
{%- if show_source and has_source and sourcename %}
|
||||
<h3>{{ _('This Page') }}</h3>
|
||||
<ul class="this-page-menu">
|
||||
<li><a href="{{ pathto('_sources/' + sourcename, true)|e }}"
|
||||
rel="nofollow">{{ _('Show Source') }}</a></li>
|
||||
</ul>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
{%- if customsidebar %}
|
||||
{% include customsidebar %}
|
||||
{%- endif %}
|
||||
{%- block sidebarsearch %}
|
||||
{%- if pagename != "search" %}
|
||||
<div id="searchbox" style="display: none">
|
||||
<h3>{{ _('Quick search') }}</h3>
|
||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||
<input type="text" name="q" size="18" />
|
||||
<input type="submit" value="{{ _('Go') }}" />
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
<p class="searchtip" style="font-size: 90%">
|
||||
{{ _('Enter search terms or a module, class or function name.') }}
|
||||
</p>
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
{%- endif %}
|
||||
{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% block relbar1 %}{% endblock relbar1 %}
|
||||
|
||||
{% block header %}
|
||||
<div id="header">
|
||||
<h1 id="logo"><a href="http://www.openstack.org/">OpenStack</a></h1>
|
||||
<ul id="navigation">
|
||||
<li><a href="http://www.openstack.org/" title="Go to the Home page" class="link">Home</a></li>
|
||||
<li><a href="http://www.openstack.org/projects/" title="Go to the OpenStack Projects page">Projects</a></li>
|
||||
<li><a href="http://www.openstack.org/user-stories/" title="Go to the User Stories page" class="link">User Stories</a></li>
|
||||
<li><a href="http://www.openstack.org/community/" title="Go to the Community page" class="link">Community</a></li>
|
||||
<li><a href="http://www.openstack.org/blog/" title="Go to the OpenStack Blog">Blog</a></li>
|
||||
<li><a href="http://wiki.openstack.org/" title="Go to the OpenStack Wiki">Wiki</a></li>
|
||||
<li><a href="http://docs.openstack.org/" title="Go to OpenStack Documentation" class="current">Documentation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,4 +0,0 @@
|
||||
[theme]
|
||||
inherit = basic
|
||||
stylesheet = nature.css
|
||||
pygments_style = tango
|
@ -1,8 +0,0 @@
|
||||
CLI Usage with version 2 API
|
||||
===========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
plans
|
||||
roles
|
@ -1,229 +0,0 @@
|
||||
Plans commands with version 2 API
|
||||
=================================
|
||||
|
||||
List All Plans
|
||||
--------------
|
||||
*tuskar plan-list [-h]*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-list
|
||||
|
||||
This will show table of all Plans.
|
||||
|
||||
Example:
|
||||
|
||||
::
|
||||
|
||||
+--------------------------------------+-------------+---------------------------+---------------------+
|
||||
| uuid | name | description | roles |
|
||||
+--------------------------------------+-------------+---------------------------+---------------------+
|
||||
| 53268a27-afc8-4b21-839f-90227dd7a001 | dev-cloud-3 | Development testing cloud | controller, compute |
|
||||
+--------------------------------------+-------------+---------------------------+---------------------+
|
||||
| a117fa66-1445-44c7-8ad1-7663d2607aca | test1 | None | |
|
||||
+--------------------------------------+-------------+---------------------------+---------------------+
|
||||
| c367b394-7179-4c44-85ed-bf84baaf9fee | dev-cloud-2 | Development testing cloud | |
|
||||
+--------------------------------------+-------------+---------------------------+---------------------+
|
||||
|
||||
Field 'roles' contains list of names of Roles assigned to the Plan.
|
||||
|
||||
Retrieve a Single Plan
|
||||
----------------------
|
||||
*tuskar plan-show [-h] [--verbose] [--only-empty-parameters] <PLAN>*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-show c367b394-7179-4c44-85ed-bf84baaf9fee
|
||||
|
||||
This command will show an overview of the Plan.
|
||||
|
||||
Example:
|
||||
|
||||
::
|
||||
|
||||
+-------------+------------------------------------------------------------------------------------------+
|
||||
| Property | Value |
|
||||
+-------------+------------------------------------------------------------------------------------------+
|
||||
| created_at | 2014-09-26T13:36:28.804272 |
|
||||
| description | Development testing cloud |
|
||||
| name | dev-cloud-2 |
|
||||
| parameters | ... |
|
||||
| roles | description=OpenStack hypervisor node. Can be wrapped in a ResourceGroup for scaling. |
|
||||
| | name=compute |
|
||||
| | uuid=b7b1583c-5c80-481f-a25b-708ed4a39734 |
|
||||
| | version=1 |
|
||||
| | |
|
||||
| | description=OpenStack control plane node. Can be wrapped in a ResourceGroup for scaling. |
|
||||
| | name=controller |
|
||||
| | uuid=df9edfac-e009-4df1-ac7f-8931d37f4be6 |
|
||||
| | version=1 |
|
||||
| updated_at | None |
|
||||
| uuid | c367b394-7179-4c44-85ed-bf84baaf9fee |
|
||||
+-------------+------------------------------------------------------------------------------------------+
|
||||
|
||||
Adding the --verbose flag will display all parameters, instead of just role counts.
|
||||
|
||||
Adding the --only-empty-parameters flag will display only parameters, which have empty or None value. When all parameters have some value, no parameters will be displayed.
|
||||
|
||||
Note: Parameters are displayed similarly as Roles, ie. set of properties with values. Each Parameter/Role separated by empty line from previous.
|
||||
|
||||
Create a New Plan
|
||||
-----------------
|
||||
*tuskar plan-create [-h] [-d <DESCRIPTION>] name*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-create -d 'Description of new plan' new-plan-name
|
||||
|
||||
Output will be the same as for showing detail of a Plan.
|
||||
Note that parameters and roles are not set for newly created Plan.
|
||||
|
||||
::
|
||||
|
||||
+-------------+--------------------------------------+
|
||||
| Property | Value |
|
||||
+-------------+--------------------------------------+
|
||||
| created_at | 2014-09-27T00:10:33.958239 |
|
||||
| description | Description of new plan |
|
||||
| name | new-plan-name |
|
||||
| parameters | |
|
||||
| roles | |
|
||||
| updated_at | None |
|
||||
| uuid | 839fcbbf-7aa0-4801-8ccb-d020da654dd6 |
|
||||
+-------------+--------------------------------------+
|
||||
|
||||
Delete an Existing Plan
|
||||
-----------------------
|
||||
*tuskar plan-delete [-h] <PLAN>*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-delete 839fcbbf-7aa0-4801-8ccb-d020da654dd6
|
||||
|
||||
When successfully deleted, you will get message like this:
|
||||
|
||||
::
|
||||
|
||||
Deleted Plan "new-plan-name".
|
||||
|
||||
Adding a Role to a Plan
|
||||
-----------------------
|
||||
*tuskar plan-add-role [-h] -r <ROLE UUID> plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-add-role -r df9edfac-e009-4df1-ac7f-8931d37f4be6 c367b394-7179-4c44-85ed-bf84baaf9fee
|
||||
|
||||
This will assign Role specified by UUID to Plan.
|
||||
Output of this command is the same as for plan-show.
|
||||
|
||||
Removing a Role from a Plan
|
||||
---------------------------
|
||||
*tuskar plan-remove-role [-h] -r <ROLE UUID> plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-remove-role -r df9edfac-e009-4df1-ac7f-8931d37f4be6 c367b394-7179-4c44-85ed-bf84baaf9fee
|
||||
|
||||
This will unassign Role from a Plan. This will not delete the Role from Tuskar.
|
||||
Output of this command is the same as for plan-show.
|
||||
|
||||
Show Plan’s scale
|
||||
-----------------
|
||||
*tuskar plan-show-scale plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-show-scale c367b394-7179-4c44-85ed-bf84baaf9fee
|
||||
|
||||
Output of this command is a table containing role names with versions and their counts.
|
||||
|
||||
Scaling a Plan
|
||||
--------------
|
||||
*tuskar plan-scale <ROLE NAME WITH VERSION> --count=<COUNT> plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-scale compute-1 --count=2 c367b394-7179-4c44-85ed-bf84baaf9fee
|
||||
|
||||
This will scale given Plan’s role with specified count of nodes.
|
||||
Output of this command is a short summary of changed values.
|
||||
|
||||
Show Plan’s Flavors assigned to Roles
|
||||
-------------------------------------
|
||||
*tuskar plan-show-flavors plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-show-flavors c367b394-7179-4c44-85ed-bf84baaf9fee
|
||||
|
||||
Output of this command is a table containing roles and assigned flavors.
|
||||
|
||||
Assign Flavors to Roles in a Plan
|
||||
---------------------------------
|
||||
*tuskar plan-flavor <ROLE NAME WITH VERSION> --flavor=<FLAVOR> plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-flavor compute-1 --flavor=baremetal c367b394-7179-4c44-85ed-bf84baaf9fee
|
||||
|
||||
This will update role-flavor assignment in a Plan.
|
||||
Output of this command is a short summary of changed values.
|
||||
|
||||
Changing a Plan’s Configuration Values
|
||||
--------------------------------------
|
||||
*tuskar plan-update [-h] [-P <KEY1=VALUE1>] plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-update -P compute-1::CeilometerPassword=secret-password -P compute-1::CeilometerMeteringSecret=secret-secret 53268a27-afc8-4b21-839f-90227dd7a001
|
||||
|
||||
This command accepts multiple name=value pairs for parameters to be updated.
|
||||
Above example will look for parameter named 'compute-1::CeilometerPassword' and update its value to 'secret-password'
|
||||
and will do similar update for 'compute-1::CeilometerMeteringSecret' parameter.
|
||||
|
||||
This command can be used only for updating existing parameters. It is not possible to create new parameter this way.
|
||||
|
||||
Retrieve a Plan’s Template Files
|
||||
--------------------------------
|
||||
*tuskar plan-templates [-h] -O <OUTPUT DIR> plan_uuid*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar plan-templates -O templates 53268a27-afc8-4b21-839f-90227dd7a001
|
||||
|
||||
This command will retrieve contents of templates of the Plan and save them as files into specified directory.
|
||||
-O/--output-dir is mandatory and application will create it if it does not exist.
|
||||
Output is list of files with templates.
|
||||
|
||||
::
|
||||
|
||||
Following templates has been written:
|
||||
templates/plan.yaml
|
||||
templates/environment.yaml
|
||||
templates/provider-controller-1.yaml
|
||||
templates/provider-compute-1.yaml
|
@ -1,27 +0,0 @@
|
||||
Roles commands with version 2 API
|
||||
=================================
|
||||
|
||||
|
||||
Retrieving Possible Roles
|
||||
-------------------------
|
||||
|
||||
*tuskar role-list [-h]*
|
||||
|
||||
Usage example:
|
||||
|
||||
::
|
||||
|
||||
tuskar role-list
|
||||
|
||||
This will show table of all Roles:
|
||||
|
||||
Example:
|
||||
|
||||
::
|
||||
|
||||
+--------------------------------------+------------+---------+------------------------------------------------------------------------------+
|
||||
| uuid | name | version | description |
|
||||
+--------------------------------------+------------+---------+------------------------------------------------------------------------------+
|
||||
| b7b1583c-5c80-481f-a25b-708ed4a39734 | compute | 1 | OpenStack hypervisor node. Can be wrapped in a ResourceGroup for scaling. |
|
||||
| df9edfac-e009-4df1-ac7f-8931d37f4be6 | controller | 1 | OpenStack control plane node. Can be wrapped in a ResourceGroup for scaling. |
|
||||
+--------------------------------------+------------+---------+------------------------------------------------------------------------------+
|
@ -1,264 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# 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.
|
||||
#
|
||||
# python-tuskarclient documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Dec 6 14:19:25 2009.
|
||||
#
|
||||
# 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
|
||||
|
||||
# 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.append(os.path.abspath('.'))
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
|
||||
|
||||
sys.path.insert(0, ROOT)
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
|
||||
|
||||
def gen_ref(ver, title, names):
|
||||
refdir = os.path.join(BASE_DIR, "ref")
|
||||
pkg = "tuskarclient"
|
||||
if ver:
|
||||
pkg = "%s.%s" % (pkg, ver)
|
||||
refdir = os.path.join(refdir, ver)
|
||||
if not os.path.exists(refdir):
|
||||
os.makedirs(refdir)
|
||||
idxpath = os.path.join(refdir, "index.rst")
|
||||
with open(idxpath, "w") as idx:
|
||||
idx.write(("%(title)s\n"
|
||||
"%(signs)s\n"
|
||||
"\n"
|
||||
".. toctree::\n"
|
||||
" :maxdepth: 1\n"
|
||||
"\n") % {"title": title, "signs": "=" * len(title)})
|
||||
for name in names:
|
||||
idx.write(" %s\n" % name)
|
||||
rstpath = os.path.join(refdir, "%s.rst" % name)
|
||||
with open(rstpath, "w") as rst:
|
||||
rst.write(("%(title)s\n"
|
||||
"%(signs)s\n"
|
||||
"\n"
|
||||
".. automodule:: %(pkg)s.%(name)s\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n"
|
||||
" :noindex:\n")
|
||||
% {"title": name.capitalize(),
|
||||
"signs": "=" * len(name),
|
||||
"pkg": pkg, "name": name})
|
||||
|
||||
gen_ref("", "Client Reference", ["client", "exc"])
|
||||
|
||||
|
||||
# -- 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']
|
||||
|
||||
# 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'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = 'python-tuskarclient'
|
||||
copyright = u'OpenStack Foundation'
|
||||
|
||||
# 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 documents that shouldn't be included in the build.
|
||||
#unused_docs = []
|
||||
|
||||
# List of directories, relative to source directory, that shouldn't be searched
|
||||
# for source files.
|
||||
exclude_trees = []
|
||||
|
||||
# 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 = []
|
||||
|
||||
# Grouping the document tree for man pages.
|
||||
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
|
||||
|
||||
man_pages = [
|
||||
('man/tuskar', 'tuskar', 'OpenStack Tuskar command line client',
|
||||
['OpenStack Contributors'], 1),
|
||||
]
|
||||
|
||||
# -- Options for HTML output --------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
||||
html_theme = '_theme'
|
||||
|
||||
# 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']
|
||||
|
||||
# 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'
|
||||
|
||||
# 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_use_modindex = 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, 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 = ''
|
||||
|
||||
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = ''
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = '%sdoc' % project
|
||||
|
||||
|
||||
# -- Options for LaTeX output -------------------------------------------------
|
||||
|
||||
# The paper size ('letter' or 'a4').
|
||||
#latex_paper_size = 'letter'
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#latex_font_size = '10pt'
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual])
|
||||
# .
|
||||
latex_documents = [
|
||||
(
|
||||
'index',
|
||||
'%s.tex' % project,
|
||||
'%s Documentation' % project,
|
||||
'OpenStack Foundation',
|
||||
'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
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#latex_preamble = ''
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_use_modindex = True
|
@ -1,51 +0,0 @@
|
||||
Python bindings to the OpenStack Tuskar API
|
||||
===========================================
|
||||
|
||||
This is a client for OpenStack Tuskar API. There's a Python API
|
||||
(the :mod:`tuskarclient` module), and a command-line script
|
||||
(installed as :program:`tuskar`).
|
||||
|
||||
Python API
|
||||
==========
|
||||
|
||||
You can use the API like so::
|
||||
|
||||
>>> from tuskarclient.client import Client
|
||||
>>> tuskar = Client('1', endpoint=tuskar_url)
|
||||
|
||||
Reference
|
||||
---------
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
ref/index
|
||||
ref/v1/index
|
||||
ref/v2/index
|
||||
|
||||
Command-line Tool
|
||||
=================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
cli/v2/index
|
||||
|
||||
Man Pages
|
||||
=========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
man/tuskar
|
||||
|
||||
Contributing
|
||||
============
|
||||
|
||||
Code is hosted `on OpenStack git`_. Submit bugs to the Tuskar project on
|
||||
`Launchpad`_. Submit code to the openstack/python-tuskarclient project
|
||||
using `Gerrit`_.
|
||||
|
||||
.. _on OpenStack git: http://git.openstack.org/cgit/openstack/python-tuskarclient
|
||||
.. _Launchpad: https://launchpad.net/python-tuskarclient
|
||||
.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
@ -1,56 +0,0 @@
|
||||
======
|
||||
Tuskar
|
||||
======
|
||||
|
||||
.. program:: tuskar
|
||||
|
||||
SYNOPSIS
|
||||
========
|
||||
|
||||
`tuskar` [options] <command> [command-options]
|
||||
|
||||
`tuskar help`
|
||||
|
||||
`tuskar help` <command>
|
||||
|
||||
|
||||
DESCRIPTION
|
||||
===========
|
||||
|
||||
`tuskar` is a command line client for controlling OpenStack Tuskar.
|
||||
|
||||
|
||||
|
||||
OPTIONS
|
||||
=======
|
||||
|
||||
To get a list of available commands and options run::
|
||||
|
||||
tuskar help
|
||||
|
||||
To get usage and options of a command run::
|
||||
|
||||
tuskar help <command>
|
||||
|
||||
|
||||
EXAMPLES
|
||||
========
|
||||
|
||||
Get information about overcloud-create command::
|
||||
|
||||
tuskar help overcloud-create
|
||||
|
||||
List available overcloud-list::
|
||||
|
||||
tuskar overcloud-list
|
||||
|
||||
List available Overcloud Roles::
|
||||
|
||||
tuskar overcloud-roles-list
|
||||
|
||||
|
||||
BUGS
|
||||
====
|
||||
|
||||
Tuskar client is hosted in Launchpad so you can view current bugs at
|
||||
https://bugs.launchpad.net/python-tuskarclient/.
|
@ -1,8 +0,0 @@
|
||||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from openstack-common
|
||||
module=apiclient
|
||||
module=cliutils
|
||||
|
||||
# The base module to hold the copy of openstack.common
|
||||
base=tuskarclient
|
@ -1,14 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
pbr>=1.6
|
||||
|
||||
Babel>=1.3
|
||||
cliff>=1.14.0 # Apache-2.0
|
||||
iso8601>=0.1.9
|
||||
PrettyTable<0.8,>=0.7
|
||||
python-keystoneclient>=1.6.0
|
||||
requests>=2.5.2
|
||||
python-openstackclient>=1.5.0
|
||||
simplejson>=2.2.0
|
||||
six>=1.9.0
|
51
setup.cfg
51
setup.cfg
@ -1,51 +0,0 @@
|
||||
[metadata]
|
||||
name = python-tuskarclient
|
||||
summary = Client library for OpenStack Management API
|
||||
description-file =
|
||||
README.rst
|
||||
license = Apache License, Version 2.0
|
||||
author = Jiri Stransky
|
||||
author-email = jistr@redhat.com
|
||||
home-page = https://github.com/openstack/python-tuskarclient
|
||||
classifier =
|
||||
Environment :: Console
|
||||
Environment :: OpenStack
|
||||
Intended Audience :: Developers
|
||||
Intended Audience :: Information Technology
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.3
|
||||
|
||||
[files]
|
||||
packages =
|
||||
tuskarclient
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
tuskar = tuskarclient.shell:main
|
||||
|
||||
openstack.cli.extension =
|
||||
management = tuskarclient.osc.plugin
|
||||
|
||||
openstack.management.v2 =
|
||||
management_plan_create = tuskarclient.osc.v2.plan:CreateManagementPlan
|
||||
management_plan_delete = tuskarclient.osc.v2.plan:DeleteManagementPlan
|
||||
management_plan_list = tuskarclient.osc.v2.plan:ListManagementPlans
|
||||
management_plan_set = tuskarclient.osc.v2.plan:SetManagementPlan
|
||||
management_plan_show = tuskarclient.osc.v2.plan:ShowManagementPlan
|
||||
management_plan_add_role = tuskarclient.osc.v2.plan:AddManagementPlanRole
|
||||
management_plan_remove_role = tuskarclient.osc.v2.plan:RemoveManagementPlanRole
|
||||
management_plan_download = tuskarclient.osc.v2.plan:DownloadManagementPlan
|
||||
management_role_list = tuskarclient.osc.v2.role:ListRoles
|
||||
|
||||
[build_sphinx]
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
all_files = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
29
setup.py
29
setup.py
@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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 FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
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>=1.8'],
|
||||
pbr=True)
|
@ -1,14 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
# Hacking already pins down pep8, pyflakes and flake8
|
||||
hacking<0.11,>=0.10.2
|
||||
|
||||
coverage>=3.6
|
||||
discover
|
||||
fixtures>=1.3.1
|
||||
mock>=1.2
|
||||
sphinx!=1.2.0,!=1.3b1,<1.3,>=1.1.2
|
||||
sphinxcontrib-pecanwsme>=0.8
|
||||
testrepository>=0.0.18
|
||||
testtools>=1.4.0
|
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
TOOLS=`dirname $0`
|
||||
VENV=$TOOLS/../.venv
|
||||
source $VENV/bin/activate && python -m tuskarclient.shell $@
|
39
tox.ini
39
tox.ini
@ -1,39 +0,0 @@
|
||||
[tox]
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
envlist = py27,py33,py34,pep8,cover
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
install_command = pip install {opts} {packages}
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = python setup.py testr --testr-args='{posargs}'
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands = coverage erase
|
||||
python setup.py testr --coverage --testr-args='{posargs}'
|
||||
coverage report -m --omit='tuskarclient/openstack/*'
|
||||
|
||||
[tox:jenkins]
|
||||
downloadcache = ~/cache/pip
|
||||
|
||||
# H405 multi line docstring summary not separated with an empty line
|
||||
[flake8]
|
||||
ignore = H405
|
||||
show-source = True
|
||||
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build
|
||||
|
||||
[hacking]
|
||||
import_exceptions =
|
||||
gettext.gettext,
|
||||
six.StringIO,
|
||||
tuskarclient.openstack.common.gettextutils._
|
@ -1,16 +0,0 @@
|
||||
# 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 pbr.version
|
||||
|
||||
|
||||
__version__ = pbr.version.VersionInfo('python-tuskarclient').version_string()
|
@ -1,63 +0,0 @@
|
||||
# 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 tuskarclient.common import auth
|
||||
from tuskarclient.openstack.common.apiclient import client as apiclient
|
||||
|
||||
VERSION_MAP = {
|
||||
'2': 'tuskarclient.v2.client.Client'
|
||||
}
|
||||
|
||||
|
||||
def get_client(api_version, **kwargs):
|
||||
"""Get an authtenticated client, based on the credentials
|
||||
in the keyword args.
|
||||
|
||||
:param api_version: the API version to use (only '2' is valid)
|
||||
:param kwargs: keyword args containing credentials, either:
|
||||
* os_auth_token: pre-existing token to re-use
|
||||
* tuskar_url: tuskar API endpoint
|
||||
or:
|
||||
* os_username: name of user
|
||||
* os_password: user's password
|
||||
* os_auth_url: endpoint to authenticate against
|
||||
* os_tenant_{name|id}: name or ID of tenant
|
||||
"""
|
||||
# Try call for client with token and endpoint.
|
||||
# If it returns None, call for client with credentials
|
||||
cli_kwargs = {
|
||||
'username': kwargs.get('os_username'),
|
||||
'password': kwargs.get('os_password'),
|
||||
'tenant_name': kwargs.get('os_tenant_name'),
|
||||
'token': kwargs.get('os_auth_token'),
|
||||
'auth_url': kwargs.get('os_auth_url'),
|
||||
'endpoint': kwargs.get('tuskar_url'),
|
||||
'cacert': kwargs.get('os_cacert'),
|
||||
'cert': kwargs.get('os_cert'),
|
||||
'key': kwargs.get('os_key'),
|
||||
}
|
||||
client = Client(api_version, **cli_kwargs)
|
||||
# If we have a client, return it
|
||||
if client:
|
||||
return client
|
||||
# otherwise raise error
|
||||
else:
|
||||
raise ValueError("Need correct set of parameters")
|
||||
|
||||
|
||||
def Client(version, **kwargs):
|
||||
client_class = apiclient.BaseClient.get_class('tuskarclient',
|
||||
version,
|
||||
VERSION_MAP)
|
||||
keystone_auth = auth.KeystoneAuthPlugin(**kwargs)
|
||||
http_client = apiclient.HTTPClient(keystone_auth)
|
||||
return client_class(http_client)
|
@ -1,82 +0,0 @@
|
||||
# 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 keystoneclient.v2_0 import client as ksclient
|
||||
|
||||
from tuskarclient.openstack.common.apiclient import auth
|
||||
from tuskarclient.openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
class KeystoneAuthPlugin(auth.BaseAuthPlugin):
|
||||
opt_names = [
|
||||
"username",
|
||||
"password",
|
||||
"tenant_id",
|
||||
"tenant_name",
|
||||
"token",
|
||||
"auth_url",
|
||||
"endpoint",
|
||||
"cacert",
|
||||
"cert",
|
||||
"key",
|
||||
]
|
||||
|
||||
def _do_authenticate(self, httpclient):
|
||||
if self.opts.get('token') is None:
|
||||
ks_kwargs = {
|
||||
'username': self.opts.get('username'),
|
||||
'password': self.opts.get('password'),
|
||||
'tenant_id': self.opts.get('tenant_id'),
|
||||
'tenant_name': self.opts.get('tenant_name'),
|
||||
'auth_url': self.opts.get('auth_url'),
|
||||
'cacert': self.opts.get('cacert'),
|
||||
'cert': self.opts.get('cert'),
|
||||
'key': self.opts.get('key'),
|
||||
}
|
||||
|
||||
self._ksclient = ksclient.Client(**ks_kwargs)
|
||||
|
||||
def token_and_endpoint(self, endpoint_type, service_type):
|
||||
token = endpoint = None
|
||||
|
||||
if self.opts.get('token') and self.opts.get('endpoint'):
|
||||
token = self.opts.get('token')
|
||||
endpoint = self.opts.get('endpoint')
|
||||
elif hasattr(self, '_ksclient'):
|
||||
token = self._ksclient.auth_token
|
||||
endpoint = (self.opts.get('endpoint') or
|
||||
self._ksclient.service_catalog.url_for(
|
||||
service_type=service_type or 'management',
|
||||
endpoint_type=endpoint_type))
|
||||
|
||||
return (token, endpoint)
|
||||
|
||||
def sufficient_options(self):
|
||||
"""Check if all required options are present.
|
||||
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
if self.opts.get('token'):
|
||||
lookup_table = ["token", "endpoint"]
|
||||
else:
|
||||
lookup_table = [
|
||||
"username",
|
||||
"password",
|
||||
"auth_url"
|
||||
]
|
||||
tenant_opts = ["tenant_id", "tenant_name"]
|
||||
if not any([self.opts.get(opt) for opt in tenant_opts]):
|
||||
raise exceptions.AuthPluginOptionsMissing(
|
||||
' or '.join(tenant_opts))
|
||||
|
||||
missing = [opt for opt in lookup_table if not self.opts.get(opt)]
|
||||
if missing:
|
||||
raise exceptions.AuthPluginOptionsMissing(missing)
|
@ -1,132 +0,0 @@
|
||||
# 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 __future__ import print_function
|
||||
|
||||
import pprint
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
import prettytable
|
||||
import six
|
||||
|
||||
|
||||
def value_formatter(value, width=70):
|
||||
# Most values can be pretty printed for a reasonable output
|
||||
if not isinstance(value, six.string_types):
|
||||
return pprint.pformat(value, width=width)
|
||||
|
||||
# pprint doesn't touch strings, so we do them manually
|
||||
|
||||
# First join lines that are not indented:
|
||||
joined = []
|
||||
parts = []
|
||||
for l in value.splitlines():
|
||||
if l[0] in u' \t\r\n':
|
||||
# break here
|
||||
joined.append(' '.join(parts))
|
||||
parts = []
|
||||
parts.append(l.rstrip())
|
||||
if parts:
|
||||
joined.append(' '.join(parts))
|
||||
|
||||
result = []
|
||||
for line in joined:
|
||||
result.append(textwrap.fill(line, width=width))
|
||||
return u"\n".join(result)
|
||||
|
||||
|
||||
def attributes_formatter(attributes):
|
||||
"""Given a simple dict format the keyvalue pairs with one on each line.
|
||||
"""
|
||||
return u"".join(u"{0}={1}\n".format(k, value_formatter(v)) for k, v in
|
||||
sorted(attributes.items()))
|
||||
|
||||
|
||||
def parameters_v2_formatter(parameters):
|
||||
"""Given a list of dicts format parameters output."""
|
||||
return u"\n".join(attributes_formatter(parameter)
|
||||
for parameter in parameters)
|
||||
|
||||
|
||||
def list_plan_roles_formatter(roles):
|
||||
"""Given a list of Roles format roles' names into row."""
|
||||
return u", ".join(role.name for role in roles)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters={}, custom_labels={}, sortby=0,
|
||||
outfile=sys.stdout):
|
||||
'''Prints a list of objects.
|
||||
|
||||
:param objs: list of objects to print
|
||||
:param fields: list of attributes of the objects to print;
|
||||
attributes beginning with '!' have a special meaning - they
|
||||
should be used with custom field labels and formatters only,
|
||||
and the formatter receives the whole object
|
||||
:param formatters: dict of functions that perform pre-print
|
||||
formatting of attributes (keys are strings from `fields`
|
||||
parameter, values are functions that take one parameter - the
|
||||
attribute)
|
||||
:param custom_labels: dict of label overrides for fields (keys are
|
||||
strings from `fields` parameter, values are custom labels -
|
||||
headers of the table)
|
||||
'''
|
||||
field_labels = [custom_labels.get(f, f) for f in fields]
|
||||
pt = prettytable.PrettyTable([f for f in field_labels],
|
||||
caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field[0] == '!': # custom field
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
raise KeyError(
|
||||
'Custom field "%s" needs a formatter.' % field)
|
||||
else: # attribute-based field
|
||||
if hasattr(o, field) and field in formatters:
|
||||
row.append(formatters[field](getattr(o, field)))
|
||||
else:
|
||||
row.append(getattr(o, field, ''))
|
||||
pt.add_row(row)
|
||||
print(pt.get_string(sortby=field_labels[sortby]), file=outfile)
|
||||
|
||||
|
||||
def print_dict(d, formatters={}, custom_labels={}, outfile=sys.stdout):
|
||||
'''Prints a dict to the provided file or file-like object.
|
||||
|
||||
:param d: dict to print
|
||||
:param formatters: dict of functions that perform pre-print
|
||||
formatting of dict values (keys are keys from `d` parameter,
|
||||
values are functions that take one parameter - the dict value
|
||||
to format). A wild card formatter can be provided as '*' which
|
||||
will be applied to all fields without a dedicated formatter.
|
||||
:param custom_labels: dict of label overrides for keys (keys are
|
||||
keys from `d` parameter, values are custom labels)
|
||||
'''
|
||||
pt = prettytable.PrettyTable(['Property', 'Value'],
|
||||
caching=False, print_empty=False)
|
||||
pt.align = 'l'
|
||||
|
||||
global_formatter = formatters.get('*')
|
||||
|
||||
for field in d.keys():
|
||||
label = custom_labels.get(field, field)
|
||||
if field in formatters:
|
||||
pt.add_row([label, formatters[field](d[field])])
|
||||
elif global_formatter:
|
||||
pt.add_row([label, global_formatter(d[field])])
|
||||
else:
|
||||
pt.add_row([label, d[field]])
|
||||
print(pt.get_string(sortby='Property'), file=outfile)
|
@ -1,168 +0,0 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import sys
|
||||
import uuid
|
||||
|
||||
from oslo_utils import importutils
|
||||
|
||||
from tuskarclient.i18n import _
|
||||
from tuskarclient.openstack.common.apiclient import exceptions as exc
|
||||
from tuskarclient.openstack.common import cliutils
|
||||
|
||||
# Using common methods from oslo cliutils
|
||||
arg = cliutils.arg
|
||||
env = cliutils.env
|
||||
|
||||
|
||||
def define_commands_from_module(subparsers, command_module):
|
||||
"""Find all methods beginning with 'do_' in a module, and add them
|
||||
as commands into a subparsers collection.
|
||||
"""
|
||||
for method_name in (a for a in dir(command_module) if a.startswith('do_')):
|
||||
# Commands should be hypen-separated instead of underscores.
|
||||
command = method_name[3:].replace('_', '-')
|
||||
callback = getattr(command_module, method_name)
|
||||
define_command(subparsers, command, callback)
|
||||
|
||||
|
||||
def define_command(subparsers, command, callback):
|
||||
"""Define a command in the subparsers collection.
|
||||
|
||||
:param subparsers: subparsers collection where the command will go
|
||||
:param command: command name
|
||||
:param callback: function that will be used to process the command
|
||||
"""
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(command, help=help, description=desc)
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||
return manager.get(int(name_or_id))
|
||||
except exc.NotFound:
|
||||
pass
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
uuid.UUID(str(name_or_id))
|
||||
return manager.get(name_or_id)
|
||||
except (ValueError, exc.NotFound):
|
||||
pass
|
||||
|
||||
# finally try to find the entity by name
|
||||
resource = getattr(manager, 'resource_class', None)
|
||||
attr = resource.NAME_ATTR if resource else 'name'
|
||||
|
||||
listing = manager.list()
|
||||
matches = [obj for obj in listing if getattr(obj, attr) == name_or_id]
|
||||
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = "No %s with name '%s' exists." % (
|
||||
manager.resource_class.__name__.lower(), name_or_id)
|
||||
raise exc.CommandError(msg)
|
||||
elif num_matches > 1:
|
||||
msg = "Multiple instances of %s with name '%s' exist." % (
|
||||
manager.resource_class.__name__.lower(), name_or_id)
|
||||
raise exc.CommandError(msg)
|
||||
|
||||
return matches[0]
|
||||
|
||||
|
||||
def import_versioned_module(version, submodule=None):
|
||||
module = 'tuskarclient.v%s' % version
|
||||
if submodule:
|
||||
module = '.'.join((module, submodule))
|
||||
return importutils.import_module(module)
|
||||
|
||||
|
||||
def format_key_value(params):
|
||||
"""Parse a list of k=v strings into an iterator of (k,v).
|
||||
|
||||
:raises: CommandError
|
||||
"""
|
||||
if not params:
|
||||
raise StopIteration
|
||||
|
||||
for param in params:
|
||||
try:
|
||||
(name, value) = param.split(('='), 1)
|
||||
except ValueError:
|
||||
msg = _('Malformed parameter({0}). Use the key=value format.')
|
||||
raise exc.CommandError(msg.format(param))
|
||||
yield name, value
|
||||
|
||||
|
||||
def format_key_value_args(params):
|
||||
"""Reformat CLI attributes into the structure expected by the API.
|
||||
|
||||
The format expected by the API for attributes is a dictionary consisting
|
||||
of only string keys and values.
|
||||
|
||||
:raises: ValidationError
|
||||
"""
|
||||
attributes = {}
|
||||
|
||||
for key, value in format_key_value(params):
|
||||
|
||||
if key in attributes:
|
||||
raise exc.ValidationError(
|
||||
_("The attribute name {0} can't be given twice.").format(key))
|
||||
|
||||
attributes[key] = value
|
||||
|
||||
return attributes
|
||||
|
||||
|
||||
def parameters_args_to_patch(parameters):
|
||||
"""Create a list of patch dicts to update the parameters in the API."""
|
||||
|
||||
return [{'name': pair[0], 'value': pair[1]}
|
||||
for pair in sorted(format_key_value_args(parameters).items())]
|
||||
|
||||
|
||||
def args_to_patch(flavors, roles, parameter):
|
||||
"""Create a list of dicts to update the given parameter in the API."""
|
||||
|
||||
role_flavors_dict = format_key_value_args(flavors)
|
||||
|
||||
roles = dict(("{0}-{1}".format(r.name, r.version), r) for r in roles)
|
||||
patch = []
|
||||
|
||||
for role_name, flavor in sorted(role_flavors_dict.items()):
|
||||
|
||||
if role_name in roles:
|
||||
patch.append({
|
||||
'name': "{0}::{1}".format(role_name, parameter),
|
||||
'value': flavor
|
||||
})
|
||||
else:
|
||||
print("ERROR: no roles were found in the Plan with the name {0}".
|
||||
format(role_name), file=sys.stderr)
|
||||
continue
|
||||
|
||||
return patch
|
@ -1,35 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""oslo_i18n integration module for tuskarclient.
|
||||
|
||||
See http://docs.openstack.org/developer/oslo.i18n/usage.html .
|
||||
|
||||
"""
|
||||
|
||||
import oslo_i18n
|
||||
|
||||
|
||||
_translators = oslo_i18n.TranslatorFactory(domain='tuskarclient')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||
# the level.
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
@ -1,45 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""oslo.i18n integration module.
|
||||
|
||||
See http://docs.openstack.org/developer/oslo.i18n/usage.html
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
import oslo_i18n
|
||||
|
||||
# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
|
||||
# application name when this module is synced into the separate
|
||||
# repository. It is OK to have more than one translation function
|
||||
# using the same domain, since there will still only be one message
|
||||
# catalog.
|
||||
_translators = oslo_i18n.TranslatorFactory(domain='tuskarclient')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||
# the level.
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
||||
except ImportError:
|
||||
# NOTE(dims): Support for cases where a project wants to use
|
||||
# code from oslo-incubator, but is not ready to be internationalized
|
||||
# (like tempest)
|
||||
_ = _LI = _LW = _LE = _LC = lambda x: x
|
@ -1,234 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 Spanish National Research Council.
|
||||
# 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.
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-tuskarclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import abc
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import six
|
||||
from stevedore import extension
|
||||
|
||||
from tuskarclient.openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
_discovered_plugins = {}
|
||||
|
||||
|
||||
def discover_auth_systems():
|
||||
"""Discover the available auth-systems.
|
||||
|
||||
This won't take into account the old style auth-systems.
|
||||
"""
|
||||
global _discovered_plugins
|
||||
_discovered_plugins = {}
|
||||
|
||||
def add_plugin(ext):
|
||||
_discovered_plugins[ext.name] = ext.plugin
|
||||
|
||||
ep_namespace = "tuskarclient.openstack.common.apiclient.auth"
|
||||
mgr = extension.ExtensionManager(ep_namespace)
|
||||
mgr.map(add_plugin)
|
||||
|
||||
|
||||
def load_auth_system_opts(parser):
|
||||
"""Load options needed by the available auth-systems into a parser.
|
||||
|
||||
This function will try to populate the parser with options from the
|
||||
available plugins.
|
||||
"""
|
||||
group = parser.add_argument_group("Common auth options")
|
||||
BaseAuthPlugin.add_common_opts(group)
|
||||
for name, auth_plugin in six.iteritems(_discovered_plugins):
|
||||
group = parser.add_argument_group(
|
||||
"Auth-system '%s' options" % name,
|
||||
conflict_handler="resolve")
|
||||
auth_plugin.add_opts(group)
|
||||
|
||||
|
||||
def load_plugin(auth_system):
|
||||
try:
|
||||
plugin_class = _discovered_plugins[auth_system]
|
||||
except KeyError:
|
||||
raise exceptions.AuthSystemNotFound(auth_system)
|
||||
return plugin_class(auth_system=auth_system)
|
||||
|
||||
|
||||
def load_plugin_from_args(args):
|
||||
"""Load required plugin and populate it with options.
|
||||
|
||||
Try to guess auth system if it is not specified. Systems are tried in
|
||||
alphabetical order.
|
||||
|
||||
:type args: argparse.Namespace
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
auth_system = args.os_auth_system
|
||||
if auth_system:
|
||||
plugin = load_plugin(auth_system)
|
||||
plugin.parse_opts(args)
|
||||
plugin.sufficient_options()
|
||||
return plugin
|
||||
|
||||
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
|
||||
plugin_class = _discovered_plugins[plugin_auth_system]
|
||||
plugin = plugin_class()
|
||||
plugin.parse_opts(args)
|
||||
try:
|
||||
plugin.sufficient_options()
|
||||
except exceptions.AuthPluginOptionsMissing:
|
||||
continue
|
||||
return plugin
|
||||
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseAuthPlugin(object):
|
||||
"""Base class for authentication plugins.
|
||||
|
||||
An authentication plugin needs to override at least the authenticate
|
||||
method to be a valid plugin.
|
||||
"""
|
||||
|
||||
auth_system = None
|
||||
opt_names = []
|
||||
common_opt_names = [
|
||||
"auth_system",
|
||||
"username",
|
||||
"password",
|
||||
"tenant_name",
|
||||
"token",
|
||||
"auth_url",
|
||||
]
|
||||
|
||||
def __init__(self, auth_system=None, **kwargs):
|
||||
self.auth_system = auth_system or self.auth_system
|
||||
self.opts = dict((name, kwargs.get(name))
|
||||
for name in self.opt_names)
|
||||
|
||||
@staticmethod
|
||||
def _parser_add_opt(parser, opt):
|
||||
"""Add an option to parser in two variants.
|
||||
|
||||
:param opt: option name (with underscores)
|
||||
"""
|
||||
dashed_opt = opt.replace("_", "-")
|
||||
env_var = "OS_%s" % opt.upper()
|
||||
arg_default = os.environ.get(env_var, "")
|
||||
arg_help = "Defaults to env[%s]." % env_var
|
||||
parser.add_argument(
|
||||
"--os-%s" % dashed_opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
default=arg_default,
|
||||
help=arg_help)
|
||||
parser.add_argument(
|
||||
"--os_%s" % opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
@classmethod
|
||||
def add_opts(cls, parser):
|
||||
"""Populate the parser with the options for this plugin.
|
||||
"""
|
||||
for opt in cls.opt_names:
|
||||
# use `BaseAuthPlugin.common_opt_names` since it is never
|
||||
# changed in child classes
|
||||
if opt not in BaseAuthPlugin.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@classmethod
|
||||
def add_common_opts(cls, parser):
|
||||
"""Add options that are common for several plugins.
|
||||
"""
|
||||
for opt in cls.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@staticmethod
|
||||
def get_opt(opt_name, args):
|
||||
"""Return option name and value.
|
||||
|
||||
:param opt_name: name of the option, e.g., "username"
|
||||
:param args: parsed arguments
|
||||
"""
|
||||
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
||||
|
||||
def parse_opts(self, args):
|
||||
"""Parse the actual auth-system options if any.
|
||||
|
||||
This method is expected to populate the attribute `self.opts` with a
|
||||
dict containing the options and values needed to make authentication.
|
||||
"""
|
||||
self.opts.update(dict(self.get_opt(opt_name, args)
|
||||
for opt_name in self.opt_names))
|
||||
|
||||
def authenticate(self, http_client):
|
||||
"""Authenticate using plugin defined method.
|
||||
|
||||
The method usually analyses `self.opts` and performs
|
||||
a request to authentication server.
|
||||
|
||||
:param http_client: client object that needs authentication
|
||||
:type http_client: HTTPClient
|
||||
:raises: AuthorizationFailure
|
||||
"""
|
||||
self.sufficient_options()
|
||||
self._do_authenticate(http_client)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _do_authenticate(self, http_client):
|
||||
"""Protected method for authentication.
|
||||
"""
|
||||
|
||||
def sufficient_options(self):
|
||||
"""Check if all required options are present.
|
||||
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
missing = [opt
|
||||
for opt in self.opt_names
|
||||
if not self.opts.get(opt)]
|
||||
if missing:
|
||||
raise exceptions.AuthPluginOptionsMissing(missing)
|
||||
|
||||
@abc.abstractmethod
|
||||
def token_and_endpoint(self, endpoint_type, service_type):
|
||||
"""Return token and endpoint.
|
||||
|
||||
:param service_type: Service type of the endpoint
|
||||
:type service_type: string
|
||||
:param endpoint_type: Type of endpoint.
|
||||
Possible values: public or publicURL,
|
||||
internal or internalURL,
|
||||
admin or adminURL
|
||||
:type endpoint_type: string
|
||||
:returns: tuple of token and endpoint strings
|
||||
:raises: EndpointException
|
||||
"""
|
@ -1,539 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2012 Grid Dynamics
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-tuskarclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
|
||||
# E1102: %s is not callable
|
||||
# pylint: disable=E1102
|
||||
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from tuskarclient.openstack.common._i18n import _
|
||||
from tuskarclient.openstack.common.apiclient import exceptions
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Return id if argument is a Resource.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
||||
class HookableMixin(object):
|
||||
"""Mixin so classes can register and run hooks."""
|
||||
_hooks_map = {}
|
||||
|
||||
@classmethod
|
||||
def add_hook(cls, hook_type, hook_func):
|
||||
"""Add a new hook of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param hook_func: hook function
|
||||
"""
|
||||
if hook_type not in cls._hooks_map:
|
||||
cls._hooks_map[hook_type] = []
|
||||
|
||||
cls._hooks_map[hook_type].append(hook_func)
|
||||
|
||||
@classmethod
|
||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
||||
"""Run all hooks of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param args: args to be passed to every hook function
|
||||
:param kwargs: kwargs to be passed to every hook function
|
||||
"""
|
||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||
for hook_func in hook_funcs:
|
||||
hook_func(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseManager(HookableMixin):
|
||||
"""Basic manager type providing common operations.
|
||||
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initializes BaseManager with `client`.
|
||||
|
||||
:param client: instance of BaseClient descendant for HTTP requests
|
||||
"""
|
||||
super(BaseManager, self).__init__()
|
||||
self.client = client
|
||||
|
||||
def _list(self, url, response_key=None, obj_class=None, json=None):
|
||||
"""List the collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
:param obj_class: class for constructing the returned objects
|
||||
(self.resource_class will be used by default)
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
"""
|
||||
if json:
|
||||
body = self.client.post(url, json=json).json()
|
||||
else:
|
||||
body = self.client.get(url).json()
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key] if response_key is not None else body
|
||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||
# unlike other services which just return the list...
|
||||
try:
|
||||
data = data['values']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||
|
||||
def _get(self, url, response_key=None):
|
||||
"""Get an object from collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
body = self.client.get(url).json()
|
||||
data = body[response_key] if response_key is not None else body
|
||||
return self.resource_class(self, data, loaded=True)
|
||||
|
||||
def _head(self, url):
|
||||
"""Retrieve request headers for an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
"""
|
||||
resp = self.client.head(url)
|
||||
return resp.status_code == 204
|
||||
|
||||
def _post(self, url, json, response_key=None, return_raw=False):
|
||||
"""Create an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'. If response_key is None - all response body
|
||||
will be used.
|
||||
:param return_raw: flag to force returning raw JSON instead of
|
||||
Python object of self.resource_class
|
||||
"""
|
||||
body = self.client.post(url, json=json).json()
|
||||
data = body[response_key] if response_key is not None else body
|
||||
if return_raw:
|
||||
return data
|
||||
return self.resource_class(self, data)
|
||||
|
||||
def _put(self, url, json=None, response_key=None):
|
||||
"""Update an object with PUT method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
resp = self.client.put(url, json=json)
|
||||
# PUT requests may not return a body
|
||||
if resp.content:
|
||||
body = resp.json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _patch(self, url, json=None, response_key=None):
|
||||
"""Update an object with PATCH method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'. If response_key is None - all response body
|
||||
will be used.
|
||||
"""
|
||||
body = self.client.patch(url, json=json).json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _delete(self, url, response_key=None):
|
||||
"""Delete an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers/my-server'
|
||||
"""
|
||||
resp = self.client.delete(url)
|
||||
# DELETE requests may not return a body
|
||||
if resp.content:
|
||||
body = resp.json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ManagerWithFind(BaseManager):
|
||||
"""Manager with additional `find()`/`findall()` methods."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def find(self, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
matches = self.findall(**kwargs)
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = _("No %(name)s matching %(args)s.") % {
|
||||
'name': self.resource_class.__name__,
|
||||
'args': kwargs
|
||||
}
|
||||
raise exceptions.NotFound(msg)
|
||||
elif num_matches > 1:
|
||||
raise exceptions.NoUniqueMatch()
|
||||
else:
|
||||
return matches[0]
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class CrudManager(BaseManager):
|
||||
"""Base manager class for manipulating entities.
|
||||
|
||||
Children of this class are expected to define a `collection_key` and `key`.
|
||||
|
||||
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
||||
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
||||
objects containing a list of member resources (e.g. `{'entities': [{},
|
||||
{}, {}]}`).
|
||||
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
||||
refer to an individual member of the collection.
|
||||
|
||||
"""
|
||||
collection_key = None
|
||||
key = None
|
||||
|
||||
def build_url(self, base_url=None, **kwargs):
|
||||
"""Builds a resource URL for the given kwargs.
|
||||
|
||||
Given an example collection where `collection_key = 'entities'` and
|
||||
`key = 'entity'`, the following URL's could be generated.
|
||||
|
||||
By default, the URL will represent a collection of entities, e.g.::
|
||||
|
||||
/entities
|
||||
|
||||
If kwargs contains an `entity_id`, then the URL will represent a
|
||||
specific member, e.g.::
|
||||
|
||||
/entities/{entity_id}
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
url = base_url if base_url is not None else ''
|
||||
|
||||
url += '/%s' % self.collection_key
|
||||
|
||||
# do we have a specific entity?
|
||||
entity_id = kwargs.get('%s_id' % self.key)
|
||||
if entity_id is not None:
|
||||
url += '/%s' % entity_id
|
||||
|
||||
return url
|
||||
|
||||
def _filter_kwargs(self, kwargs):
|
||||
"""Drop null values and handle ids."""
|
||||
for key, ref in six.iteritems(kwargs.copy()):
|
||||
if ref is None:
|
||||
kwargs.pop(key)
|
||||
else:
|
||||
if isinstance(ref, Resource):
|
||||
kwargs.pop(key)
|
||||
kwargs['%s_id' % key] = getid(ref)
|
||||
return kwargs
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._post(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: kwargs},
|
||||
self.key)
|
||||
|
||||
def get(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._get(
|
||||
self.build_url(**kwargs),
|
||||
self.key)
|
||||
|
||||
def head(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._head(self.build_url(**kwargs))
|
||||
|
||||
def list(self, base_url=None, **kwargs):
|
||||
"""List the collection.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
|
||||
def put(self, base_url=None, **kwargs):
|
||||
"""Update an element.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._put(self.build_url(base_url=base_url, **kwargs))
|
||||
|
||||
def update(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
params = kwargs.copy()
|
||||
params.pop('%s_id' % self.key)
|
||||
|
||||
return self._patch(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: params},
|
||||
self.key)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._delete(
|
||||
self.build_url(**kwargs))
|
||||
|
||||
def find(self, base_url=None, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
rl = self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
num = len(rl)
|
||||
|
||||
if num == 0:
|
||||
msg = _("No %(name)s matching %(args)s.") % {
|
||||
'name': self.resource_class.__name__,
|
||||
'args': kwargs
|
||||
}
|
||||
raise exceptions.NotFound(404, msg)
|
||||
elif num > 1:
|
||||
raise exceptions.NoUniqueMatch
|
||||
else:
|
||||
return rl[0]
|
||||
|
||||
|
||||
class Extension(HookableMixin):
|
||||
"""Extension descriptor."""
|
||||
|
||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
||||
manager_class = None
|
||||
|
||||
def __init__(self, name, module):
|
||||
super(Extension, self).__init__()
|
||||
self.name = name
|
||||
self.module = module
|
||||
self._parse_extension_module()
|
||||
|
||||
def _parse_extension_module(self):
|
||||
self.manager_class = None
|
||||
for attr_name, attr_value in self.module.__dict__.items():
|
||||
if attr_name in self.SUPPORTED_HOOKS:
|
||||
self.add_hook(attr_name, attr_value)
|
||||
else:
|
||||
try:
|
||||
if issubclass(attr_value, BaseManager):
|
||||
self.manager_class = attr_value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension '%s'>" % self.name
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
HUMAN_ID = False
|
||||
NAME_ATTR = 'name'
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
"""Populate and bind to a manager.
|
||||
|
||||
:param manager: BaseManager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k
|
||||
for k in self.__dict__.keys()
|
||||
if k[0] != '_' and k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion.
|
||||
"""
|
||||
if self.HUMAN_ID:
|
||||
name = getattr(self, self.NAME_ATTR, None)
|
||||
if name is not None:
|
||||
return strutils.to_slug(name)
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in six.iteritems(info):
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
self._info[k] = v
|
||||
except AttributeError:
|
||||
# In this case we already defined the attribute on the class
|
||||
pass
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def get(self):
|
||||
"""Support for lazy loading details.
|
||||
|
||||
Some clients, such as novaclient have the option to lazy load the
|
||||
details, details which can be loaded with this function.
|
||||
"""
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self.set_loaded(True)
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
self._add_details(
|
||||
{'x_request_id': self.manager.client.last_request_id})
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Resource):
|
||||
return NotImplemented
|
||||
# two resources of different types are not equal
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
@ -1,388 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 Grid Dynamics
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
import time
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import importutils
|
||||
import requests
|
||||
|
||||
from tuskarclient.openstack.common._i18n import _
|
||||
from tuskarclient.openstack.common.apiclient import exceptions
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',)
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
"""This client handles sending HTTP requests to OpenStack servers.
|
||||
|
||||
Features:
|
||||
|
||||
- share authentication information between several clients to different
|
||||
services (e.g., for compute and image clients);
|
||||
- reissue authentication request for expired tokens;
|
||||
- encode/decode JSON bodies;
|
||||
- raise exceptions on HTTP errors;
|
||||
- pluggable authentication;
|
||||
- store authentication information in a keyring;
|
||||
- store time spent for requests;
|
||||
- register clients for particular services, so one can use
|
||||
`http_client.identity` or `http_client.compute`;
|
||||
- log requests and responses in a format that is easy to copy-and-paste
|
||||
into terminal and send the same request with curl.
|
||||
"""
|
||||
|
||||
user_agent = "tuskarclient.openstack.common.apiclient"
|
||||
|
||||
def __init__(self,
|
||||
auth_plugin,
|
||||
region_name=None,
|
||||
endpoint_type="publicURL",
|
||||
original_ip=None,
|
||||
verify=True,
|
||||
cert=None,
|
||||
timeout=None,
|
||||
timings=False,
|
||||
keyring_saver=None,
|
||||
debug=False,
|
||||
user_agent=None,
|
||||
http=None):
|
||||
self.auth_plugin = auth_plugin
|
||||
|
||||
self.endpoint_type = endpoint_type
|
||||
self.region_name = region_name
|
||||
|
||||
self.original_ip = original_ip
|
||||
self.timeout = timeout
|
||||
self.verify = verify
|
||||
self.cert = cert
|
||||
|
||||
self.keyring_saver = keyring_saver
|
||||
self.debug = debug
|
||||
self.user_agent = user_agent or self.user_agent
|
||||
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
self.timings = timings
|
||||
|
||||
# requests within the same session can reuse TCP connections from pool
|
||||
self.http = http or requests.Session()
|
||||
|
||||
self.cached_token = None
|
||||
self.last_request_id = None
|
||||
|
||||
def _safe_header(self, name, value):
|
||||
if name in SENSITIVE_HEADERS:
|
||||
# because in python3 byte string handling is ... ug
|
||||
v = value.encode('utf-8')
|
||||
h = hashlib.sha1(v)
|
||||
d = h.hexdigest()
|
||||
return encodeutils.safe_decode(name), "{SHA1}%s" % d
|
||||
else:
|
||||
return (encodeutils.safe_decode(name),
|
||||
encodeutils.safe_decode(value))
|
||||
|
||||
def _http_log_req(self, method, url, kwargs):
|
||||
if not self.debug:
|
||||
return
|
||||
|
||||
string_parts = [
|
||||
"curl -g -i",
|
||||
"-X '%s'" % method,
|
||||
"'%s'" % url,
|
||||
]
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ("-H '%s: %s'" %
|
||||
self._safe_header(element, kwargs['headers'][element]))
|
||||
string_parts.append(header)
|
||||
|
||||
_logger.debug("REQ: %s" % " ".join(string_parts))
|
||||
if 'data' in kwargs:
|
||||
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
|
||||
|
||||
def _http_log_resp(self, resp):
|
||||
if not self.debug:
|
||||
return
|
||||
_logger.debug(
|
||||
"RESP: [%s] %s\n",
|
||||
resp.status_code,
|
||||
resp.headers)
|
||||
if resp._content_consumed:
|
||||
_logger.debug(
|
||||
"RESP BODY: %s\n",
|
||||
resp.text)
|
||||
|
||||
def serialize(self, kwargs):
|
||||
if kwargs.get('json') is not None:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = json.dumps(kwargs['json'])
|
||||
try:
|
||||
del kwargs['json']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def reset_timings(self):
|
||||
self.times = []
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around `requests.Session.request` to handle tasks such as
|
||||
setting headers, JSON encoding/decoding, and error handling.
|
||||
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
requests.Session.request (such as `headers`) or `json`
|
||||
that will be encoded as JSON and used as `data` argument
|
||||
"""
|
||||
kwargs.setdefault("headers", {})
|
||||
kwargs["headers"]["User-Agent"] = self.user_agent
|
||||
if self.original_ip:
|
||||
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
||||
self.original_ip, self.user_agent)
|
||||
if self.timeout is not None:
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
kwargs.setdefault("verify", self.verify)
|
||||
if self.cert is not None:
|
||||
kwargs.setdefault("cert", self.cert)
|
||||
self.serialize(kwargs)
|
||||
|
||||
self._http_log_req(method, url, kwargs)
|
||||
if self.timings:
|
||||
start_time = time.time()
|
||||
resp = self.http.request(method, url, **kwargs)
|
||||
if self.timings:
|
||||
self.times.append(("%s %s" % (method, url),
|
||||
start_time, time.time()))
|
||||
self._http_log_resp(resp)
|
||||
|
||||
self.last_request_id = resp.headers.get('x-openstack-request-id')
|
||||
|
||||
if resp.status_code >= 400:
|
||||
_logger.debug(
|
||||
"Request returned failure status: %s",
|
||||
resp.status_code)
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def concat_url(endpoint, url):
|
||||
"""Concatenate endpoint and final URL.
|
||||
|
||||
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
||||
"http://keystone/v2.0/tokens".
|
||||
|
||||
:param endpoint: the base URL
|
||||
:param url: the final URL
|
||||
"""
|
||||
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
||||
|
||||
def client_request(self, client, method, url, **kwargs):
|
||||
"""Send an http request using `client`'s endpoint and specified `url`.
|
||||
|
||||
If request was rejected as unauthorized (possibly because the token is
|
||||
expired), issue one authorization attempt and send the request once
|
||||
again.
|
||||
|
||||
:param client: instance of BaseClient descendant
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
`HTTPClient.request`
|
||||
"""
|
||||
|
||||
filter_args = {
|
||||
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
||||
"service_type": client.service_type,
|
||||
}
|
||||
token, endpoint = (self.cached_token, client.cached_endpoint)
|
||||
just_authenticated = False
|
||||
if not (token and endpoint):
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
pass
|
||||
if not (token and endpoint):
|
||||
self.authenticate()
|
||||
just_authenticated = True
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
if not (token and endpoint):
|
||||
raise exceptions.AuthorizationFailure(
|
||||
_("Cannot find endpoint or token for request"))
|
||||
|
||||
old_token_endpoint = (token, endpoint)
|
||||
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
# Perform the request once. If we get Unauthorized, then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
except exceptions.Unauthorized as unauth_ex:
|
||||
if just_authenticated:
|
||||
raise
|
||||
self.cached_token = None
|
||||
client.cached_endpoint = None
|
||||
if self.auth_plugin.opts.get('token'):
|
||||
self.auth_plugin.opts['token'] = None
|
||||
if self.auth_plugin.opts.get('endpoint'):
|
||||
self.auth_plugin.opts['endpoint'] = None
|
||||
self.authenticate()
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
raise unauth_ex
|
||||
if (not (token and endpoint) or
|
||||
old_token_endpoint == (token, endpoint)):
|
||||
raise unauth_ex
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
kwargs["headers"]["X-Auth-Token"] = token
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
|
||||
def add_client(self, base_client_instance):
|
||||
"""Add a new instance of :class:`BaseClient` descendant.
|
||||
|
||||
`self` will store a reference to `base_client_instance`.
|
||||
|
||||
Example:
|
||||
|
||||
>>> def test_clients():
|
||||
... from keystoneclient.auth import keystone
|
||||
... from openstack.common.apiclient import client
|
||||
... auth = keystone.KeystoneAuthPlugin(
|
||||
... username="user", password="pass", tenant_name="tenant",
|
||||
... auth_url="http://auth:5000/v2.0")
|
||||
... openstack_client = client.HTTPClient(auth)
|
||||
... # create nova client
|
||||
... from novaclient.v1_1 import client
|
||||
... client.Client(openstack_client)
|
||||
... # create keystone client
|
||||
... from keystoneclient.v2_0 import client
|
||||
... client.Client(openstack_client)
|
||||
... # use them
|
||||
... openstack_client.identity.tenants.list()
|
||||
... openstack_client.compute.servers.list()
|
||||
"""
|
||||
service_type = base_client_instance.service_type
|
||||
if service_type and not hasattr(self, service_type):
|
||||
setattr(self, service_type, base_client_instance)
|
||||
|
||||
def authenticate(self):
|
||||
self.auth_plugin.authenticate(self)
|
||||
# Store the authentication results in the keyring for later requests
|
||||
if self.keyring_saver:
|
||||
self.keyring_saver.save(self)
|
||||
|
||||
|
||||
class BaseClient(object):
|
||||
"""Top-level object to access the OpenStack API.
|
||||
|
||||
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
||||
will handle a bunch of issues such as authentication.
|
||||
"""
|
||||
|
||||
service_type = None
|
||||
endpoint_type = None # "publicURL" will be used
|
||||
cached_endpoint = None
|
||||
|
||||
def __init__(self, http_client, extensions=None):
|
||||
self.http_client = http_client
|
||||
http_client.add_client(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
for extension in extensions:
|
||||
if extension.manager_class:
|
||||
setattr(self, extension.name,
|
||||
extension.manager_class(self))
|
||||
|
||||
def client_request(self, method, url, **kwargs):
|
||||
return self.http_client.client_request(
|
||||
self, method, url, **kwargs)
|
||||
|
||||
@property
|
||||
def last_request_id(self):
|
||||
return self.http_client.last_request_id
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.client_request("HEAD", url, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self.client_request("GET", url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.client_request("POST", url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.client_request("PUT", url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.client_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.client_request("PATCH", url, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_class(api_name, version, version_map):
|
||||
"""Returns the client class for the requested API version
|
||||
|
||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
||||
:param version: the requested API version
|
||||
:param version_map: a dict of client classes keyed by version
|
||||
:rtype: a client class for the requested API version
|
||||
"""
|
||||
try:
|
||||
client_path = version_map[str(version)]
|
||||
except (KeyError, ValueError):
|
||||
msg = _("Invalid %(api_name)s client version '%(version)s'. "
|
||||
"Must be one of: %(version_map)s") % {
|
||||
'api_name': api_name,
|
||||
'version': version,
|
||||
'version_map': ', '.join(version_map.keys())}
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
|
||||
return importutils.import_class(client_path)
|
@ -1,479 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-tuskarclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from tuskarclient.openstack.common._i18n import _
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedVersion(ClientException):
|
||||
"""User is trying to use an unsupported version of the API."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(ClientException):
|
||||
"""Error in CLI tool."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(ClientException):
|
||||
"""Cannot authorize API client."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionError(ClientException):
|
||||
"""Cannot connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionRefused(ConnectionError):
|
||||
"""Connection refused while trying to connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||
"""Auth plugin misses some options."""
|
||||
def __init__(self, opt_names):
|
||||
super(AuthPluginOptionsMissing, self).__init__(
|
||||
_("Authentication failed. Missing options: %s") %
|
||||
", ".join(opt_names))
|
||||
self.opt_names = opt_names
|
||||
|
||||
|
||||
class AuthSystemNotFound(AuthorizationFailure):
|
||||
"""User has specified an AuthSystem that is not installed."""
|
||||
def __init__(self, auth_system):
|
||||
super(AuthSystemNotFound, self).__init__(
|
||||
_("AuthSystemNotFound: %r") % auth_system)
|
||||
self.auth_system = auth_system
|
||||
|
||||
|
||||
class NoUniqueMatch(ClientException):
|
||||
"""Multiple entities found instead of one."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointException(ClientException):
|
||||
"""Something is rotten in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(EndpointException):
|
||||
"""Could not find requested endpoint in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(EndpointException):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
super(AmbiguousEndpoints, self).__init__(
|
||||
_("AmbiguousEndpoints: %r") % endpoints)
|
||||
self.endpoints = endpoints
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class for all HTTP exceptions.
|
||||
"""
|
||||
http_status = 0
|
||||
message = _("HTTP Error")
|
||||
|
||||
def __init__(self, message=None, details=None,
|
||||
response=None, request_id=None,
|
||||
url=None, method=None, http_status=None):
|
||||
self.http_status = http_status or self.http_status
|
||||
self.message = message or self.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.response = response
|
||||
self.url = url
|
||||
self.method = method
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||
if request_id:
|
||||
formatted_string += " (Request-ID: %s)" % request_id
|
||||
super(HttpError, self).__init__(formatted_string)
|
||||
|
||||
|
||||
class HTTPRedirection(HttpError):
|
||||
"""HTTP Redirection."""
|
||||
message = _("HTTP Redirection")
|
||||
|
||||
|
||||
class HTTPClientError(HttpError):
|
||||
"""Client-side HTTP error.
|
||||
|
||||
Exception for cases in which the client seems to have erred.
|
||||
"""
|
||||
message = _("HTTP Client Error")
|
||||
|
||||
|
||||
class HttpServerError(HttpError):
|
||||
"""Server-side HTTP error.
|
||||
|
||||
Exception for cases in which the server is aware that it has
|
||||
erred or is incapable of performing the request.
|
||||
"""
|
||||
message = _("HTTP Server Error")
|
||||
|
||||
|
||||
class MultipleChoices(HTTPRedirection):
|
||||
"""HTTP 300 - Multiple Choices.
|
||||
|
||||
Indicates multiple options for the resource that the client may follow.
|
||||
"""
|
||||
|
||||
http_status = 300
|
||||
message = _("Multiple Choices")
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
http_status = 400
|
||||
message = _("Bad Request")
|
||||
|
||||
|
||||
class Unauthorized(HTTPClientError):
|
||||
"""HTTP 401 - Unauthorized.
|
||||
|
||||
Similar to 403 Forbidden, but specifically for use when authentication
|
||||
is required and has failed or has not yet been provided.
|
||||
"""
|
||||
http_status = 401
|
||||
message = _("Unauthorized")
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
http_status = 402
|
||||
message = _("Payment Required")
|
||||
|
||||
|
||||
class Forbidden(HTTPClientError):
|
||||
"""HTTP 403 - Forbidden.
|
||||
|
||||
The request was a valid request, but the server is refusing to respond
|
||||
to it.
|
||||
"""
|
||||
http_status = 403
|
||||
message = _("Forbidden")
|
||||
|
||||
|
||||
class NotFound(HTTPClientError):
|
||||
"""HTTP 404 - Not Found.
|
||||
|
||||
The requested resource could not be found but may be available again
|
||||
in the future.
|
||||
"""
|
||||
http_status = 404
|
||||
message = _("Not Found")
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPClientError):
|
||||
"""HTTP 405 - Method Not Allowed.
|
||||
|
||||
A request was made of a resource using a request method not supported
|
||||
by that resource.
|
||||
"""
|
||||
http_status = 405
|
||||
message = _("Method Not Allowed")
|
||||
|
||||
|
||||
class NotAcceptable(HTTPClientError):
|
||||
"""HTTP 406 - Not Acceptable.
|
||||
|
||||
The requested resource is only capable of generating content not
|
||||
acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
http_status = 406
|
||||
message = _("Not Acceptable")
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
http_status = 407
|
||||
message = _("Proxy Authentication Required")
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
http_status = 408
|
||||
message = _("Request Timeout")
|
||||
|
||||
|
||||
class Conflict(HTTPClientError):
|
||||
"""HTTP 409 - Conflict.
|
||||
|
||||
Indicates that the request could not be processed because of conflict
|
||||
in the request, such as an edit conflict.
|
||||
"""
|
||||
http_status = 409
|
||||
message = _("Conflict")
|
||||
|
||||
|
||||
class Gone(HTTPClientError):
|
||||
"""HTTP 410 - Gone.
|
||||
|
||||
Indicates that the resource requested is no longer available and will
|
||||
not be available again.
|
||||
"""
|
||||
http_status = 410
|
||||
message = _("Gone")
|
||||
|
||||
|
||||
class LengthRequired(HTTPClientError):
|
||||
"""HTTP 411 - Length Required.
|
||||
|
||||
The request did not specify the length of its content, which is
|
||||
required by the requested resource.
|
||||
"""
|
||||
http_status = 411
|
||||
message = _("Length Required")
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPClientError):
|
||||
"""HTTP 412 - Precondition Failed.
|
||||
|
||||
The server does not meet one of the preconditions that the requester
|
||||
put on the request.
|
||||
"""
|
||||
http_status = 412
|
||||
message = _("Precondition Failed")
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPClientError):
|
||||
"""HTTP 413 - Request Entity Too Large.
|
||||
|
||||
The request is larger than the server is willing or able to process.
|
||||
"""
|
||||
http_status = 413
|
||||
message = _("Request Entity Too Large")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestUriTooLong(HTTPClientError):
|
||||
"""HTTP 414 - Request-URI Too Long.
|
||||
|
||||
The URI provided was too long for the server to process.
|
||||
"""
|
||||
http_status = 414
|
||||
message = _("Request-URI Too Long")
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPClientError):
|
||||
"""HTTP 415 - Unsupported Media Type.
|
||||
|
||||
The request entity has a media type which the server or resource does
|
||||
not support.
|
||||
"""
|
||||
http_status = 415
|
||||
message = _("Unsupported Media Type")
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||
|
||||
The client has asked for a portion of the file, but the server cannot
|
||||
supply that portion.
|
||||
"""
|
||||
http_status = 416
|
||||
message = _("Requested Range Not Satisfiable")
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPClientError):
|
||||
"""HTTP 417 - Expectation Failed.
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header field.
|
||||
"""
|
||||
http_status = 417
|
||||
message = _("Expectation Failed")
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPClientError):
|
||||
"""HTTP 422 - Unprocessable Entity.
|
||||
|
||||
The request was well-formed but was unable to be followed due to semantic
|
||||
errors.
|
||||
"""
|
||||
http_status = 422
|
||||
message = _("Unprocessable Entity")
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
http_status = 500
|
||||
message = _("Internal Server Error")
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HttpNotImplemented(HttpServerError):
|
||||
"""HTTP 501 - Not Implemented.
|
||||
|
||||
The server either does not recognize the request method, or it lacks
|
||||
the ability to fulfill the request.
|
||||
"""
|
||||
http_status = 501
|
||||
message = _("Not Implemented")
|
||||
|
||||
|
||||
class BadGateway(HttpServerError):
|
||||
"""HTTP 502 - Bad Gateway.
|
||||
|
||||
The server was acting as a gateway or proxy and received an invalid
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 502
|
||||
message = _("Bad Gateway")
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
http_status = 503
|
||||
message = _("Service Unavailable")
|
||||
|
||||
|
||||
class GatewayTimeout(HttpServerError):
|
||||
"""HTTP 504 - Gateway Timeout.
|
||||
|
||||
The server was acting as a gateway or proxy and did not receive a timely
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 504
|
||||
message = _("Gateway Timeout")
|
||||
|
||||
|
||||
class HttpVersionNotSupported(HttpServerError):
|
||||
"""HTTP 505 - HttpVersion Not Supported.
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
http_status = 505
|
||||
message = _("HTTP Version Not Supported")
|
||||
|
||||
|
||||
# _code_map contains all the classes that have http_status attribute.
|
||||
_code_map = dict(
|
||||
(getattr(obj, 'http_status', None), obj)
|
||||
for name, obj in six.iteritems(vars(sys.modules[__name__]))
|
||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||
)
|
||||
|
||||
|
||||
def from_response(response, method, url):
|
||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||
|
||||
:param response: instance of `requests.Response` class
|
||||
:param method: HTTP method used for request
|
||||
:param url: URL used for request
|
||||
"""
|
||||
|
||||
req_id = response.headers.get("x-openstack-request-id")
|
||||
# NOTE(hdd) true for older versions of nova and cinder
|
||||
if not req_id:
|
||||
req_id = response.headers.get("x-compute-request-id")
|
||||
kwargs = {
|
||||
"http_status": response.status_code,
|
||||
"response": response,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"request_id": req_id,
|
||||
}
|
||||
if "retry-after" in response.headers:
|
||||
kwargs["retry_after"] = response.headers["retry-after"]
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(body, dict):
|
||||
error = body.get(list(body)[0])
|
||||
if isinstance(error, dict):
|
||||
kwargs["message"] = (error.get("message") or
|
||||
error.get("faultstring"))
|
||||
kwargs["details"] = (error.get("details") or
|
||||
six.text_type(body))
|
||||
elif content_type.startswith("text/"):
|
||||
kwargs["details"] = response.text
|
||||
|
||||
try:
|
||||
cls = _code_map[response.status_code]
|
||||
except KeyError:
|
||||
if 500 <= response.status_code < 600:
|
||||
cls = HttpServerError
|
||||
elif 400 <= response.status_code < 500:
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
return cls(**kwargs)
|
@ -1,190 +0,0 @@
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""
|
||||
A fake server that "responds" to API methods with pre-canned responses.
|
||||
|
||||
All of these responses come from the spec, so if for some reason the spec's
|
||||
wrong the tests might raise AssertionError. I've indicated in comments the
|
||||
places where actual behavior differs from the spec.
|
||||
"""
|
||||
|
||||
########################################################################
|
||||
#
|
||||
# THIS MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-tuskarclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
# W0102: Dangerous default value %s as argument
|
||||
# pylint: disable=W0102
|
||||
|
||||
import json
|
||||
|
||||
import requests
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from tuskarclient.openstack.common.apiclient import client
|
||||
|
||||
|
||||
def assert_has_keys(dct, required=None, optional=None):
|
||||
required = required or []
|
||||
optional = optional or []
|
||||
for k in required:
|
||||
try:
|
||||
assert k in dct
|
||||
except AssertionError:
|
||||
extra_keys = set(dct.keys()).difference(set(required + optional))
|
||||
raise AssertionError("found unexpected keys: %s" %
|
||||
list(extra_keys))
|
||||
|
||||
|
||||
class TestResponse(requests.Response):
|
||||
"""Wrap requests.Response and provide a convenient initialization.
|
||||
"""
|
||||
|
||||
def __init__(self, data):
|
||||
super(TestResponse, self).__init__()
|
||||
self._content_consumed = True
|
||||
if isinstance(data, dict):
|
||||
self.status_code = data.get('status_code', 200)
|
||||
# Fake the text attribute to streamline Response creation
|
||||
text = data.get('text', "")
|
||||
if isinstance(text, (dict, list)):
|
||||
self._content = json.dumps(text)
|
||||
default_headers = {
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
else:
|
||||
self._content = text
|
||||
default_headers = {}
|
||||
if six.PY3 and isinstance(self._content, six.string_types):
|
||||
self._content = self._content.encode('utf-8', 'strict')
|
||||
self.headers = data.get('headers') or default_headers
|
||||
else:
|
||||
self.status_code = data
|
||||
|
||||
def __eq__(self, other):
|
||||
return (self.status_code == other.status_code and
|
||||
self.headers == other.headers and
|
||||
self._content == other._content)
|
||||
|
||||
|
||||
class FakeHTTPClient(client.HTTPClient):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.callstack = []
|
||||
self.fixtures = kwargs.pop("fixtures", None) or {}
|
||||
if not args and "auth_plugin" not in kwargs:
|
||||
args = (None, )
|
||||
super(FakeHTTPClient, self).__init__(*args, **kwargs)
|
||||
|
||||
def assert_called(self, method, url, body=None, pos=-1):
|
||||
"""Assert than an API method was just called.
|
||||
"""
|
||||
expected = (method, url)
|
||||
called = self.callstack[pos][0:2]
|
||||
assert self.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
assert expected == called, 'Expected %s %s; got %s %s' % \
|
||||
(expected + called)
|
||||
|
||||
if body is not None:
|
||||
if self.callstack[pos][3] != body:
|
||||
raise AssertionError('%r != %r' %
|
||||
(self.callstack[pos][3], body))
|
||||
|
||||
def assert_called_anytime(self, method, url, body=None):
|
||||
"""Assert than an API method was called anytime in the test.
|
||||
"""
|
||||
expected = (method, url)
|
||||
|
||||
assert self.callstack, \
|
||||
"Expected %s %s but no calls were made." % expected
|
||||
|
||||
found = False
|
||||
entry = None
|
||||
for entry in self.callstack:
|
||||
if expected == entry[0:2]:
|
||||
found = True
|
||||
break
|
||||
|
||||
assert found, 'Expected %s %s; got %s' % \
|
||||
(method, url, self.callstack)
|
||||
if body is not None:
|
||||
assert entry[3] == body, "%s != %s" % (entry[3], body)
|
||||
|
||||
self.callstack = []
|
||||
|
||||
def clear_callstack(self):
|
||||
self.callstack = []
|
||||
|
||||
def authenticate(self):
|
||||
pass
|
||||
|
||||
def client_request(self, client, method, url, **kwargs):
|
||||
# Check that certain things are called correctly
|
||||
if method in ["GET", "DELETE"]:
|
||||
assert "json" not in kwargs
|
||||
|
||||
# Note the call
|
||||
self.callstack.append(
|
||||
(method,
|
||||
url,
|
||||
kwargs.get("headers") or {},
|
||||
kwargs.get("json") or kwargs.get("data")))
|
||||
try:
|
||||
fixture = self.fixtures[url][method]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
return TestResponse({"headers": fixture[0],
|
||||
"text": fixture[1]})
|
||||
|
||||
# Call the method
|
||||
args = parse.parse_qsl(parse.urlparse(url)[4])
|
||||
kwargs.update(args)
|
||||
munged_url = url.rsplit('?', 1)[0]
|
||||
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
||||
munged_url = munged_url.replace('-', '_')
|
||||
|
||||
callback = "%s_%s" % (method.lower(), munged_url)
|
||||
|
||||
if not hasattr(self, callback):
|
||||
raise AssertionError('Called unknown API method: %s %s, '
|
||||
'expected fakes method name: %s' %
|
||||
(method, url, callback))
|
||||
|
||||
resp = getattr(self, callback)(**kwargs)
|
||||
if len(resp) == 3:
|
||||
status, headers, body = resp
|
||||
else:
|
||||
status, body = resp
|
||||
headers = {}
|
||||
self.last_request_id = headers.get('x-openstack-request-id',
|
||||
'req-test')
|
||||
return TestResponse({
|
||||
"status_code": status,
|
||||
"text": body,
|
||||
"headers": headers,
|
||||
})
|
@ -1,100 +0,0 @@
|
||||
#
|
||||
# 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 MODULE IS DEPRECATED
|
||||
#
|
||||
# Please refer to
|
||||
# https://etherpad.openstack.org/p/kilo-tuskarclient-library-proposals for
|
||||
# the discussion leading to this deprecation.
|
||||
#
|
||||
# We recommend checking out the python-openstacksdk project
|
||||
# (https://launchpad.net/python-openstacksdk) instead.
|
||||
#
|
||||
########################################################################
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
import six
|
||||
|
||||
from tuskarclient.openstack.common._i18n import _
|
||||
from tuskarclient.openstack.common.apiclient import exceptions
|
||||
from tuskarclient.openstack.common import uuidutils
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id, **find_args):
|
||||
"""Look for resource in a given manager.
|
||||
|
||||
Used as a helper for the _find_* methods.
|
||||
Example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
def _find_hypervisor(cs, hypervisor):
|
||||
#Get a hypervisor by name or ID.
|
||||
return cliutils.find_resource(cs.hypervisors, hypervisor)
|
||||
"""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
return manager.get(int(name_or_id))
|
||||
except (TypeError, ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
if six.PY2:
|
||||
tmp_id = encodeutils.safe_encode(name_or_id)
|
||||
else:
|
||||
tmp_id = encodeutils.safe_decode(name_or_id)
|
||||
|
||||
if uuidutils.is_uuid_like(tmp_id):
|
||||
return manager.get(tmp_id)
|
||||
except (TypeError, ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
# for str id which is not uuid
|
||||
if getattr(manager, 'is_alphanum_id_allowed', False):
|
||||
try:
|
||||
return manager.get(name_or_id)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
return manager.find(human_id=name_or_id, **find_args)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
# finally try to find entity by name
|
||||
try:
|
||||
resource = getattr(manager, 'resource_class', None)
|
||||
name_attr = resource.NAME_ATTR if resource else 'name'
|
||||
kwargs = {name_attr: name_or_id}
|
||||
kwargs.update(find_args)
|
||||
return manager.find(**kwargs)
|
||||
except exceptions.NotFound:
|
||||
msg = _("No %(name)s with a name or "
|
||||
"ID of '%(name_or_id)s' exists.") % \
|
||||
{
|
||||
"name": manager.resource_class.__name__.lower(),
|
||||
"name_or_id": name_or_id
|
||||
}
|
||||
raise exceptions.CommandError(msg)
|
||||
except exceptions.NoUniqueMatch:
|
||||
msg = _("Multiple %(name)s matches found for "
|
||||
"'%(name_or_id)s', use an ID to be more specific.") % \
|
||||
{
|
||||
"name": manager.resource_class.__name__.lower(),
|
||||
"name_or_id": name_or_id
|
||||
}
|
||||
raise exceptions.CommandError(msg)
|
@ -1,271 +0,0 @@
|
||||
# Copyright 2012 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.
|
||||
|
||||
# W0603: Using the global statement
|
||||
# W0621: Redefining name %s from outer scope
|
||||
# pylint: disable=W0603,W0621
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import getpass
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
import prettytable
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
from tuskarclient.openstack.common._i18n import _
|
||||
|
||||
|
||||
class MissingArgs(Exception):
|
||||
"""Supplied arguments are not sufficient for calling a function."""
|
||||
def __init__(self, missing):
|
||||
self.missing = missing
|
||||
msg = _("Missing arguments: %s") % ", ".join(missing)
|
||||
super(MissingArgs, self).__init__(msg)
|
||||
|
||||
|
||||
def validate_args(fn, *args, **kwargs):
|
||||
"""Check that the supplied args are sufficient for calling a function.
|
||||
|
||||
>>> validate_args(lambda a: None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): a
|
||||
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): b, d
|
||||
|
||||
:param fn: the function to check
|
||||
:param arg: the positional arguments supplied
|
||||
:param kwargs: the keyword arguments supplied
|
||||
"""
|
||||
argspec = inspect.getargspec(fn)
|
||||
|
||||
num_defaults = len(argspec.defaults or [])
|
||||
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
||||
|
||||
def isbound(method):
|
||||
return getattr(method, '__self__', None) is not None
|
||||
|
||||
if isbound(fn):
|
||||
required_args.pop(0)
|
||||
|
||||
missing = [arg for arg in required_args if arg not in kwargs]
|
||||
missing = missing[len(args):]
|
||||
if missing:
|
||||
raise MissingArgs(missing)
|
||||
|
||||
|
||||
def arg(*args, **kwargs):
|
||||
"""Decorator for CLI args.
|
||||
|
||||
Example:
|
||||
|
||||
>>> @arg("name", help="Name of the new entity")
|
||||
... def entity_create(args):
|
||||
... pass
|
||||
"""
|
||||
def _decorator(func):
|
||||
add_arg(func, *args, **kwargs)
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def env(*args, **kwargs):
|
||||
"""Returns the first environment variable set.
|
||||
|
||||
If all are empty, defaults to '' or keyword arg `default`.
|
||||
"""
|
||||
for arg in args:
|
||||
value = os.environ.get(arg)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def add_arg(func, *args, **kwargs):
|
||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||
|
||||
if not hasattr(func, 'arguments'):
|
||||
func.arguments = []
|
||||
|
||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||
# tests.
|
||||
if (args, kwargs) not in func.arguments:
|
||||
# Because of the semantics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.arguments.insert(0, (args, kwargs))
|
||||
|
||||
|
||||
def unauthenticated(func):
|
||||
"""Adds 'unauthenticated' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
>>> @unauthenticated
|
||||
... def mymethod(f):
|
||||
... pass
|
||||
"""
|
||||
func.unauthenticated = True
|
||||
return func
|
||||
|
||||
|
||||
def isunauthenticated(func):
|
||||
"""Checks if the function does not require authentication.
|
||||
|
||||
Mark such functions with the `@unauthenticated` decorator.
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
return getattr(func, 'unauthenticated', False)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||
mixed_case_fields=None, field_labels=None):
|
||||
"""Print a list or objects as a table, one row per object.
|
||||
|
||||
:param objs: iterable of :class:`Resource`
|
||||
:param fields: attributes that correspond to columns, in order
|
||||
:param formatters: `dict` of callables for field formatting
|
||||
:param sortby_index: index of the field for sorting table rows
|
||||
:param mixed_case_fields: fields corresponding to object attributes that
|
||||
have mixed case names (e.g., 'serverId')
|
||||
:param field_labels: Labels to use in the heading of the table, default to
|
||||
fields.
|
||||
"""
|
||||
formatters = formatters or {}
|
||||
mixed_case_fields = mixed_case_fields or []
|
||||
field_labels = field_labels or fields
|
||||
if len(field_labels) != len(fields):
|
||||
raise ValueError(_("Field labels list %(labels)s has different number "
|
||||
"of elements than fields list %(fields)s"),
|
||||
{'labels': field_labels, 'fields': fields})
|
||||
|
||||
if sortby_index is None:
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = {'sortby': field_labels[sortby_index]}
|
||||
pt = prettytable.PrettyTable(field_labels)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
if field in mixed_case_fields:
|
||||
field_name = field.replace(' ', '_')
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
|
||||
|
||||
|
||||
def print_dict(dct, dict_property="Property", wrap=0):
|
||||
"""Print a `dict` as a table of two columns.
|
||||
|
||||
:param dct: `dict` to print
|
||||
:param dict_property: name of the first column
|
||||
:param wrap: wrapping for the second column
|
||||
"""
|
||||
pt = prettytable.PrettyTable([dict_property, 'Value'])
|
||||
pt.align = 'l'
|
||||
for k, v in six.iteritems(dct):
|
||||
# convert dict to str to check length
|
||||
if isinstance(v, dict):
|
||||
v = six.text_type(v)
|
||||
if wrap > 0:
|
||||
v = textwrap.fill(six.text_type(v), wrap)
|
||||
# if value has a newline, add in multiple rows
|
||||
# e.g. fault with stacktrace
|
||||
if v and isinstance(v, six.string_types) and r'\n' in v:
|
||||
lines = v.strip().split(r'\n')
|
||||
col1 = k
|
||||
for line in lines:
|
||||
pt.add_row([col1, line])
|
||||
col1 = ''
|
||||
else:
|
||||
pt.add_row([k, v])
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string()).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string()))
|
||||
|
||||
|
||||
def get_password(max_password_prompts=3):
|
||||
"""Read password from TTY."""
|
||||
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
||||
pw = None
|
||||
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
||||
# Check for Ctrl-D
|
||||
try:
|
||||
for __ in moves.range(max_password_prompts):
|
||||
pw1 = getpass.getpass("OS Password: ")
|
||||
if verify:
|
||||
pw2 = getpass.getpass("Please verify: ")
|
||||
else:
|
||||
pw2 = pw1
|
||||
if pw1 == pw2 and pw1:
|
||||
pw = pw1
|
||||
break
|
||||
except EOFError:
|
||||
pass
|
||||
return pw
|
||||
|
||||
|
||||
def service_type(stype):
|
||||
"""Adds 'service_type' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@service_type('volume')
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
def inner(f):
|
||||
f.service_type = stype
|
||||
return f
|
||||
return inner
|
||||
|
||||
|
||||
def get_service_type(f):
|
||||
"""Retrieves service type from function."""
|
||||
return getattr(f, 'service_type', None)
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def exit(msg=''):
|
||||
if msg:
|
||||
print (msg, file=sys.stderr)
|
||||
sys.exit(1)
|
@ -1,37 +0,0 @@
|
||||
# Copyright (c) 2012 Intel Corporation.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
UUID related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def is_uuid_like(val):
|
||||
"""Returns validation of a value as a UUID.
|
||||
|
||||
For our purposes, a UUID is a canonical form string:
|
||||
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
||||
|
||||
"""
|
||||
try:
|
||||
return str(uuid.UUID(val)).lower() == val.lower()
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
return False
|
@ -1,61 +0,0 @@
|
||||
# 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 openstackclient.common import utils
|
||||
|
||||
from tuskarclient import client as tuskar_client
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_MANAGEMENT_API_VERSION = '2'
|
||||
API_VERSION_OPTION = 'os_management_api_version'
|
||||
API_NAME = 'management'
|
||||
API_VERSIONS = {
|
||||
'2': 'tuskarclient.v2.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
"""Returns a management service client."""
|
||||
|
||||
endpoint = instance.get_endpoint_for_service_type(
|
||||
API_NAME,
|
||||
region_name=instance._region_name,
|
||||
)
|
||||
|
||||
token = instance.auth.get_token(instance.session)
|
||||
|
||||
client = tuskar_client.get_client(
|
||||
instance._api_version[API_NAME],
|
||||
tuskar_url=endpoint,
|
||||
os_auth_token=token,
|
||||
username=instance._username,
|
||||
password=instance._password,
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def build_option_parser(parser):
|
||||
"""Hook to add global options."""
|
||||
parser.add_argument(
|
||||
'--os-management-api-version',
|
||||
metavar='<management-api-version>',
|
||||
default=utils.env(
|
||||
'OS_MANAGEMENT_API_VERSION',
|
||||
default=DEFAULT_MANAGEMENT_API_VERSION),
|
||||
help='Management API version, default=' +
|
||||
DEFAULT_MANAGEMENT_API_VERSION +
|
||||
' (Env: OS_MANAGEMENT_API_VERSION)')
|
||||
return parser
|
@ -1,344 +0,0 @@
|
||||
# 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 __future__ import print_function
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
|
||||
from cliff import command
|
||||
from cliff import lister
|
||||
from cliff import show
|
||||
|
||||
from tuskarclient.common import formatting
|
||||
from tuskarclient.common import utils
|
||||
from tuskarclient.openstack.common.apiclient import exceptions as exc
|
||||
|
||||
|
||||
class CreateManagementPlan(show.ShowOne):
|
||||
"""Create a Management Plan."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.CreateManagementPlan')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CreateManagementPlan, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'name',
|
||||
help="Name of the plan being created."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-d', '--description',
|
||||
help='A textual description of the plan.')
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
name = parsed_args.name
|
||||
|
||||
try:
|
||||
plan = client.plans.create(
|
||||
name=name,
|
||||
description=parsed_args.description
|
||||
)
|
||||
except exc.Conflict:
|
||||
raise exc.CommandError(
|
||||
'Plan with name "%s" already exists.' % name)
|
||||
|
||||
return self.dict2columns(plan.to_dict())
|
||||
|
||||
|
||||
class DeleteManagementPlan(command.Command):
|
||||
"""Delete a Management Plan."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.DeleteManagementPlan')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeleteManagementPlan, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'plan_uuid',
|
||||
help="The UUID of the plan being deleted."
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
|
||||
client.plans.delete(parsed_args.plan_uuid)
|
||||
|
||||
|
||||
class ListManagementPlans(lister.Lister):
|
||||
"""List the Management Plans."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ListManagementPlans')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListManagementPlans, self).get_parser(prog_name)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
|
||||
plans = client.plans.list()
|
||||
|
||||
return (
|
||||
('uuid', 'name', 'description', 'roles'),
|
||||
((p.uuid, p.name, p.description,
|
||||
', '.join(r.name for r in p.roles))
|
||||
for p in plans)
|
||||
)
|
||||
|
||||
|
||||
class SetManagementPlan(show.ShowOne):
|
||||
"""Update a Management Plans properties."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.SetManagementPlan')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SetManagementPlan, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'plan_uuid',
|
||||
help="The UUID of the plan being updated."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-P', '--parameter', dest='parameters', metavar='<KEY1=VALUE1>',
|
||||
help=('Set a parameter in the Plan. This can be specified '
|
||||
'multiple times.'),
|
||||
action='append'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-F', '--flavor', dest='flavors', metavar='<ROLE=FLAVOR>',
|
||||
help=('Set the flavor for a role in the Plan. This can be '
|
||||
'specified multiple times.'),
|
||||
action='append'
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-S', '--scale', dest='scales', metavar='<ROLE=SCALE-COUNT>',
|
||||
help=('Set the Scale count for a role in the Plan. This can be '
|
||||
'specified multiple times.'),
|
||||
action='append'
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
|
||||
plan = client.plans.get(parsed_args.plan_uuid)
|
||||
roles = plan.roles
|
||||
|
||||
patch = []
|
||||
|
||||
patch.extend(utils.parameters_args_to_patch(parsed_args.parameters))
|
||||
patch.extend(utils.args_to_patch(parsed_args.flavors, roles, "Flavor"))
|
||||
patch.extend(utils.args_to_patch(parsed_args.scales, roles, "count"))
|
||||
|
||||
if len(patch) > 0:
|
||||
plan = client.plans.patch(parsed_args.plan_uuid, patch)
|
||||
else:
|
||||
print(("WARNING: No valid arguments passed. No update operation "
|
||||
"has been performed."), file=sys.stderr)
|
||||
|
||||
return self.dict2columns(plan.to_dict())
|
||||
|
||||
|
||||
class ShowManagementPlan(show.ShowOne):
|
||||
"""Show a Management Plan."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ShowManagementPlan')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowManagementPlan, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'plan_uuid',
|
||||
help="The UUID of the plan to show."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--long', default=False, action="store_true",
|
||||
help="Display full plan details"
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
plan = client.plans.get(parsed_args.plan_uuid)
|
||||
plan_dict = plan.to_dict()
|
||||
|
||||
if not parsed_args.long:
|
||||
if 'parameters' in plan_dict:
|
||||
plan_dict['parameters'] = ("Parameter output suppressed. Use "
|
||||
"--long to display them.")
|
||||
plan_dict['roles'] = ', '.join([r.name for r in plan.roles])
|
||||
|
||||
return self.dict2columns(plan_dict)
|
||||
|
||||
|
||||
class AddManagementPlanRole(show.ShowOne):
|
||||
"""Add a Role to a Management Plan."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.AddManagementPlanRole')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(AddManagementPlanRole, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'plan_uuid',
|
||||
help="The UUID of the plan."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'role_uuid',
|
||||
help="The UUID of the Role being added to the Plan."
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
|
||||
plan = client.plans.add_role(
|
||||
parsed_args.plan_uuid,
|
||||
parsed_args.role_uuid
|
||||
)
|
||||
|
||||
return self.dict2columns(filtered_plan_dict(plan.to_dict()))
|
||||
|
||||
|
||||
class RemoveManagementPlanRole(show.ShowOne):
|
||||
"""Remove a Role from a Management Plan."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.RemoveManagementPlanRole')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(RemoveManagementPlanRole, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'plan_uuid',
|
||||
help="The UUID of the plan."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'role_uuid',
|
||||
help="The UUID of the Role being removed from the Plan."
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
|
||||
plan = client.plans.remove_role(
|
||||
parsed_args.plan_uuid,
|
||||
parsed_args.role_uuid
|
||||
)
|
||||
|
||||
return self.dict2columns(filtered_plan_dict(plan.to_dict()))
|
||||
|
||||
|
||||
class DownloadManagementPlan(command.Command):
|
||||
"""Download a Management Plan."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.DownloadManagementPlan')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DownloadManagementPlan, self).get_parser(prog_name)
|
||||
|
||||
parser.add_argument(
|
||||
'plan_uuid',
|
||||
help="The UUID of the plan to download."
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'-O', '--output-dir', metavar='<OUTPUT DIR>',
|
||||
required=True,
|
||||
help=('Directory to write template files into. It will be created '
|
||||
'if it does not exist and any existing files in the '
|
||||
'directory will be removed.')
|
||||
)
|
||||
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
|
||||
output_dir = parsed_args.output_dir
|
||||
|
||||
if os.path.isdir(output_dir):
|
||||
shutil.rmtree(output_dir)
|
||||
|
||||
os.mkdir(output_dir)
|
||||
|
||||
# retrieve templates
|
||||
templates = client.plans.templates(parsed_args.plan_uuid)
|
||||
|
||||
# write file for each key-value in templates
|
||||
print("The following templates will be written:")
|
||||
for template_name, template_content in templates.items():
|
||||
|
||||
# It's possible to organize the role templates and their dependent
|
||||
# files into directories, in which case the template_name will
|
||||
# carry the directory information. If that's the case, first
|
||||
# create the directory structure (if it hasn't already been
|
||||
# created by another file in the templates list).
|
||||
template_dir = os.path.dirname(template_name)
|
||||
output_template_dir = os.path.join(output_dir, template_dir)
|
||||
if template_dir and not os.path.exists(output_template_dir):
|
||||
os.makedirs(output_template_dir)
|
||||
|
||||
filename = os.path.join(output_dir, template_name)
|
||||
with open(filename, 'w+') as template_file:
|
||||
template_file.write(template_content)
|
||||
print(filename)
|
||||
|
||||
|
||||
def filtered_plan_dict(plan_dict):
|
||||
if 'parameters' in plan_dict and 'roles' in plan_dict:
|
||||
plan_dict['parameters'] = [param for param in
|
||||
plan_dict['parameters']
|
||||
if param['name'].endswith('::count')]
|
||||
|
||||
plan_dict['parameters'] = formatting.parameters_v2_formatter(
|
||||
plan_dict['parameters'])
|
||||
|
||||
plan_dict['roles'] = formatting.parameters_v2_formatter(
|
||||
plan_dict['roles'])
|
||||
|
||||
return plan_dict
|
@ -1,38 +0,0 @@
|
||||
# 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 cliff import lister
|
||||
|
||||
|
||||
class ListRoles(lister.Lister):
|
||||
"""List Roles."""
|
||||
|
||||
log = logging.getLogger(__name__ + '.ListRoles')
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListRoles, self).get_parser(prog_name)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
self.log.debug("take_action(%s)" % parsed_args)
|
||||
|
||||
client = self.app.client_manager.management
|
||||
|
||||
roles = client.roles.list()
|
||||
|
||||
return (
|
||||
('uuid', 'name', 'version', 'description'),
|
||||
((r.uuid, r.name, r.version, r.description.strip())
|
||||
for r in roles)
|
||||
)
|
@ -1,257 +0,0 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Command-line interface to the Tuskar API.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.handlers
|
||||
import sys
|
||||
|
||||
from keystoneclient import session as kssession
|
||||
import six
|
||||
|
||||
import tuskarclient
|
||||
from tuskarclient import client
|
||||
import tuskarclient.common.utils as utils
|
||||
from tuskarclient.openstack.common.apiclient import exceptions as exc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TuskarShell(object):
|
||||
|
||||
def __init__(self, raw_args,
|
||||
argument_parser_class=argparse.ArgumentParser):
|
||||
self.raw_args = raw_args
|
||||
self.argument_parser_class = argument_parser_class
|
||||
|
||||
self.partial_args = None
|
||||
self.parser = None
|
||||
self.subparsers = None
|
||||
self._prepare_parsers()
|
||||
|
||||
def _prepare_parsers(self):
|
||||
nonversioned_parser = self._nonversioned_parser()
|
||||
self.partial_args = (
|
||||
nonversioned_parser.parse_known_args(self.raw_args)[0])
|
||||
self.parser, self.subparsers = (
|
||||
self._parser(self.partial_args.tuskar_api_version))
|
||||
|
||||
def run(self):
|
||||
'''Run the CLI. Parse arguments and do the respective action.'''
|
||||
|
||||
# run self.do_help() if we have no raw_args at all or just -h/--help
|
||||
if (not self.raw_args
|
||||
or self.raw_args in (['-h'], ['--help'])):
|
||||
self.do_help(self.partial_args)
|
||||
return 0
|
||||
|
||||
args = self.parser.parse_args(self.raw_args)
|
||||
|
||||
self._setup_logging(args.debug)
|
||||
|
||||
# run self.do_help() if we have help subcommand or -h/--help option
|
||||
if args.func == self.do_help or args.help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
|
||||
self._ensure_auth_info(args)
|
||||
|
||||
tuskar_client = client.get_client(
|
||||
self.partial_args.tuskar_api_version, **args.__dict__)
|
||||
args.func(tuskar_client, args)
|
||||
|
||||
def _ensure_auth_info(self, args):
|
||||
'''Ensure that authentication information is provided. Two variants
|
||||
of authentication are supported:
|
||||
- provide username, password, tenant and auth url
|
||||
- provide token and tuskar url (or auth url instead of tuskar url)
|
||||
'''
|
||||
if not args.os_auth_token:
|
||||
if not args.os_username:
|
||||
raise exc.CommandError("You must provide username via either "
|
||||
"--os-username or env[OS_USERNAME]")
|
||||
|
||||
if not args.os_password:
|
||||
raise exc.CommandError("You must provide password via either "
|
||||
"--os-password or env[OS_PASSWORD]")
|
||||
|
||||
if not args.os_tenant_id and not args.os_tenant_name:
|
||||
raise exc.CommandError("You must provide tenant via either "
|
||||
"--os-tenant-name or --os-tenant-id or "
|
||||
"env[OS_TENANT_NAME] or "
|
||||
"env[OS_TENANT_ID]")
|
||||
|
||||
if not args.os_auth_url:
|
||||
raise exc.CommandError("You must provide auth URL via either "
|
||||
"--os-auth-url or env[OS_AUTH_URL]")
|
||||
else:
|
||||
if not args.tuskar_url and not args.os_auth_url:
|
||||
raise exc.CommandError("You must provide either "
|
||||
"--tuskar-url or --os-auth-url or "
|
||||
"env[TUSKAR_URL] or env[OS_AUTH_URL]")
|
||||
|
||||
def _parser(self, version):
|
||||
'''Create a top level argument parser.
|
||||
|
||||
:param version: version of Tuskar API (and corresponding CLI
|
||||
commands) to use
|
||||
:return: main parser and subparsers
|
||||
:rtype: (Parser, Subparsers)
|
||||
'''
|
||||
parser = self._nonversioned_parser()
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
versioned_shell = utils.import_versioned_module(version, 'shell')
|
||||
versioned_shell.enhance_parser(parser, subparsers)
|
||||
utils.define_commands_from_module(subparsers, self)
|
||||
return parser, subparsers
|
||||
|
||||
def _nonversioned_parser(self):
|
||||
'''Create a basic parser that doesn't contain version-specific
|
||||
subcommands. This one is mainly useful for parsing which API
|
||||
version should be used for the versioned full blown parser and
|
||||
defining common version-agnostic options.
|
||||
'''
|
||||
parser = self.argument_parser_class(
|
||||
prog='tuskar',
|
||||
description='OpenStack Management CLI',
|
||||
add_help=False,
|
||||
)
|
||||
|
||||
parser.add_argument('-h', '--help',
|
||||
action='store_true',
|
||||
help="Print this help message and exit.",
|
||||
)
|
||||
|
||||
parser.add_argument('--version',
|
||||
action='version',
|
||||
version=tuskarclient.__version__,
|
||||
help="Shows the client version and exits.")
|
||||
|
||||
parser.add_argument('-d', '--debug',
|
||||
default=bool(utils.env('TUSKARCLIENT_DEBUG')),
|
||||
action='store_true',
|
||||
help='Defaults to env[TUSKARCLIENT_DEBUG].')
|
||||
|
||||
parser.add_argument('--os-username',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help='Defaults to env[OS_USERNAME]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_username',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-password',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help='Defaults to env[OS_PASSWORD]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_password',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-tenant-id',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help='Defaults to env[OS_TENANT_ID]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-tenant-name',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help='Defaults to env[OS_TENANT_NAME]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-auth-url',
|
||||
default=utils.env('OS_AUTH_URL'),
|
||||
help='Defaults to env[OS_AUTH_URL]',
|
||||
)
|
||||
|
||||
parser.add_argument('--os_auth_url',
|
||||
help=argparse.SUPPRESS,
|
||||
)
|
||||
|
||||
parser.add_argument('--os-auth-token',
|
||||
default=utils.env('OS_AUTH_TOKEN',
|
||||
default=None),
|
||||
help='Defaults to env[OS_AUTH_TOKEN]')
|
||||
|
||||
parser.add_argument('--os_auth_token',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--tuskar-url',
|
||||
default=utils.env('TUSKAR_URL'),
|
||||
help='Defaults to env[TUSKAR_URL]')
|
||||
|
||||
parser.add_argument('--tuskar_url',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--tuskar-api-version',
|
||||
default=utils.env('TUSKAR_API_VERSION',
|
||||
default='2'),
|
||||
help='Defaults to env[TUSKAR_API_VERSION] '
|
||||
'or 2')
|
||||
|
||||
parser.add_argument('--tuskar_api_version',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
kssession.Session.register_cli_options(parser)
|
||||
|
||||
return parser
|
||||
|
||||
@utils.arg(
|
||||
'command', metavar='<subcommand>', nargs='?',
|
||||
help='Display help for <subcommand>')
|
||||
def do_help(self, args):
|
||||
"""Display help about this program or one of its subcommands."""
|
||||
if getattr(args, 'command', None):
|
||||
if args.command in self.subparsers.choices:
|
||||
# print help for subcommand
|
||||
self.subparsers.choices[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||
args.command)
|
||||
else:
|
||||
# print general help
|
||||
self.parser.print_help()
|
||||
|
||||
def _setup_logging(self, debug):
|
||||
log_lvl = logging.DEBUG if debug else logging.WARNING
|
||||
logging.basicConfig(
|
||||
format="%(levelname)s (%(module)s) %(message)s",
|
||||
level=log_lvl)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
TuskarShell(sys.argv[1:]).run()
|
||||
except exc.CommandError as e:
|
||||
print(six.text_type(e), file=sys.stderr)
|
||||
except Exception as e:
|
||||
logger.exception("Exiting due to an error:")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,123 +0,0 @@
|
||||
# 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 mock
|
||||
|
||||
from tuskarclient.common import auth
|
||||
from tuskarclient.openstack.common.apiclient import client
|
||||
from tuskarclient.openstack.common.apiclient import exceptions as exc
|
||||
from tuskarclient.tests import utils as test_utils
|
||||
|
||||
|
||||
@mock.patch.object(auth.ksclient, 'Client')
|
||||
class KeystoneAuthPluginTest(test_utils.TestCase):
|
||||
def setUp(self):
|
||||
super(KeystoneAuthPluginTest, self).setUp()
|
||||
plugin = auth.KeystoneAuthPlugin(
|
||||
username="fake-username",
|
||||
password="fake-password",
|
||||
tenant_id="fake-tenant-id",
|
||||
tenant_name="fake-tenant-name",
|
||||
auth_url="http://auth",
|
||||
endpoint="http://tuskar")
|
||||
self.cs = client.HTTPClient(auth_plugin=plugin)
|
||||
|
||||
def test_authenticate(self, mock_ksclient):
|
||||
self.cs.authenticate()
|
||||
mock_ksclient.assert_called_with(
|
||||
username="fake-username",
|
||||
password="fake-password",
|
||||
tenant_id="fake-tenant-id",
|
||||
tenant_name="fake-tenant-name",
|
||||
auth_url="http://auth",
|
||||
cacert=None,
|
||||
cert=None,
|
||||
key=None)
|
||||
|
||||
def test_authenticate_with_ssl(self, mock_ksclient):
|
||||
plugin = auth.KeystoneAuthPlugin(
|
||||
username="fake-username",
|
||||
password="fake-password",
|
||||
tenant_id="fake-tenant-id",
|
||||
tenant_name="fake-tenant-name",
|
||||
auth_url="http://auth",
|
||||
endpoint="http://tuskar",
|
||||
cacert="/fake/cacert.pem",
|
||||
cert="/fake/cert.pem",
|
||||
key="/fake/key.pem")
|
||||
self.cs = client.HTTPClient(auth_plugin=plugin)
|
||||
self.cs.authenticate()
|
||||
mock_ksclient.assert_called_with(
|
||||
username="fake-username",
|
||||
password="fake-password",
|
||||
tenant_id="fake-tenant-id",
|
||||
tenant_name="fake-tenant-name",
|
||||
auth_url="http://auth",
|
||||
cacert="/fake/cacert.pem",
|
||||
cert="/fake/cert.pem",
|
||||
key="/fake/key.pem")
|
||||
|
||||
def test_token_and_endpoint(self, mock_ksclient):
|
||||
self.cs.authenticate()
|
||||
(token, endpoint) = self.cs.auth_plugin.token_and_endpoint(
|
||||
"fake-endpoint-type", "fake-service-type")
|
||||
self.assertIsInstance(token, mock.MagicMock)
|
||||
self.assertEqual("http://tuskar", endpoint)
|
||||
|
||||
def test_token_and_endpoint_before_auth(self, mock_ksclient):
|
||||
(token, endpoint) = self.cs.auth_plugin.token_and_endpoint(
|
||||
"fake-endpoint-type", "fake-service-type")
|
||||
self.assertIsNone(token, None)
|
||||
self.assertIsNone(endpoint, None)
|
||||
|
||||
|
||||
@mock.patch.object(auth.ksclient, 'Client')
|
||||
class KeystoneAuthPluginTokenTest(test_utils.TestCase):
|
||||
def test_token_and_endpoint(self, mock_ksclient):
|
||||
plugin = auth.KeystoneAuthPlugin(
|
||||
token="fake-token",
|
||||
endpoint="http://tuskar")
|
||||
cs = client.HTTPClient(auth_plugin=plugin)
|
||||
|
||||
cs.authenticate()
|
||||
(token, endpoint) = cs.auth_plugin.token_and_endpoint(
|
||||
"fake-endpoint-type", "fake-service-type")
|
||||
self.assertEqual('fake-token', token)
|
||||
self.assertEqual('http://tuskar', endpoint)
|
||||
|
||||
|
||||
class KeystoneAuthPluginOptionsTest(test_utils.TestCase):
|
||||
def setUp(self):
|
||||
super(KeystoneAuthPluginOptionsTest, self).setUp()
|
||||
self.kwargs = {
|
||||
'username': "fake-username",
|
||||
'password': "fake-password",
|
||||
'tenant_id': "fake-tenant-id",
|
||||
'tenant_name': "fake-tenant-name",
|
||||
'auth_url': "http://auth",
|
||||
'endpoint': "http://tuskar"
|
||||
}
|
||||
|
||||
def test_it_raises_error_without_tenant_id_and_name(self):
|
||||
kwargs = self.kwargs.copy()
|
||||
del kwargs['tenant_id']
|
||||
del kwargs['tenant_name']
|
||||
auth_plugin = auth.KeystoneAuthPlugin(**kwargs)
|
||||
self.assertRaises(exc.AuthPluginOptionsMissing,
|
||||
auth_plugin.sufficient_options)
|
||||
|
||||
def test_it_raises_error_withtout_username(self):
|
||||
kwargs = self.kwargs.copy()
|
||||
del kwargs['username']
|
||||
auth_plugin = auth.KeystoneAuthPlugin(**kwargs)
|
||||
self.assertRaises(exc.AuthPluginOptionsMissing,
|
||||
auth_plugin.sufficient_options)
|
@ -1,99 +0,0 @@
|
||||
# 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 mock
|
||||
import six
|
||||
|
||||
import tuskarclient.common.formatting as fmt
|
||||
import tuskarclient.tests.utils as tutils
|
||||
from tuskarclient.v2 import plans
|
||||
|
||||
|
||||
class PrintTest(tutils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(PrintTest, self).setUp()
|
||||
self.outfile = six.StringIO()
|
||||
|
||||
def test_print_dict(self):
|
||||
dict_ = {'k': 'v', 'key': 'value'}
|
||||
formatters = {'key': lambda v: 'custom ' + v}
|
||||
custom_labels = {'k': 'custom_key'}
|
||||
|
||||
fmt.print_dict(dict_, formatters, custom_labels,
|
||||
outfile=self.outfile)
|
||||
self.assertEqual(
|
||||
('+------------+--------------+\n'
|
||||
'| Property | Value |\n'
|
||||
'+------------+--------------+\n'
|
||||
'| custom_key | v |\n'
|
||||
'| key | custom value |\n'
|
||||
'+------------+--------------+\n'),
|
||||
self.outfile.getvalue()
|
||||
)
|
||||
|
||||
def test_print_list(self):
|
||||
fields = ['thing', 'color', '!artistic_name']
|
||||
formatters = {
|
||||
'!artistic_name': lambda obj: '{0} {1}'.format(obj.color,
|
||||
obj.thing),
|
||||
'color': lambda c: c.split(' ')[1],
|
||||
}
|
||||
custom_labels = {'thing': 'name', '!artistic_name': 'artistic name'}
|
||||
|
||||
fmt.print_list(self.objects(), fields, formatters, custom_labels,
|
||||
outfile=self.outfile)
|
||||
self.assertEqual(
|
||||
('+------+-------+-----------------+\n'
|
||||
'| name | color | artistic name |\n'
|
||||
'+------+-------+-----------------+\n'
|
||||
'| moon | green | dark green moon |\n'
|
||||
'| sun | blue | bright blue sun |\n'
|
||||
'+------+-------+-----------------+\n'),
|
||||
self.outfile.getvalue()
|
||||
)
|
||||
|
||||
def test_print_list_custom_field_without_formatter(self):
|
||||
fields = ['!artistic_name']
|
||||
|
||||
self.assertRaises(KeyError, fmt.print_list, self.objects(), fields)
|
||||
|
||||
def objects(self):
|
||||
return [
|
||||
mock.Mock(thing='sun', color='bright blue'),
|
||||
mock.Mock(thing='moon', color='dark green'),
|
||||
]
|
||||
|
||||
|
||||
class FormattersTest(tutils.TestCase):
|
||||
|
||||
def test_attributes_formatter(self):
|
||||
"""Test the attributes formatter displays the attributes correctly."""
|
||||
|
||||
attributes = {
|
||||
'password': 'pass',
|
||||
'mysql_host': 'http://somewhere',
|
||||
'a thing': 'a value'
|
||||
}
|
||||
self.assertEqual(
|
||||
("a thing=a value\nmysql_host=http://somewhere\npassword=pass\n"),
|
||||
fmt.attributes_formatter(attributes),
|
||||
)
|
||||
|
||||
def test_list_plan_roles_formatter(self):
|
||||
roles = plans.Plan(None,
|
||||
{'roles': [{'name': 'foo_role'},
|
||||
{'name': 'bar_role'}]}).roles
|
||||
self.assertEqual(
|
||||
"foo_role, bar_role",
|
||||
fmt.list_plan_roles_formatter(roles)
|
||||
)
|
@ -1,154 +0,0 @@
|
||||
# Copyright 2013 OpenStack LLC.
|
||||
# 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 mock
|
||||
|
||||
from tuskarclient.common import utils
|
||||
from tuskarclient.openstack.common.apiclient import exceptions as exc
|
||||
from tuskarclient.tests import utils as test_utils
|
||||
|
||||
|
||||
class DefineCommandsTest(test_utils.TestCase):
|
||||
|
||||
def test_define_commands_from_module(self):
|
||||
subparsers = mock.Mock()
|
||||
subparser = mock.MagicMock()
|
||||
subparsers.add_parser.return_value = subparser
|
||||
dummy_module = self.dummy_command_module()
|
||||
|
||||
utils.define_commands_from_module(subparsers, dummy_module)
|
||||
subparsers.add_parser.assert_called_with(
|
||||
'dummy-list', help="Docstring.", description="Docstring.")
|
||||
subparser.add_argument.assert_called_with(
|
||||
'-a', metavar='<NUMBER>', help="Add a number.")
|
||||
subparser.set_defaults.assert_called_with(
|
||||
func=dummy_module.do_dummy_list)
|
||||
|
||||
def dummy_command_module(self):
|
||||
@utils.arg('-a', metavar='<NUMBER>', help="Add a number.")
|
||||
def do_dummy_list():
|
||||
'''Docstring.'''
|
||||
return 42
|
||||
|
||||
dummy = mock.Mock()
|
||||
dummy.do_dummy_list = do_dummy_list
|
||||
dummy.other_method = mock.Mock('other_method', return_value=43)
|
||||
return dummy
|
||||
|
||||
|
||||
class FindResourceTest(test_utils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(FindResourceTest, self).setUp()
|
||||
|
||||
self.overcloud1 = mock.Mock()
|
||||
self.overcloud1.id = '1'
|
||||
self.overcloud1.name = 'My Overcloud 1'
|
||||
|
||||
self.overcloud2 = mock.Mock()
|
||||
self.overcloud2.id = '2'
|
||||
self.overcloud2.name = 'My Overcloud 2'
|
||||
|
||||
self.overcloud3 = mock.Mock()
|
||||
self.overcloud3.id = '3'
|
||||
self.overcloud3.name = 'My Overcloud 2'
|
||||
|
||||
self.manager = mock.Mock()
|
||||
self.manager.resource_class = None
|
||||
self.manager.get.return_value = self.overcloud1
|
||||
self.manager.resource_class = mock.Mock(__name__='Overcloud',
|
||||
NAME_ATTR='name')
|
||||
self.manager.list.return_value = [
|
||||
self.overcloud1,
|
||||
self.overcloud2,
|
||||
self.overcloud3]
|
||||
|
||||
def test_with_id(self):
|
||||
overcloud = utils.find_resource(self.manager, '1')
|
||||
self.manager.get.assert_called_with(1)
|
||||
self.assertEqual(self.overcloud1, overcloud)
|
||||
|
||||
def test_with_name(self):
|
||||
overcloud = utils.find_resource(self.manager, 'My Overcloud 1')
|
||||
self.assertEqual(self.overcloud1, overcloud)
|
||||
|
||||
def test_no_match(self):
|
||||
self.assertRaises(exc.CommandError,
|
||||
utils.find_resource,
|
||||
self.manager,
|
||||
'My Overcloud 3')
|
||||
|
||||
def test_multiple_matches(self):
|
||||
self.assertRaises(exc.CommandError,
|
||||
utils.find_resource,
|
||||
self.manager,
|
||||
'My Overcloud 2')
|
||||
|
||||
|
||||
class ParseCLIArgsTest(test_utils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ParseCLIArgsTest, self).setUp()
|
||||
|
||||
self.mock_role1 = mock.Mock()
|
||||
self.mock_role1.configure_mock(name="role1", version=1)
|
||||
|
||||
self.mock_role2 = mock.Mock()
|
||||
self.mock_role2.configure_mock(name="role2", version=2)
|
||||
|
||||
self.roles = [self.mock_role1, self.mock_role2]
|
||||
|
||||
def test_parameters_args_to_patch(self):
|
||||
|
||||
args = [
|
||||
"parameter1=value1",
|
||||
"parameter2=value2",
|
||||
]
|
||||
|
||||
result = utils.parameters_args_to_patch(args)
|
||||
|
||||
self.assertEqual([
|
||||
{'name': 'parameter1', 'value': 'value1'},
|
||||
{'name': 'parameter2', 'value': 'value2'},
|
||||
], result)
|
||||
|
||||
def test_flavors_args_to_patch(self):
|
||||
|
||||
args = [
|
||||
"role1-1=flavor1",
|
||||
"role2-2=flavor2",
|
||||
]
|
||||
|
||||
result = utils.args_to_patch(args, self.roles, "Flavor")
|
||||
|
||||
self.assertEqual([
|
||||
{'name': 'role1-1::Flavor', 'value': 'flavor1'},
|
||||
{'name': 'role2-2::Flavor', 'value': 'flavor2'}
|
||||
], result)
|
||||
|
||||
def test_scale_args_to_patch(self):
|
||||
|
||||
args = [
|
||||
"role1-1=1",
|
||||
"role2-2=2",
|
||||
]
|
||||
|
||||
result = utils.args_to_patch(args, self.roles, "count")
|
||||
|
||||
self.assertEqual([
|
||||
{'name': 'role1-1::count', 'value': '1'},
|
||||
{'name': 'role2-2::count', 'value': '2'}
|
||||
], result)
|
@ -1,101 +0,0 @@
|
||||
# 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 tuskarclient.tests.utils as tutils
|
||||
|
||||
|
||||
class HelpCommandTest(tutils.CommandTestCase):
|
||||
pass
|
||||
|
||||
tests = [
|
||||
# help
|
||||
{
|
||||
'commands': ['help'], # commands to test "tuskar help"
|
||||
# helps find failed tests in code - needs "test_" prefix
|
||||
'test_identifiers': ['test_help'],
|
||||
'out_includes': [ # what should be in output
|
||||
'usage:',
|
||||
'positional arguments:',
|
||||
'optional arguments:',
|
||||
],
|
||||
'out_excludes': [ # what should not be in output
|
||||
'foo bar baz',
|
||||
],
|
||||
'err_string': '', # how error output should look like
|
||||
'return_code': 0,
|
||||
},
|
||||
{
|
||||
'commands': ['help -h', 'help --help', 'help help'],
|
||||
'test_identifiers': ['test_help_dash_h',
|
||||
'test_help_dashdash_help',
|
||||
'test_help_help'],
|
||||
'out_includes': [
|
||||
'usage:',
|
||||
'positional arguments:',
|
||||
'optional arguments:',
|
||||
'Display help for <subcommand>',
|
||||
],
|
||||
'out_excludes': [
|
||||
'flavor-list',
|
||||
'--os-username OS_USERNAME',
|
||||
],
|
||||
'err_string': '',
|
||||
'return_code': 0,
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
|
||||
def create_test_method(command, expected_values):
|
||||
def test_command_method(self):
|
||||
self.assertThat(
|
||||
self.run_tuskar("--tuskar-api-version 2 %s" % command),
|
||||
tutils.CommandOutputMatches(
|
||||
out_str=expected_values.get('out_string'),
|
||||
out_inc=expected_values.get('out_includes'),
|
||||
out_exc=expected_values.get('out_excludes'),
|
||||
err_str=expected_values.get('err_string'),
|
||||
err_inc=expected_values.get('err_includes'),
|
||||
err_exc=expected_values.get('err_excludes'),
|
||||
))
|
||||
return test_command_method
|
||||
|
||||
# Create a method for each command found in the above tests. The tests will be
|
||||
# constructed on the HelpCommandTest class with the name given in the test
|
||||
# identifiers. This way the developer can search the above structure for the
|
||||
# identifier and find the actual data used in the test.
|
||||
duplicated = []
|
||||
|
||||
for test in tests:
|
||||
commands = test.get('commands')
|
||||
for index, command in enumerate(commands):
|
||||
test_command_method = create_test_method(command, test)
|
||||
test_command_method.__name__ = test.get('test_identifiers')[index]
|
||||
|
||||
if hasattr(HelpCommandTest, test_command_method.__name__):
|
||||
duplicated.append(test_command_method.__name__)
|
||||
|
||||
setattr(HelpCommandTest,
|
||||
test_command_method.__name__,
|
||||
test_command_method)
|
||||
|
||||
|
||||
# Finally add a meta test to verify that no test identifiers were used twice
|
||||
# which would result in only the last test being added.
|
||||
def _meta_verify_test_builder(self):
|
||||
self.assertEqual(
|
||||
duplicated, [], "Expected no test identifiers to be "
|
||||
"duplicated but found {0}".format(len(duplicated))
|
||||
)
|
||||
|
||||
setattr(HelpCommandTest, 'test_test_builder_for_duplicates',
|
||||
_meta_verify_test_builder)
|
@ -1,42 +0,0 @@
|
||||
# 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 unittest
|
||||
|
||||
import mock
|
||||
|
||||
from tuskarclient.osc import plugin
|
||||
|
||||
|
||||
class TestManagementPlugin(unittest.TestCase):
|
||||
|
||||
@mock.patch("tuskarclient.client.get_client")
|
||||
def test_make_client(self, mock_get_client):
|
||||
|
||||
mock_instance = mock.Mock()
|
||||
mock_instance._api_version = {'management': 2}
|
||||
mock_instance.get_endpoint_for_service_type.return_value = "ENDPOINT"
|
||||
mock_instance.auth.get_token.return_value = "TOKEN"
|
||||
|
||||
plugin.make_client(mock_instance)
|
||||
|
||||
mock_instance.get_endpoint_for_service_type.assert_called_with(
|
||||
'management', region_name=mock_instance._region_name)
|
||||
mock_instance.auth.get_token.assert_called_with(mock_instance.session)
|
||||
|
||||
mock_get_client.assert_called_with(
|
||||
2,
|
||||
username=mock_instance._username,
|
||||
password=mock_instance._password,
|
||||
tuskar_url="ENDPOINT",
|
||||
os_auth_token="TOKEN"
|
||||
)
|
@ -1,44 +0,0 @@
|
||||
# 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 mock
|
||||
from openstackclient.tests import utils
|
||||
|
||||
|
||||
def _create_mock(**kwargs):
|
||||
mock_plan = mock.Mock()
|
||||
mock_plan.configure_mock(**kwargs)
|
||||
mock_plan.to_dict.return_value = kwargs
|
||||
return mock_plan
|
||||
|
||||
|
||||
mock_roles = [
|
||||
_create_mock(uuid="UUID1", name="Role 1 Name", version=1,
|
||||
description="Mock Role 1"),
|
||||
_create_mock(uuid="UUID2", name="Role 2 Name", version=2,
|
||||
description="Mock Role 2"),
|
||||
]
|
||||
|
||||
mock_plans = [
|
||||
_create_mock(uuid="UUID1", name="Plan 1 Name", description="Plan 1",
|
||||
roles=mock_roles),
|
||||
_create_mock(uuid="UUID2", name="Plan 2 Name", description="Plan 2",
|
||||
roles=[])
|
||||
]
|
||||
|
||||
|
||||
class TestManagement(utils.TestCommand):
|
||||
|
||||
def setUp(self):
|
||||
super(TestManagement, self).setUp()
|
||||
|
||||
self.app.client_manager.management = mock.Mock()
|
@ -1,340 +0,0 @@
|
||||
# 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 tempfile
|
||||
|
||||
from tuskarclient.osc.v2 import plan
|
||||
from tuskarclient.tests.osc.v2 import fakes
|
||||
|
||||
|
||||
class TestPlans(fakes.TestManagement):
|
||||
|
||||
def setUp(self):
|
||||
super(TestPlans, self).setUp()
|
||||
|
||||
self.management_mock = self.app.client_manager.management
|
||||
self.management_mock.reset_mock()
|
||||
|
||||
|
||||
class TestCreateManagementPlan(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestCreateManagementPlan, self).setUp()
|
||||
self.cmd = plan.CreateManagementPlan(self.app, None)
|
||||
|
||||
def test_create_plan(self):
|
||||
arglist = ["Plan 2 Name", '-d', 'Plan 2']
|
||||
verifylist = [
|
||||
('name', "Plan 2 Name"),
|
||||
('description', "Plan 2"),
|
||||
]
|
||||
|
||||
self.management_mock.plans.create.return_value = fakes.mock_plans[1]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 2', 'Plan 2 Name', [], 'UUID2')
|
||||
], list(result)
|
||||
)
|
||||
|
||||
def test_create_plan_no_description(self):
|
||||
arglist = ["Plan1Name", ]
|
||||
verifylist = [
|
||||
('name', "Plan1Name"),
|
||||
('description', None),
|
||||
]
|
||||
|
||||
self.management_mock.plans.create.return_value = fakes.mock_plans[0]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 1', 'Plan 1 Name', fakes.mock_roles, 'UUID1')
|
||||
], list(result))
|
||||
|
||||
|
||||
class TestDeleteManagementPlan(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDeleteManagementPlan, self).setUp()
|
||||
self.cmd = plan.DeleteManagementPlan(self.app, None)
|
||||
|
||||
def test_delete_plan(self):
|
||||
arglist = ['UUID1', ]
|
||||
verifylist = [
|
||||
('plan_uuid', "UUID1"),
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
self.management_mock.plans.delete.assert_called_with('UUID1')
|
||||
|
||||
|
||||
class TestListManagementPlan(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestListManagementPlan, self).setUp()
|
||||
self.cmd = plan.ListManagementPlans(self.app, None)
|
||||
|
||||
def test_list_plans(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.management_mock.plans.list.return_value = fakes.mock_plans
|
||||
|
||||
titles, rows = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(titles, ('uuid', 'name', 'description', 'roles'))
|
||||
self.assertEqual([
|
||||
('UUID1', 'Plan 1 Name', 'Plan 1', 'Role 1 Name, Role 2 Name'),
|
||||
('UUID2', 'Plan 2 Name', 'Plan 2', '')
|
||||
], list(rows))
|
||||
|
||||
|
||||
class TestSetManagementPlan(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSetManagementPlan, self).setUp()
|
||||
self.cmd = plan.SetManagementPlan(self.app, None)
|
||||
|
||||
def test_update_plan_nothing(self):
|
||||
arglist = ['UUID1', ]
|
||||
verifylist = [
|
||||
('plan_uuid', "UUID1"),
|
||||
('parameters', None),
|
||||
('flavors', None),
|
||||
('scales', None),
|
||||
]
|
||||
|
||||
self.management_mock.plans.get.return_value = fakes.mock_plans[1]
|
||||
self.management_mock.plans.patch.return_value = fakes.mock_plans[1]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
self.management_mock.plans.patch.assert_not_called()
|
||||
|
||||
def test_update_plan_parameters(self):
|
||||
arglist = ['UUID1', '-P', 'A=1', '-P', 'B=2']
|
||||
verifylist = [
|
||||
('plan_uuid', "UUID1"),
|
||||
('parameters', ['A=1', 'B=2']),
|
||||
('flavors', None),
|
||||
('scales', None),
|
||||
]
|
||||
|
||||
self.management_mock.plans.get.return_value = fakes.mock_plans[1]
|
||||
self.management_mock.plans.patch.return_value = fakes.mock_plans[1]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 2', 'Plan 2 Name', [], 'UUID2')
|
||||
], list(result))
|
||||
|
||||
self.management_mock.plans.patch.assert_called_with('UUID1', [
|
||||
{'value': '1', 'name': 'A'},
|
||||
{'value': '2', 'name': 'B'}
|
||||
])
|
||||
|
||||
def test_update_plan_flavors(self):
|
||||
arglist = ['UUID1', '-F', 'Role 1 Name-1=strawberry',
|
||||
'-F', 'Role 2 Name-2=cherry']
|
||||
verifylist = [
|
||||
('plan_uuid', "UUID1"),
|
||||
('parameters', None),
|
||||
('flavors', ['Role 1 Name-1=strawberry', 'Role 2 Name-2=cherry']),
|
||||
('scales', None),
|
||||
]
|
||||
|
||||
self.management_mock.plans.get.return_value = fakes.mock_plans[0]
|
||||
self.management_mock.plans.patch.return_value = fakes.mock_plans[1]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 2', 'Plan 2 Name', [], 'UUID2')
|
||||
], list(result))
|
||||
|
||||
self.management_mock.plans.patch.assert_called_with('UUID1', [
|
||||
{'value': 'strawberry', 'name': 'Role 1 Name-1::Flavor'},
|
||||
{'value': 'cherry', 'name': 'Role 2 Name-2::Flavor'}
|
||||
])
|
||||
|
||||
def test_update_plan_scale(self):
|
||||
arglist = ['UUID1', '-S', 'Role 1 Name-1=2', '-S', 'Role 2 Name-2=3']
|
||||
verifylist = [
|
||||
('plan_uuid', "UUID1"),
|
||||
('parameters', None),
|
||||
('flavors', None),
|
||||
('scales', ['Role 1 Name-1=2', 'Role 2 Name-2=3']),
|
||||
]
|
||||
|
||||
self.management_mock.plans.get.return_value = fakes.mock_plans[0]
|
||||
self.management_mock.plans.patch.return_value = fakes.mock_plans[1]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 2', 'Plan 2 Name', [], 'UUID2')
|
||||
], list(result))
|
||||
|
||||
self.management_mock.plans.patch.assert_called_with('UUID1', [
|
||||
{'value': '2', 'name': 'Role 1 Name-1::count'},
|
||||
{'value': '3', 'name': 'Role 2 Name-2::count'}
|
||||
])
|
||||
|
||||
|
||||
class TestShowManagementPlan(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestShowManagementPlan, self).setUp()
|
||||
self.cmd = plan.ShowManagementPlan(self.app, None)
|
||||
|
||||
def test_show_plan(self):
|
||||
arglist = ['UUID2', ]
|
||||
verifylist = [
|
||||
('long', False),
|
||||
]
|
||||
|
||||
self.management_mock.plans.get.return_value = fakes.mock_plans[0]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 1', 'Plan 1 Name', 'Role 1 Name, Role 2 Name', 'UUID1')
|
||||
], list(result))
|
||||
|
||||
def test_show_plan_verbose(self):
|
||||
arglist = ['UUID1', '--long']
|
||||
verifylist = [
|
||||
('long', True),
|
||||
]
|
||||
|
||||
self.management_mock.plans.get.return_value = fakes.mock_plans[1]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 2', 'Plan 2 Name', [], 'UUID2')
|
||||
], list(result))
|
||||
|
||||
|
||||
class TestAddManagementPlanRole(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestAddManagementPlanRole, self).setUp()
|
||||
self.cmd = plan.AddManagementPlanRole(self.app, None)
|
||||
|
||||
def test_add_plan_role(self):
|
||||
arglist = ['UUID1', 'UUID2']
|
||||
verifylist = [
|
||||
('plan_uuid', 'UUID1'),
|
||||
('role_uuid', 'UUID2'),
|
||||
]
|
||||
|
||||
self.management_mock.plans.add_role.return_value = fakes.mock_plans[0]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 1', 'Plan 1 Name', fakes.mock_roles, 'UUID1')
|
||||
], list(result))
|
||||
|
||||
|
||||
class TestRemoveManagementPlanRole(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRemoveManagementPlanRole, self).setUp()
|
||||
self.cmd = plan.RemoveManagementPlanRole(self.app, None)
|
||||
|
||||
def test_remove_plan_role(self):
|
||||
arglist = ['UUID1', 'UUID2']
|
||||
verifylist = [
|
||||
('plan_uuid', 'UUID1'),
|
||||
('role_uuid', 'UUID2'),
|
||||
]
|
||||
|
||||
self.management_mock.plans.remove_role.return_value = (
|
||||
fakes.mock_plans[0])
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual([
|
||||
('description', 'name', 'roles', 'uuid'),
|
||||
('Plan 1', 'Plan 1 Name', fakes.mock_roles, 'UUID1')
|
||||
], list(result))
|
||||
|
||||
|
||||
class TestDownloadManagementPlan(TestPlans):
|
||||
|
||||
def setUp(self):
|
||||
super(TestDownloadManagementPlan, self).setUp()
|
||||
self.cmd = plan.DownloadManagementPlan(self.app, None)
|
||||
|
||||
def test_download_plan_templates(self):
|
||||
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
|
||||
arglist = ['UUID1', '-O', temp_dir]
|
||||
verifylist = [
|
||||
('plan_uuid', 'UUID1'),
|
||||
('output_dir', temp_dir),
|
||||
]
|
||||
|
||||
mock_result = {
|
||||
'template-1-name': 'template 1 content',
|
||||
'template-2-name': 'template 2 content',
|
||||
}
|
||||
|
||||
self.management_mock.plans.templates.return_value = mock_result
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.cmd.take_action(parsed_args)
|
||||
|
||||
for template_name in mock_result:
|
||||
full_path = os.path.join(temp_dir, template_name)
|
||||
self.assertTrue(os.path.exists(full_path))
|
@ -1,46 +0,0 @@
|
||||
# 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 tuskarclient.osc.v2 import role
|
||||
from tuskarclient.tests.osc.v2 import fakes
|
||||
|
||||
|
||||
class TestRoles(fakes.TestManagement):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRoles, self).setUp()
|
||||
|
||||
self.management_mock = self.app.client_manager.management
|
||||
self.management_mock.reset_mock()
|
||||
|
||||
|
||||
class TestRoleList(TestRoles):
|
||||
|
||||
def setUp(self):
|
||||
super(TestRoleList, self).setUp()
|
||||
self.cmd = role.ListRoles(self.app, None)
|
||||
|
||||
def test_list_roles(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.management_mock.roles.list.return_value = fakes.mock_roles
|
||||
|
||||
titles, rows = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(titles, ('uuid', 'name', 'version', 'description'))
|
||||
self.assertEqual([
|
||||
('UUID1', 'Role 1 Name', 1, 'Mock Role 1'),
|
||||
('UUID2', 'Role 2 Name', 2, 'Mock Role 2')
|
||||
], list(rows))
|
@ -1,90 +0,0 @@
|
||||
# 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 mock
|
||||
|
||||
from tuskarclient import client
|
||||
from tuskarclient.common import auth
|
||||
from tuskarclient.openstack.common.apiclient import client as apiclient
|
||||
from tuskarclient.tests import utils as tutils
|
||||
|
||||
|
||||
class ClientGetClientTest(tutils.TestCase):
|
||||
def setUp(self):
|
||||
super(ClientGetClientTest, self).setUp()
|
||||
self.kwargs = {
|
||||
'os_username': 'os_username',
|
||||
'os_password': 'os_password',
|
||||
'os_tenant_name': 'os_tenant_name',
|
||||
'os_auth_token': 'os_auth_token',
|
||||
'os_auth_url': 'os_auth_url',
|
||||
'tuskar_url': 'tuskar_url',
|
||||
'os_cacert': 'os_cacert',
|
||||
'os_cert': 'os_cert',
|
||||
'os_key': 'os_key',
|
||||
}
|
||||
self.client_kwargs = {
|
||||
'username': 'os_username',
|
||||
'password': 'os_password',
|
||||
'tenant_name': 'os_tenant_name',
|
||||
'token': 'os_auth_token',
|
||||
'auth_url': 'os_auth_url',
|
||||
'endpoint': 'tuskar_url',
|
||||
'cacert': 'os_cacert',
|
||||
'cert': 'os_cert',
|
||||
'key': 'os_key',
|
||||
}
|
||||
self.api_version = 2
|
||||
|
||||
@mock.patch.object(client, 'Client')
|
||||
def test_it_works(self, mocked_Client):
|
||||
mocked_Client.return_value = 'client'
|
||||
self.assertEqual(client.get_client(self.api_version, **self.kwargs),
|
||||
'client')
|
||||
mocked_Client.assert_called_with(self.api_version,
|
||||
**self.client_kwargs)
|
||||
|
||||
@mock.patch.object(client, 'Client')
|
||||
def test_it_raises_error_without_proper_params(
|
||||
self,
|
||||
mocked_Client):
|
||||
|
||||
mocked_Client.return_value = None
|
||||
kwargs = self.kwargs.copy()
|
||||
del kwargs['os_password']
|
||||
self.assertRaises(ValueError,
|
||||
client.get_client, self.api_version, **self.kwargs
|
||||
)
|
||||
mocked_Client.assert_called_with(self.api_version,
|
||||
**self.client_kwargs)
|
||||
|
||||
|
||||
class ClientClientTest(tutils.TestCase):
|
||||
def setUp(self):
|
||||
super(ClientClientTest, self).setUp()
|
||||
self.client_kwargs = {
|
||||
'username': 'os_username',
|
||||
'password': 'os_password',
|
||||
'tenant_name': 'os_tenant_name',
|
||||
'token': 'os_auth_token',
|
||||
'auth_url': 'os_auth_url',
|
||||
'endpoint': 'tuskar_url'
|
||||
}
|
||||
self.api_version = 2
|
||||
|
||||
@mock.patch.object(apiclient, 'HTTPClient')
|
||||
@mock.patch.object(auth, 'KeystoneAuthPlugin')
|
||||
def test_client_initialization(self, mocked_KeystoneAuthPlugin,
|
||||
mocked_HTTPClient):
|
||||
client.Client(self.api_version, **self.client_kwargs)
|
||||
mocked_KeystoneAuthPlugin.assert_called_with(**self.client_kwargs)
|
||||
mocked_HTTPClient.assert_called_with(mocked_KeystoneAuthPlugin())
|
@ -1,71 +0,0 @@
|
||||
# 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 tuskarclient.openstack.common.apiclient import exceptions as exc
|
||||
from tuskarclient import shell
|
||||
import tuskarclient.tests.utils as tutils
|
||||
|
||||
|
||||
class ShellTest(tutils.TestCase):
|
||||
|
||||
args_attributes = [
|
||||
'os_username', 'os_password', 'os_tenant_name', 'os_tenant_id',
|
||||
'os_auth_url', 'os_auth_token', 'tuskar_url', 'tuskar_api_version',
|
||||
'os_cacert', 'os_cert', 'os_key',
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
super(ShellTest, self).setUp()
|
||||
self.s = shell.TuskarShell([])
|
||||
|
||||
def empty_args(self):
|
||||
args = lambda: None # i'd use object(), but it can't have attributes
|
||||
for attr in self.args_attributes:
|
||||
setattr(args, attr, None)
|
||||
|
||||
return args
|
||||
|
||||
def test_ensure_auth_info_with_credentials(self):
|
||||
ensure = self.s._ensure_auth_info
|
||||
command_error = exc.CommandError
|
||||
args = self.empty_args()
|
||||
|
||||
args.os_username = 'user'
|
||||
args.os_password = 'pass'
|
||||
args.os_tenant_name = 'tenant'
|
||||
self.assertRaises(command_error, ensure, args)
|
||||
|
||||
args.os_auth_url = 'keystone'
|
||||
ensure(args) # doesn't raise
|
||||
|
||||
def test_ensure_auth_info_with_token(self):
|
||||
ensure = self.s._ensure_auth_info
|
||||
command_error = exc.CommandError
|
||||
args = self.empty_args()
|
||||
|
||||
args.os_auth_token = 'token'
|
||||
self.assertRaises(command_error, ensure, args)
|
||||
|
||||
args.tuskar_url = 'tuskar'
|
||||
ensure(args) # doesn't raise
|
||||
|
||||
def test_parser_v2(self):
|
||||
v2_commands = [
|
||||
]
|
||||
parser, subparsers = self.s._parser(2)
|
||||
tuskar_help = parser.format_help()
|
||||
|
||||
for arg in map(lambda a: a.replace('_', '-'), self.args_attributes):
|
||||
self.assertIn(arg, tuskar_help)
|
||||
|
||||
for command in v2_commands:
|
||||
self.assertIn(command, tuskar_help)
|
@ -1,327 +0,0 @@
|
||||
# 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 argparse
|
||||
from gettext import gettext as _
|
||||
import os
|
||||
import sys
|
||||
|
||||
import fixtures
|
||||
from six import StringIO
|
||||
import testtools
|
||||
|
||||
from tuskarclient import shell
|
||||
|
||||
|
||||
class TestCase(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestCase, self).setUp()
|
||||
if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or
|
||||
os.environ.get('OS_STDOUT_CAPTURE') == '1'):
|
||||
stdout = self.useFixture(fixtures.StringStream('stdout')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout))
|
||||
if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or
|
||||
os.environ.get('OS_STDERR_CAPTURE') == '1'):
|
||||
stderr = self.useFixture(fixtures.StringStream('stderr')).stream
|
||||
self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr))
|
||||
|
||||
|
||||
class HasManager(object):
|
||||
|
||||
def __init__(self, cls_name, attr_name):
|
||||
self.cls_name = cls_name
|
||||
self.attr_name = attr_name
|
||||
|
||||
def match(self, client):
|
||||
if not hasattr(client, self.attr_name):
|
||||
return ManagerClassMismatch(client, self.cls_name, self.attr_name)
|
||||
|
||||
obj = getattr(client, self.attr_name)
|
||||
if self.cls_name != obj.__class__.__name__:
|
||||
return ManagerClassMismatch(client, self.cls_name, self.attr_name)
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class ManagerClassMismatch(object):
|
||||
|
||||
def __init__(self, client, cls_name, attr_name):
|
||||
self.client = client
|
||||
self.cls_name = cls_name
|
||||
self.attr_name = attr_name
|
||||
|
||||
def describe(self):
|
||||
return "Class %r mismatch for attribute %r on %r" % (
|
||||
self.cls_name, self.attr_name, self.client)
|
||||
|
||||
def get_details(self):
|
||||
return {}
|
||||
|
||||
|
||||
class IsMethodOn(object):
|
||||
"""Match if there is method with same name on object."""
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
|
||||
def __str__(self):
|
||||
return 'IsMethodOn(%s)' % (self.obj)
|
||||
|
||||
def match(self, method_name):
|
||||
result = hasattr(self.obj, method_name)
|
||||
if result:
|
||||
return None
|
||||
else:
|
||||
return testtools.matchers.Mismatch("%s is not a method on %s" %
|
||||
(method_name, self.obj))
|
||||
|
||||
|
||||
class CommandTestCase(TestCase):
|
||||
def setUp(self):
|
||||
super(CommandTestCase, self).setUp()
|
||||
self.tuskar_bin = os.path.join(
|
||||
os.path.dirname(os.path.realpath(sys.executable)),
|
||||
'tuskar')
|
||||
|
||||
def run_tuskar(self, params=''):
|
||||
args = params.split()
|
||||
out = StringIO()
|
||||
err = StringIO()
|
||||
ArgumentParserForTests.OUT = out
|
||||
ArgumentParserForTests.ERR = err
|
||||
try:
|
||||
shell.TuskarShell(
|
||||
args, argument_parser_class=ArgumentParserForTests).run()
|
||||
except TestExit:
|
||||
pass
|
||||
outvalue = out.getvalue()
|
||||
errvalue = err.getvalue()
|
||||
return [outvalue, errvalue]
|
||||
|
||||
|
||||
class CommandOutputMatches(object):
|
||||
def __init__(self,
|
||||
out_str=None, out_inc=None, out_exc=None,
|
||||
err_str=None, err_inc=None, err_exc=None,
|
||||
return_code=None):
|
||||
self.out_str = out_str
|
||||
self.out_inc = out_inc or []
|
||||
self.out_exc = out_exc or []
|
||||
self.err_str = err_str
|
||||
self.err_inc = err_inc or []
|
||||
self.err_exc = err_exc or []
|
||||
self.return_code = return_code
|
||||
|
||||
def match(self, outputs):
|
||||
out, err = outputs[0], outputs[1]
|
||||
errors = []
|
||||
|
||||
# tests for exact output and error output match
|
||||
errors.append(self.match_output(out, self.out_str, type='output'))
|
||||
errors.append(self.match_output(err, self.err_str, type='error'))
|
||||
|
||||
# tests for what output should include and what it should not
|
||||
errors.append(self.match_includes(out, self.out_inc, type='output'))
|
||||
errors.append(self.match_excludes(out, self.out_exc, type='output'))
|
||||
|
||||
# tests for what error output should include and what it should not
|
||||
errors.append(self.match_includes(err, self.err_inc, type='error'))
|
||||
errors.append(self.match_excludes(err, self.err_exc, type='error'))
|
||||
|
||||
# get first non None item or None if none is found and return it
|
||||
return next((item for item in errors if item is not None), None)
|
||||
|
||||
def match_return_code(self, return_code, expected_return_code):
|
||||
if expected_return_code is not None:
|
||||
if expected_return_code != return_code:
|
||||
return CommandOutputReturnCodeMismatch(
|
||||
return_code, expected_return_code)
|
||||
|
||||
def match_output(self, output, expected_output, type='output'):
|
||||
if expected_output is not None:
|
||||
if expected_output != output:
|
||||
return CommandOutputMismatch(
|
||||
output, expected_output, type=type)
|
||||
|
||||
def match_includes(self, output, includes, type='output'):
|
||||
for part in includes:
|
||||
if part not in output:
|
||||
return CommandOutputMissingMismatch(output, part, type=type)
|
||||
|
||||
def match_excludes(self, output, excludes, type='error'):
|
||||
for part in excludes:
|
||||
if part in output:
|
||||
return CommandOutputExtraMismatch(output, part, type=type)
|
||||
|
||||
|
||||
class CommandOutputMismatch(object):
|
||||
def __init__(self, out, out_str, type='output'):
|
||||
if type == 'error':
|
||||
self.type = 'Error output'
|
||||
else:
|
||||
self.type = 'Output'
|
||||
self.out = out
|
||||
self.out_str = out_str
|
||||
|
||||
def describe(self):
|
||||
return "%s '%s' should be '%s'" % (self.type, self.out, self.out_str)
|
||||
|
||||
def get_details(self):
|
||||
return {}
|
||||
|
||||
|
||||
class CommandOutputMissingMismatch(object):
|
||||
def __init__(self, out, out_inc, type='output'):
|
||||
if type == 'error':
|
||||
self.type = 'Error output'
|
||||
else:
|
||||
self.type = 'Output'
|
||||
self.out = out
|
||||
self.out_inc = out_inc
|
||||
|
||||
def describe(self):
|
||||
return "%s '%s' should contain '%s'" % (
|
||||
self.type, self.out, self.out_inc)
|
||||
|
||||
def get_details(self):
|
||||
return {}
|
||||
|
||||
|
||||
class CommandOutputExtraMismatch(object):
|
||||
def __init__(self, out, out_exc, type='output'):
|
||||
if type == 'error':
|
||||
self.type = 'Error output'
|
||||
else:
|
||||
self.type = 'Output'
|
||||
self.out = out
|
||||
self.out_exc = out_exc
|
||||
|
||||
def describe(self):
|
||||
return "%s '%s' should not contain '%s'" % (
|
||||
self.type, self.out, self.out_exc)
|
||||
|
||||
def get_details(self):
|
||||
return {}
|
||||
|
||||
|
||||
class CommandOutputReturnCodeMismatch(object):
|
||||
def __init__(self, ret, ret_exp):
|
||||
self.ret = ret
|
||||
self.ret_exp = ret_exp
|
||||
|
||||
def describe(self):
|
||||
return "Return code is '%s' but expected '%s'" % (
|
||||
self.ret, self.ret_exp)
|
||||
|
||||
def get_details(self):
|
||||
return {}
|
||||
|
||||
|
||||
class TestExit(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ArgumentParserForTests(argparse.ArgumentParser):
|
||||
OUT = sys.stdout
|
||||
ERR = sys.stderr
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.out = ArgumentParserForTests.OUT
|
||||
self.err = ArgumentParserForTests.ERR
|
||||
|
||||
super(ArgumentParserForTests, self).__init__(**kwargs)
|
||||
|
||||
def error(self, message):
|
||||
self.print_usage(self.err)
|
||||
self.exit(2, _('%(prog)s: error: %(message)s\n') %
|
||||
{'prog': self.prog, 'message': message})
|
||||
|
||||
def exit(self, status=0, message=None):
|
||||
if message:
|
||||
self._print_message(message, self.err)
|
||||
raise TestExit
|
||||
|
||||
def print_usage(self, file=None):
|
||||
if file is None:
|
||||
file = self.out
|
||||
self._print_message(self.format_usage(), file)
|
||||
|
||||
def print_help(self, file=None):
|
||||
if file is None:
|
||||
file = self.out
|
||||
self._print_message(self.format_help(), file)
|
||||
|
||||
def print_version(self, file=None):
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'The print_version method is deprecated -- the "version" '
|
||||
'argument to ArgumentParser is no longer supported.',
|
||||
DeprecationWarning)
|
||||
self._print_message(self.format_version(), file)
|
||||
|
||||
def _print_message(self, message, file=None):
|
||||
if message:
|
||||
if file is None:
|
||||
file = self.err
|
||||
file.write(message)
|
||||
|
||||
|
||||
def create_test_dictionary_pair(default_keys, redundant_keys, missing_keys,
|
||||
**kwargs):
|
||||
"""Creates a pair of dictionaries for testing
|
||||
|
||||
This function creates two dictionaries from three sets of keys.
|
||||
|
||||
The first returned dictionary contains keys from default_keys,
|
||||
keys from redundant_keys but is missing keys from missing_keys.
|
||||
All with value of key + '_value'.
|
||||
|
||||
The second returned dictionary contains keys from default_keys
|
||||
with value of key + '_value' except for keys from missing_keys.
|
||||
These contains value None.
|
||||
|
||||
These two dictionaries can be used in test cases when testing
|
||||
if tested function filters out set of keys from kwargs
|
||||
and passes it to other function.
|
||||
|
||||
:param default_keys: set of keys expected to be passed on
|
||||
:param redundant_keys: set of keys expected to be filtered out
|
||||
:param missing_keys: set of keys missing from passed_dictionary
|
||||
and expected to be set to None
|
||||
:param kwargs: key translation pairs. original=new_one will create
|
||||
original='original_value' in passed_dictionary and
|
||||
new_one='original_value' in called_dictionary.
|
||||
"""
|
||||
passed_dictionary = {}
|
||||
translations = kwargs
|
||||
|
||||
for key in default_keys | redundant_keys:
|
||||
if key not in missing_keys:
|
||||
passed_dictionary[key] = key + '_value'
|
||||
|
||||
called_dictionary = passed_dictionary.copy()
|
||||
|
||||
for key in redundant_keys:
|
||||
del called_dictionary[key]
|
||||
|
||||
for key in missing_keys:
|
||||
called_dictionary[key] = None
|
||||
|
||||
for key in translations:
|
||||
if key in called_dictionary:
|
||||
# create new key with name from translations dict
|
||||
# with original value
|
||||
called_dictionary[translations[key]] = called_dictionary[key]
|
||||
# delete original key
|
||||
del called_dictionary[key]
|
||||
|
||||
return passed_dictionary, called_dictionary
|
@ -1,29 +0,0 @@
|
||||
# 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 mock
|
||||
|
||||
from tuskarclient.tests import utils as tutils
|
||||
from tuskarclient.v2 import client
|
||||
|
||||
|
||||
class ClientTest(tutils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ClientTest, self).setUp()
|
||||
mock_http_client = mock.MagicMock()
|
||||
self.client = client.Client(mock_http_client)
|
||||
|
||||
def test_managers_present(self):
|
||||
self.assertThat(self.client, tutils.HasManager('PlanManager',
|
||||
'plans'))
|
||||
self.assertThat(self.client, tutils.HasManager('RoleManager',
|
||||
'roles'))
|
@ -1,147 +0,0 @@
|
||||
# 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 mock
|
||||
|
||||
import tuskarclient.tests.utils as tutils
|
||||
from tuskarclient.v2 import plans
|
||||
from tuskarclient.v2 import roles
|
||||
|
||||
|
||||
class PlanManagerTest(tutils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Create a mock API object and bind to the PlanManager manager.
|
||||
"""
|
||||
super(PlanManagerTest, self).setUp()
|
||||
self.api = mock.Mock()
|
||||
self.pm = plans.PlanManager(self.api)
|
||||
|
||||
def test_get(self):
|
||||
"""Test a standard GET operation to read/retrieve the plan."""
|
||||
self.assertThat('_get', tutils.IsMethodOn(self.pm))
|
||||
self.pm._get = mock.Mock(return_value='fake_plan')
|
||||
|
||||
self.assertEqual(self.pm.get('fake_plan'), 'fake_plan')
|
||||
self.pm._get.assert_called_with('/plans/fake_plan')
|
||||
|
||||
def test_get_404(self):
|
||||
"""Test a 404 response to a standard GET."""
|
||||
self.assertThat('_get', tutils.IsMethodOn(self.pm))
|
||||
self.pm._get = mock.Mock(return_value=None)
|
||||
|
||||
self.assertEqual(self.pm.get('fake_plan'), None)
|
||||
self.pm._get.assert_called_with('/plans/fake_plan')
|
||||
|
||||
def test_list(self):
|
||||
"""Test retrieving a list of plans via GET."""
|
||||
self.assertThat('_list', tutils.IsMethodOn(self.pm))
|
||||
self.pm._list = mock.Mock(return_value=['fake_plan'])
|
||||
|
||||
self.assertEqual(self.pm.list(), ['fake_plan'])
|
||||
self.pm._list.assert_called_with('/plans')
|
||||
|
||||
def test_create(self):
|
||||
"""Test creating a new plan via POST."""
|
||||
self.assertThat('_post', tutils.IsMethodOn(self.pm))
|
||||
self.pm._post = mock.Mock(return_value=['fake_plan'])
|
||||
|
||||
self.assertEqual(
|
||||
self.pm.create(dummy='dummy plan data'),
|
||||
['fake_plan'])
|
||||
|
||||
self.pm._post.assert_called_with(
|
||||
'/plans',
|
||||
{'dummy': 'dummy plan data'})
|
||||
|
||||
def test_patch(self):
|
||||
"""Test patching a plan."""
|
||||
self.assertThat('_patch', tutils.IsMethodOn(self.pm))
|
||||
self.pm._patch = mock.Mock(return_value=['fake_plan'])
|
||||
|
||||
self.assertEqual(
|
||||
self.pm.patch('42', [{'name': 'dummy',
|
||||
'value': 'dummy plan data'}]),
|
||||
['fake_plan'])
|
||||
|
||||
self.pm._patch.assert_called_with(
|
||||
'/plans/42',
|
||||
[{'name': 'dummy',
|
||||
'value': 'dummy plan data'}])
|
||||
|
||||
def test_delete(self):
|
||||
"""Test deleting/removing an plan via DELETE."""
|
||||
self.assertThat('_delete', tutils.IsMethodOn(self.pm))
|
||||
self.pm._delete = mock.Mock(return_value=None)
|
||||
|
||||
self.assertEqual(self.pm.delete(42), None)
|
||||
self.pm._delete.assert_called_with('/plans/42')
|
||||
|
||||
def test_roles_path_with_role_id(self):
|
||||
"""Test for building path for Role using UUID."""
|
||||
self.assertEqual(self.pm._roles_path('plan_42', 'role_abc'),
|
||||
'/plans/plan_42/roles/role_abc')
|
||||
|
||||
def test_roles_path_without_role_id(self):
|
||||
"""Test for building path for Role for POST requests."""
|
||||
self.assertEqual(self.pm._roles_path('plan_42'),
|
||||
'/plans/plan_42/roles')
|
||||
|
||||
def test_add_role(self):
|
||||
"""Test assigning Role to a Plan."""
|
||||
self.assertThat('_post', tutils.IsMethodOn(self.pm))
|
||||
self.pm._post = mock.Mock(return_value='dummy plan')
|
||||
|
||||
self.assertEqual(self.pm.add_role('42', role_uuid='qwert12345'),
|
||||
'dummy plan')
|
||||
self.pm._post.assert_called_with(
|
||||
'/plans/42/roles',
|
||||
{'uuid': 'qwert12345'})
|
||||
|
||||
def test_remove_role(self):
|
||||
"""Test assigning Role to a Plan."""
|
||||
self.assertThat('delete', tutils.IsMethodOn(self.api))
|
||||
api_delete_return_mock = mock.Mock()
|
||||
self.api.delete = mock.Mock(return_value=api_delete_return_mock)
|
||||
self.assertThat('resource_class', tutils.IsMethodOn(self.pm))
|
||||
self.pm.resource_class = mock.Mock(return_value='fake_plan')
|
||||
|
||||
self.assertEqual(self.pm.remove_role('42', role_uuid='qwert12345'),
|
||||
'fake_plan')
|
||||
self.api.delete.assert_called_with('/plans/42/roles/qwert12345')
|
||||
self.pm.resource_class.assert_called_with(
|
||||
self.pm, api_delete_return_mock.json())
|
||||
|
||||
def test_templates_path(self):
|
||||
self.assertEqual(self.pm._templates_path('42'),
|
||||
'/plans/42/templates')
|
||||
|
||||
def test_templates(self):
|
||||
"""Test a GET operation to retrieve the plan's templates."""
|
||||
self.assertThat('_get', tutils.IsMethodOn(self.pm))
|
||||
self.pm._get = mock.MagicMock()
|
||||
self.pm._get.return_value.to_dict.return_value = 'fake_templates_dict'
|
||||
|
||||
self.assertEqual(self.pm.templates('fake_plan'), 'fake_templates_dict')
|
||||
self.pm._get.assert_called_with('/plans/fake_plan/templates')
|
||||
|
||||
def test_roles_subresource(self):
|
||||
self.assertThat('_get', tutils.IsMethodOn(self.pm))
|
||||
self.pm._get = mock.Mock(
|
||||
return_value=plans.Plan(None,
|
||||
{'roles': [
|
||||
{'name': 'foo_role'},
|
||||
{'name': 'bar_role'}
|
||||
]}))
|
||||
test_roles = self.pm.get('42').roles
|
||||
self.assertTrue(isinstance(test_roles, list))
|
||||
self.assertTrue(isinstance(test_roles[0], roles.Role))
|
@ -1,384 +0,0 @@
|
||||
# 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 mock
|
||||
import six
|
||||
|
||||
import tuskarclient.tests.utils as tutils
|
||||
from tuskarclient.v2 import plans_shell
|
||||
|
||||
|
||||
def empty_args():
|
||||
args = mock.Mock(spec=[])
|
||||
for attr in ['uuid', 'name', 'description', 'parameters',
|
||||
'only_empty_parameters']:
|
||||
setattr(args, attr, None)
|
||||
return args
|
||||
|
||||
|
||||
def mock_plan():
|
||||
plan = mock.Mock()
|
||||
plan.uuid = '5'
|
||||
plan.name = 'My Plan'
|
||||
plan.parameters = []
|
||||
plan.parameters.append({'name': 'compute-1::count', 'value': '2'})
|
||||
plan.parameters.append({'name': 'compute-1::Flavor', 'value': 'baremetal'})
|
||||
plan.to_dict.return_value = {
|
||||
'uuid': 5,
|
||||
'name': 'My Plan',
|
||||
'parameters': plan.parameters,
|
||||
}
|
||||
return plan
|
||||
|
||||
|
||||
class BasePlansShellTest(tutils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BasePlansShellTest, self).setUp()
|
||||
self.outfile = six.StringIO()
|
||||
self.tuskar = mock.MagicMock()
|
||||
self.shell = plans_shell
|
||||
|
||||
|
||||
class PlansShellTest(BasePlansShellTest):
|
||||
|
||||
@mock.patch('tuskarclient.common.formatting.print_list')
|
||||
def test_plan_list(self, mock_print_list):
|
||||
args = empty_args()
|
||||
|
||||
self.shell.do_plan_list(self.tuskar, args, outfile=self.outfile)
|
||||
# testing the other arguments would be just copy-paste
|
||||
mock_print_list.assert_called_with(
|
||||
self.tuskar.plans.list.return_value, mock.ANY, mock.ANY,
|
||||
outfile=self.outfile
|
||||
)
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print_plan_summary')
|
||||
def test_plan_show(self, mock_print_summary, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
args = empty_args()
|
||||
args.plan = '5'
|
||||
args.verbose = False
|
||||
|
||||
self.shell.do_plan_show(self.tuskar, args, outfile=self.outfile)
|
||||
mock_find_resource.assert_called_with(self.tuskar.plans, '5')
|
||||
mock_print_summary.assert_called_with(mock_find_resource.return_value,
|
||||
outfile=self.outfile)
|
||||
|
||||
def test_filter_empty_parameters(self):
|
||||
parameters = [{'name': 'setup param', 'value': '1'},
|
||||
{'name': 'empty-parameter', 'value': ''},
|
||||
{'name': 'empty-parameter', 'value': None}]
|
||||
|
||||
filtered_parameters = self.shell.filter_empty_parameters(parameters)
|
||||
|
||||
self.assertEqual([{'name': 'empty-parameter', 'value': ''},
|
||||
{'name': 'empty-parameter', 'value': None}],
|
||||
filtered_parameters)
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
@mock.patch('tuskarclient.common.formatting.print_dict')
|
||||
def test_plan_show_scale(self, mock_print_dict, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
args = empty_args()
|
||||
args.plan = '5'
|
||||
|
||||
self.shell.do_plan_show_scale(self.tuskar, args, outfile=self.outfile)
|
||||
mock_find_resource.assert_called_with(self.tuskar.plans, '5')
|
||||
mock_print_dict.assert_called_with({'compute-1': '2'},
|
||||
outfile=self.outfile)
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
@mock.patch('tuskarclient.common.formatting.print_dict')
|
||||
def test_plan_show_flavors(self, mock_print_dict, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
args = empty_args()
|
||||
args.plan = '5'
|
||||
|
||||
self.shell.do_plan_show_flavors(self.tuskar, args,
|
||||
outfile=self.outfile)
|
||||
mock_find_resource.assert_called_with(self.tuskar.plans, '5')
|
||||
mock_print_dict.assert_called_with({'compute-1': 'baremetal'},
|
||||
outfile=self.outfile)
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
def test_plan_delete(self, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
args = empty_args()
|
||||
args.plan = '5'
|
||||
|
||||
self.shell.do_plan_delete(self.tuskar, args, outfile=self.outfile)
|
||||
self.tuskar.plans.delete.assert_called_with('5')
|
||||
self.assertEqual('Deleted Plan "My Plan".\n',
|
||||
self.outfile.getvalue())
|
||||
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print_plan_summary')
|
||||
def test_plan_create(self, mock_print_summary):
|
||||
args = empty_args()
|
||||
args.name = 'my_plan'
|
||||
args.description = 'my plan description'
|
||||
|
||||
self.shell.do_plan_create(self.tuskar, args, outfile=self.outfile)
|
||||
self.tuskar.plans.create.assert_called_with(
|
||||
name='my_plan',
|
||||
description='my plan description'
|
||||
)
|
||||
mock_print_summary.assert_called_with(
|
||||
self.tuskar.plans.create.return_value, outfile=self.outfile)
|
||||
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print_plan_summary')
|
||||
def test_add_role(self, mock_print_summary):
|
||||
args = empty_args()
|
||||
args.plan_uuid = '42'
|
||||
args.role_uuid = 'role_uuid'
|
||||
|
||||
self.shell.do_plan_add_role(self.tuskar, args, outfile=self.outfile)
|
||||
self.tuskar.plans.add_role.assert_called_with('42', 'role_uuid')
|
||||
|
||||
mock_print_summary.assert_called_with(
|
||||
self.tuskar.plans.add_role.return_value, outfile=self.outfile)
|
||||
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print_plan_summary')
|
||||
def test_remove_role(self, mock_print_summary):
|
||||
args = empty_args()
|
||||
args.plan_uuid = '42'
|
||||
args.role_uuid = 'role_uuid'
|
||||
|
||||
self.shell.do_plan_remove_role(self.tuskar, args, outfile=self.outfile)
|
||||
self.tuskar.plans.remove_role.assert_called_with('42', 'role_uuid')
|
||||
|
||||
mock_print_summary.assert_called_with(
|
||||
self.tuskar.plans.remove_role.return_value, outfile=self.outfile)
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
def test_plan_scale(self, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
role = mock.Mock()
|
||||
role.name = 'compute'
|
||||
role.version = 1
|
||||
self.tuskar.roles.list.return_value = [role]
|
||||
|
||||
args = empty_args()
|
||||
args.plan_uuid = 'plan_uuid'
|
||||
args.role_name = 'compute-1'
|
||||
args.count = '9'
|
||||
|
||||
parameters = [{'name': 'compute-1::count', 'value': '9'}]
|
||||
|
||||
self.shell.do_plan_scale(self.tuskar, args, outfile=self.outfile)
|
||||
self.assertEqual(self.tuskar.plans.patch.call_count, 1)
|
||||
|
||||
self.assertEqual('plan_uuid', self.tuskar.plans.patch.call_args[0][0])
|
||||
self.assertEqual(
|
||||
sorted(parameters, key=lambda k: k['name']),
|
||||
sorted(self.tuskar.plans.patch.call_args[0][1],
|
||||
key=lambda k: k['name']))
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
def test_plan_flavor(self, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
role = mock.Mock()
|
||||
role.name = 'compute'
|
||||
role.version = 1
|
||||
self.tuskar.roles.list.return_value = [role]
|
||||
|
||||
args = empty_args()
|
||||
args.plan_uuid = 'plan_uuid'
|
||||
args.role_name = 'compute-1'
|
||||
args.flavor = 'baremetalssd'
|
||||
|
||||
parameters = [{'name': 'compute-1::Flavor', 'value': 'baremetalssd'}]
|
||||
|
||||
self.shell.do_plan_flavor(self.tuskar, args, outfile=self.outfile)
|
||||
self.assertEqual(self.tuskar.plans.patch.call_count, 1)
|
||||
|
||||
self.assertEqual('plan_uuid', self.tuskar.plans.patch.call_args[0][0])
|
||||
self.assertEqual(
|
||||
sorted(parameters, key=lambda k: k['name']),
|
||||
sorted(self.tuskar.plans.patch.call_args[0][1],
|
||||
key=lambda k: k['name']))
|
||||
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print_plan_summary')
|
||||
def test_plan_patch(self, mock_print_summary):
|
||||
args = empty_args()
|
||||
args.plan_uuid = 'plan_uuid'
|
||||
args.parameters = ['foo_name=foo_value',
|
||||
'bar_name=bar_value']
|
||||
args.attributes = None
|
||||
parameters = [{'name': 'foo_name', 'value': 'foo_value'},
|
||||
{'name': 'bar_name', 'value': 'bar_value'}]
|
||||
self.shell.do_plan_patch(self.tuskar, args, outfile=self.outfile)
|
||||
self.assertEqual(self.tuskar.plans.patch.call_count, 1)
|
||||
|
||||
self.assertEqual('plan_uuid',
|
||||
self.tuskar.plans.patch.call_args[0][0])
|
||||
self.assertEqual(
|
||||
sorted(parameters, key=lambda k: k['name']),
|
||||
sorted(self.tuskar.plans.patch.call_args[0][1],
|
||||
key=lambda k: k['name']))
|
||||
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print_plan_summary')
|
||||
def test_plan_patch_deprecated(self, mock_print_summary):
|
||||
"""Test plan_patch with the deprecated --attribute flag."""
|
||||
args = empty_args()
|
||||
args.plan_uuid = 'plan_uuid'
|
||||
args.attributes = ['foo_name=foo_value',
|
||||
'bar_name=bar_value']
|
||||
args.parameters = None
|
||||
parameters = [{'name': 'foo_name', 'value': 'foo_value'},
|
||||
{'name': 'bar_name', 'value': 'bar_value'}]
|
||||
self.shell.do_plan_patch(self.tuskar, args, outfile=self.outfile)
|
||||
self.assertEqual(self.tuskar.plans.patch.call_count, 1)
|
||||
|
||||
self.assertEqual('plan_uuid',
|
||||
self.tuskar.plans.patch.call_args[0][0])
|
||||
self.assertEqual(
|
||||
sorted(parameters, key=lambda k: k['name']),
|
||||
sorted(self.tuskar.plans.patch.call_args[0][1],
|
||||
key=lambda k: k['name']))
|
||||
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print_plan_detail')
|
||||
def test_plan_update(self, mock_print_detail):
|
||||
args = empty_args()
|
||||
args.plan_uuid = 'plan_uuid'
|
||||
args.parameters = ['foo_name=foo_value',
|
||||
'bar_name=bar_value']
|
||||
parameters = [{'name': 'foo_name', 'value': 'foo_value'},
|
||||
{'name': 'bar_name', 'value': 'bar_value'}]
|
||||
args.attributes = None
|
||||
self.shell.do_plan_update(self.tuskar, args, outfile=self.outfile)
|
||||
self.assertEqual(self.tuskar.plans.patch.call_count, 1)
|
||||
|
||||
self.assertEqual('plan_uuid',
|
||||
self.tuskar.plans.patch.call_args[0][0])
|
||||
self.assertEqual(
|
||||
sorted(parameters, key=lambda k: k['name']),
|
||||
sorted(self.tuskar.plans.patch.call_args[0][1],
|
||||
key=lambda k: k['name']))
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
def test_print_plan_summary(self, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
args = empty_args()
|
||||
args.plan = '5'
|
||||
args.verbose = False
|
||||
|
||||
self.shell.do_plan_show(self.tuskar, args, outfile=self.outfile)
|
||||
mock_find_resource.assert_called_with(self.tuskar.plans, '5')
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
def test_print_plan_wrap(self, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
mock_find_resource.return_value.parameters.append(
|
||||
{'name': 'foo',
|
||||
'value': 'This is a really long parameter value with '
|
||||
'multiple lines to test the output wrapping.\n'
|
||||
'Indents is assumed to be code:\n'
|
||||
' {\n'
|
||||
' "like": "this"\n'
|
||||
' }\n'}
|
||||
)
|
||||
|
||||
args = empty_args()
|
||||
args.plan = '5'
|
||||
args.verbose = True
|
||||
|
||||
self.shell.do_plan_show(self.tuskar, args, outfile=self.outfile)
|
||||
output = self.outfile.getvalue()
|
||||
|
||||
# Lines should not be way to long:
|
||||
self.assertTrue(all(len(line) < 100 for line in output.splitlines()))
|
||||
# The lines are rewraped:
|
||||
self.assertIn("wrapping. Indents", output)
|
||||
# But not if the start with an indent:
|
||||
self.assertIn(" {", output)
|
||||
|
||||
@mock.patch('tuskarclient.common.utils.find_resource')
|
||||
def test_print_plan_detail(self, mock_find_resource):
|
||||
mock_find_resource.return_value = mock_plan()
|
||||
args = empty_args()
|
||||
args.plan = '5'
|
||||
args.verbose = True
|
||||
|
||||
self.shell.do_plan_show(self.tuskar, args, outfile=self.outfile)
|
||||
mock_find_resource.assert_called_with(self.tuskar.plans, '5')
|
||||
|
||||
def test_filter_parameters_to_dict(self):
|
||||
parameters = [{'name': 'compute-1::count', 'value': '2'}]
|
||||
self.assertEqual(
|
||||
self.shell.filter_parameters_to_dict(parameters, 'count'),
|
||||
{'compute-1': '2'}
|
||||
)
|
||||
|
||||
@mock.patch('tuskarclient.v2.plans_shell.print', create=True)
|
||||
@mock.patch('tuskarclient.v2.plans_shell.os.mkdir', create=True)
|
||||
@mock.patch('tuskarclient.v2.plans_shell.os.path.isdir', create=True)
|
||||
@mock.patch('tuskarclient.v2.plans_shell.open', create=True)
|
||||
@mock.patch('tuskarclient.v2.plans_shell.os.path.exists', create=True)
|
||||
@mock.patch('tuskarclient.v2.plans_shell.os.makedirs', create=True)
|
||||
def test_plan_templates(
|
||||
self, mock_makedirs, mock_exists, mock_open, mock_isdir,
|
||||
mock_mkdir, mock_print):
|
||||
args = empty_args()
|
||||
args.plan_uuid = 'plan_uuid'
|
||||
args.output_dir = 'outdir/subdir'
|
||||
|
||||
# Simulate the first exists check being false and the subsequent check
|
||||
# being true so as to exercise that makesdirs is only called once
|
||||
# per nested directory.
|
||||
exists_return_values = [False, True]
|
||||
|
||||
def toggle_exists_result(*e_args, **e_kwargs):
|
||||
return exists_return_values.pop(0)
|
||||
mock_exists.side_effect = toggle_exists_result
|
||||
|
||||
mock_isdir.return_value = False
|
||||
self.tuskar.plans.templates.return_value = {
|
||||
'name_foo': 'value_foo',
|
||||
'name_bar': 'value_bar',
|
||||
'nested/name_baz': 'value_baz',
|
||||
'nested/name_zom': 'value_zom'
|
||||
}
|
||||
|
||||
self.shell.do_plan_templates(self.tuskar, args, outfile=self.outfile)
|
||||
|
||||
# Initial check and creation of the output directory
|
||||
mock_isdir.assert_any_call('outdir/subdir')
|
||||
mock_mkdir.assert_any_call('outdir/subdir')
|
||||
|
||||
# Checks and creation of nested directory
|
||||
self.assertEqual(mock_exists.call_count, 2)
|
||||
self.assertEqual(mock_makedirs.call_count, 1)
|
||||
mock_makedirs.assert_called_with('outdir/subdir/nested')
|
||||
|
||||
self.tuskar.plans.templates.assert_called_with('plan_uuid')
|
||||
|
||||
mock_open.assert_any_call('outdir/subdir/name_foo', 'w+')
|
||||
mock_open.assert_any_call('outdir/subdir/name_bar', 'w+')
|
||||
mock_open.assert_any_call('outdir/subdir/nested/name_baz', 'w+')
|
||||
mock_open.assert_any_call('outdir/subdir/nested/name_zom', 'w+')
|
||||
self.assertEqual(mock_open.call_count, 4)
|
||||
|
||||
mock_opened_file = mock_open.return_value.__enter__.return_value
|
||||
mock_opened_file.write.assert_any_call('value_foo')
|
||||
mock_opened_file.write.assert_any_call('value_bar')
|
||||
mock_opened_file.write.assert_any_call('value_baz')
|
||||
mock_opened_file.write.assert_any_call('value_zom')
|
||||
self.assertEqual(mock_opened_file.write.call_count, 4)
|
||||
|
||||
mock_print.assert_any_call('The following templates will be written:')
|
||||
mock_print.assert_any_call('outdir/subdir/name_foo')
|
||||
mock_print.assert_any_call('outdir/subdir/name_bar')
|
||||
mock_print.assert_any_call('outdir/subdir/nested/name_baz')
|
||||
mock_print.assert_any_call('outdir/subdir/nested/name_zom')
|
||||
self.assertEqual(mock_print.call_count, 5)
|
@ -1,44 +0,0 @@
|
||||
# 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 mock
|
||||
|
||||
import tuskarclient.tests.utils as tutils
|
||||
from tuskarclient.v2 import roles
|
||||
|
||||
|
||||
class RoleManagerTest(tutils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
"""Create a mock API object and bind to the PlanManager manager.
|
||||
"""
|
||||
super(RoleManagerTest, self).setUp()
|
||||
self.api = mock.Mock()
|
||||
self.rm = roles.RoleManager(self.api)
|
||||
|
||||
def test_list(self):
|
||||
"""Test retrieving a list of Roles via GET."""
|
||||
self.assertThat('_list', tutils.IsMethodOn(self.rm))
|
||||
self.rm._list = mock.Mock(return_value=['fake_role'])
|
||||
|
||||
self.assertEqual(self.rm.list(), ['fake_role'])
|
||||
self.rm._list.assert_called_with('/roles')
|
||||
|
||||
def test_path_without_id(self):
|
||||
"""Test _path returns list uri."""
|
||||
self.assertEqual(self.rm._path(), '/roles')
|
||||
|
||||
def test_path_with_id(self):
|
||||
"""Test _path returns single item uri."""
|
||||
plan_id = self.getUniqueString()
|
||||
self.assertEqual(self.rm._path(plan_id),
|
||||
'/roles/%s' % plan_id)
|
@ -1,47 +0,0 @@
|
||||
# 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 mock
|
||||
import six
|
||||
|
||||
import tuskarclient.tests.utils as tutils
|
||||
from tuskarclient.v2 import roles_shell
|
||||
|
||||
|
||||
def empty_args():
|
||||
args = mock.Mock(spec=[])
|
||||
for attr in ['uuid', 'name', 'version', 'description']:
|
||||
setattr(args, attr, None)
|
||||
return args
|
||||
|
||||
|
||||
class BaseRolesShellTest(tutils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(BaseRolesShellTest, self).setUp()
|
||||
self.outfile = six.StringIO()
|
||||
self.tuskar = mock.MagicMock()
|
||||
self.shell = roles_shell
|
||||
|
||||
|
||||
class RolesShellTest(BaseRolesShellTest):
|
||||
|
||||
@mock.patch('tuskarclient.common.formatting.print_list')
|
||||
def test_role_list(self, mock_print_list):
|
||||
args = empty_args()
|
||||
|
||||
self.shell.do_role_list(self.tuskar, args, outfile=self.outfile)
|
||||
# testing the other arguments would be just copy-paste
|
||||
mock_print_list.assert_called_with(
|
||||
self.tuskar.roles.list.return_value, mock.ANY, mock.ANY,
|
||||
outfile=self.outfile
|
||||
)
|
@ -1,29 +0,0 @@
|
||||
# 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 tuskarclient.openstack.common.apiclient import client
|
||||
from tuskarclient.v2 import plans
|
||||
from tuskarclient.v2 import roles
|
||||
|
||||
|
||||
class Client(client.BaseClient):
|
||||
"""Client for the Tuskar v2 HTTP API.
|
||||
|
||||
:param string endpoint: Endpoint URL for the tuskar service.
|
||||
:param string token: Keystone authentication token.
|
||||
:param integer timeout: Timeout for client http requests. (optional)
|
||||
"""
|
||||
|
||||
def __init__(self, http_client, extensions=None):
|
||||
super(Client, self).__init__(http_client, extensions)
|
||||
self.plans = plans.PlanManager(self)
|
||||
self.roles = roles.RoleManager(self)
|
@ -1,168 +0,0 @@
|
||||
# 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 tuskarclient.openstack.common.apiclient import base
|
||||
from tuskarclient.v2 import roles
|
||||
|
||||
|
||||
class Plan(base.Resource):
|
||||
"""Represents an instance of a Plan in the Tuskar API.
|
||||
|
||||
:param manager: Manager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
super(Plan, self).__init__(manager, info, loaded=loaded)
|
||||
self.roles = [roles.Role(None, role) for role in self.roles]
|
||||
|
||||
|
||||
class Templates(base.Resource):
|
||||
"""Represents sets of templates of a Plan in the Tuskar API.
|
||||
|
||||
:param manager: Manager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
|
||||
|
||||
class PlanManager(base.BaseManager):
|
||||
"""PlanManager interacts with the Tuskar API and provides CRUD
|
||||
operations for the Plan type.
|
||||
"""
|
||||
|
||||
#: The class used to represent an Plan instance
|
||||
resource_class = Plan
|
||||
|
||||
@staticmethod
|
||||
def _path(plan_id=None):
|
||||
|
||||
if plan_id:
|
||||
return '/plans/%s' % plan_id
|
||||
|
||||
return '/plans'
|
||||
|
||||
def _roles_path(self, plan_id, role_id=None):
|
||||
roles_path = '%s/roles' % self._path(plan_id)
|
||||
|
||||
if role_id:
|
||||
return '%(roles_path)s/%(role_id)s' % {'roles_path': roles_path,
|
||||
'role_id': role_id}
|
||||
|
||||
return roles_path
|
||||
|
||||
def _templates_path(self, plan_id):
|
||||
templates_path = '%s/templates' % self._path(plan_id)
|
||||
|
||||
return templates_path
|
||||
|
||||
def get(self, plan_uuid):
|
||||
"""Get the Plan by its UUID.
|
||||
|
||||
:param plan_uuid: UUID of the Plan.
|
||||
:type plan_uuid: string
|
||||
|
||||
:return: A Plan instance or None if its not found.
|
||||
:rtype: tuskarclient.v2.plans.Plan or None
|
||||
"""
|
||||
return self._get(self._path(plan_uuid))
|
||||
|
||||
def list(self):
|
||||
"""Get a list of the existing Plans
|
||||
|
||||
:return: A list of plans or an empty list if none are found.
|
||||
:rtype: [tuskarclient.v2.plans.Plan] or []
|
||||
"""
|
||||
return self._list(self._path())
|
||||
|
||||
def create(self, **fields):
|
||||
"""Create a new Plan.
|
||||
|
||||
:param fields: A set of key/value pairs representing a Plan.
|
||||
:type fields: string
|
||||
|
||||
:return: A Plan instance or None if its not found.
|
||||
:rtype: tuskarclient.v2.plans.Plan
|
||||
"""
|
||||
return self._post(self._path(), fields)
|
||||
|
||||
def patch(self, plan_uuid, parameter_list):
|
||||
"""Patch an existing Plan.
|
||||
|
||||
:param plan_uuid: UUID of the Plan.
|
||||
:type plan_uuid: string
|
||||
|
||||
:param parameter_list: a list of parameter name/value dicts
|
||||
Example: [{'name': <attr_name>, 'value': <attr_value>}]
|
||||
:type parameter_list: list
|
||||
|
||||
:return: A Plan instance or None if its not found.
|
||||
:rtype: tuskarclient.v2.plans.Plan or None
|
||||
"""
|
||||
return self._patch(self._path(plan_uuid),
|
||||
parameter_list)
|
||||
|
||||
def delete(self, plan_uuid):
|
||||
"""Delete a Plan.
|
||||
|
||||
:param plan_uuid: uuid of the Plan.
|
||||
:type plan_uuid: string
|
||||
|
||||
:return: None
|
||||
:rtype: None
|
||||
"""
|
||||
return self._delete(self._path(plan_uuid))
|
||||
|
||||
def add_role(self, plan_uuid, role_uuid):
|
||||
"""Adds a Role to a Plan.
|
||||
|
||||
:param plan_uuid: UUID of the Plan.
|
||||
:type plan_uuid: string
|
||||
|
||||
:param role_uuid: UUID of the Role.
|
||||
:type role_uuid: string
|
||||
|
||||
:return: A Plan instance or None if its not found.
|
||||
:rtype: tuskarclient.v2.plans.Plan
|
||||
"""
|
||||
return self._post(self._roles_path(plan_uuid), {'uuid': role_uuid})
|
||||
|
||||
def remove_role(self, plan_uuid, role_uuid):
|
||||
"""Removes a Role from a Plan.
|
||||
|
||||
:param plan_uuid: UUID of the Plan.
|
||||
:type plan_uuid: string
|
||||
|
||||
:param role_uuid: UUID of the Role.
|
||||
:type role_uuid: string
|
||||
|
||||
:return: A Plan instance or None if its not found.
|
||||
:rtype: tuskarclient.v2.plans.Plan
|
||||
"""
|
||||
|
||||
return self._delete(self._roles_path(plan_uuid, role_uuid))
|
||||
|
||||
def templates(self, plan_uuid):
|
||||
"""Gets template files from a Plan.
|
||||
|
||||
:param plan_uuid: UUID of the Plan.
|
||||
:type plan_uuid: string
|
||||
|
||||
:return: Template files contents
|
||||
:rtype: dict
|
||||
"""
|
||||
|
||||
self.resource_class = Templates
|
||||
res = self._get(self._templates_path(plan_uuid)).to_dict()
|
||||
self.resource_class = Plan
|
||||
return res
|
@ -1,302 +0,0 @@
|
||||
# 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 __future__ import print_function
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
import tuskarclient.common.formatting as fmt
|
||||
from tuskarclient.common import utils
|
||||
from tuskarclient.openstack.common.apiclient import exceptions as exc
|
||||
|
||||
|
||||
def do_plan_list(tuskar, args, outfile=sys.stdout):
|
||||
"""Show a list of the Plans."""
|
||||
plans = tuskar.plans.list()
|
||||
fields = ['uuid', 'name', 'description', 'roles']
|
||||
|
||||
formatters = {
|
||||
'roles': fmt.list_plan_roles_formatter,
|
||||
}
|
||||
|
||||
fmt.print_list(plans, fields, formatters, outfile=outfile)
|
||||
|
||||
|
||||
@utils.arg('plan', metavar="<PLAN>",
|
||||
help="UUID of the Plan to show.")
|
||||
@utils.arg('--verbose', default=False, action="store_true",
|
||||
help="Display full plan details")
|
||||
@utils.arg('--only-empty-parameters', default=False, action="store_true",
|
||||
help="Display only parameters with empty or None value")
|
||||
def do_plan_show(tuskar, args, outfile=sys.stdout):
|
||||
"""Show an individual Plan by its UUID."""
|
||||
plan = utils.find_resource(tuskar.plans, args.plan)
|
||||
if args.only_empty_parameters:
|
||||
plan._info['parameters'] = (
|
||||
filter_empty_parameters(plan._info['parameters']))
|
||||
if args.verbose:
|
||||
print_plan_detail(plan, outfile=outfile)
|
||||
else:
|
||||
print_plan_summary(plan, outfile=outfile)
|
||||
|
||||
|
||||
def print_plan_summary(plan, outfile=sys.stdout):
|
||||
"""Print a summary of Plan information (for plan-show etc.)."""
|
||||
|
||||
formatters = {
|
||||
'roles': fmt.parameters_v2_formatter,
|
||||
'parameters': fmt.parameters_v2_formatter,
|
||||
}
|
||||
plan_dict = plan.to_dict()
|
||||
plan_dict['parameters'] = [param for param in
|
||||
plan_dict['parameters']
|
||||
if param['name'].endswith('::count')]
|
||||
fmt.print_dict(plan_dict, formatters, outfile=outfile)
|
||||
|
||||
|
||||
@utils.arg('plan', metavar="<PLAN>",
|
||||
help="UUID of the Plan to show a scale.")
|
||||
def do_plan_show_scale(tuskar, args, outfile=sys.stdout):
|
||||
"""Show scale counts of Plan."""
|
||||
plan = utils.find_resource(tuskar.plans, args.plan)
|
||||
scales = filter_parameters_to_dict(plan.parameters, 'count')
|
||||
fmt.print_dict(scales, outfile=outfile)
|
||||
|
||||
|
||||
@utils.arg('plan', metavar="<PLAN>",
|
||||
help="UUID of the Plan to show a scale.")
|
||||
def do_plan_show_flavors(tuskar, args, outfile=sys.stdout):
|
||||
"""Show flavors assigned to roles of Plan."""
|
||||
plan = utils.find_resource(tuskar.plans, args.plan)
|
||||
flavors = filter_parameters_to_dict(plan.parameters, 'Flavor')
|
||||
fmt.print_dict(flavors, outfile=outfile)
|
||||
|
||||
|
||||
def filter_parameters_to_dict(parameters, param_name):
|
||||
"""Filters list of parameters for given parameter name suffix."""
|
||||
filtered_params = {}
|
||||
suffix = '::{0}'.format(param_name)
|
||||
for param in parameters:
|
||||
if param['name'].endswith(suffix):
|
||||
filtered_params[param['name'].replace(suffix, '')] = param["value"]
|
||||
return filtered_params
|
||||
|
||||
|
||||
def filter_empty_parameters(parameters):
|
||||
"""Filters parameters with empty or None value."""
|
||||
filtered_parameters = [param for param in parameters
|
||||
if param['value'] == '' or param['value'] is None]
|
||||
return filtered_parameters
|
||||
|
||||
|
||||
def print_plan_detail(plan, outfile=sys.stdout):
|
||||
"""Print detailed Plan information (for plan-show --verbose etc.)."""
|
||||
|
||||
formatters = {
|
||||
'roles': fmt.parameters_v2_formatter,
|
||||
'parameters': fmt.parameters_v2_formatter,
|
||||
}
|
||||
plan_dict = plan.to_dict()
|
||||
fmt.print_dict(plan_dict, formatters, outfile=outfile)
|
||||
|
||||
|
||||
@utils.arg('plan', metavar="<PLAN>",
|
||||
help="UUID of the plan to delete.")
|
||||
def do_plan_delete(tuskar, args, outfile=sys.stdout):
|
||||
"""Delete an plan by its UUID."""
|
||||
plan = utils.find_resource(tuskar.plans, args.plan)
|
||||
tuskar.plans.delete(plan.uuid)
|
||||
print(u'Deleted Plan "%s".' % plan.name, file=outfile)
|
||||
|
||||
|
||||
@utils.arg('name', help="Name of the Plan to create.")
|
||||
@utils.arg('-d', '--description', metavar="<DESCRIPTION>",
|
||||
help='User-readable text describing the Plan.')
|
||||
def do_plan_create(tuskar, args, outfile=sys.stdout):
|
||||
"""Create a new plan."""
|
||||
name = vars(args).get('name')
|
||||
try:
|
||||
plan = tuskar.plans.create(
|
||||
name=name,
|
||||
description=vars(args).get('description')
|
||||
)
|
||||
except exc.Conflict:
|
||||
raise exc.CommandError('Plan with name "%s" already exists.' % name)
|
||||
print_plan_summary(plan, outfile=outfile)
|
||||
|
||||
|
||||
@utils.arg('plan_uuid', help="UUID of the Plan to assign role to.")
|
||||
@utils.arg('-r', '--role-uuid', metavar="<ROLE UUID>",
|
||||
required=True, help='UUID of the Role to be assigned.')
|
||||
def do_plan_add_role(tuskar, args, outfile=sys.stdout):
|
||||
"""Associate role to a plan."""
|
||||
plan = tuskar.plans.add_role(
|
||||
vars(args).get('plan_uuid'),
|
||||
vars(args).get('role_uuid')
|
||||
)
|
||||
print_plan_summary(plan, outfile=outfile)
|
||||
|
||||
|
||||
@utils.arg('plan_uuid', help="UUID of the Plan to remove role from.")
|
||||
@utils.arg('-r', '--role-uuid', metavar="<ROLE UUID>",
|
||||
required=True, help='UUID of the Role to be removed.')
|
||||
def do_plan_remove_role(tuskar, args, outfile=sys.stdout):
|
||||
"""Remove role from a plan."""
|
||||
plan = tuskar.plans.remove_role(
|
||||
vars(args).get('plan_uuid'),
|
||||
vars(args).get('role_uuid')
|
||||
)
|
||||
print_plan_summary(plan, outfile=outfile)
|
||||
|
||||
|
||||
@utils.arg('role_name', help="Name of role which you want scale.")
|
||||
@utils.arg('plan_uuid', help="UUID of the Plan to modify.")
|
||||
@utils.arg('-C', '--count', help="Count of nodes to be set.", required=True)
|
||||
def do_plan_scale(tuskar, args, outfile=sys.stdout):
|
||||
"""Scale plan by changing count of roles."""
|
||||
roles = tuskar.roles.list()
|
||||
plan = utils.find_resource(tuskar.plans, args.plan_uuid)
|
||||
parameters = []
|
||||
|
||||
for role in roles:
|
||||
versioned_name = "{name}-{v}".format(name=role.name, v=role.version)
|
||||
if versioned_name == args.role_name:
|
||||
role_name_key = versioned_name + "::count"
|
||||
parameters.append({'name': role_name_key,
|
||||
'value': args.count})
|
||||
old_val = [p['value'] for p in plan.parameters
|
||||
if p['name'] == role_name_key][0]
|
||||
|
||||
if old_val != args.count:
|
||||
print("Scaling {role} count: {old_val} -> {new_val}".format(
|
||||
role=args.role_name, old_val=old_val, new_val=args.count
|
||||
), file=outfile)
|
||||
else:
|
||||
print("Keeping scale {role} count: {count}".format(
|
||||
role=args.role_name, count=old_val), file=outfile)
|
||||
return
|
||||
|
||||
if parameters:
|
||||
return tuskar.plans.patch(args.plan_uuid, parameters)
|
||||
else:
|
||||
print("ERROR: no roles were found in the Plan with the name {0}".
|
||||
format(args.role_name), file=sys.stderr)
|
||||
|
||||
|
||||
@utils.arg('role_name', help="Name of role which you want to flavor.")
|
||||
@utils.arg('plan_uuid', help="UUID of the Plan to modify.")
|
||||
@utils.arg('-F', '--flavor', help="Flavor which shall be assigned to role.",
|
||||
required=True)
|
||||
def do_plan_flavor(tuskar, args, outfile=sys.stdout):
|
||||
"""Change flavor of role in the plan."""
|
||||
roles = tuskar.roles.list()
|
||||
plan = utils.find_resource(tuskar.plans, args.plan_uuid)
|
||||
parameters = []
|
||||
|
||||
for role in roles:
|
||||
versioned_name = "{name}-{v}".format(name=role.name, v=role.version)
|
||||
if versioned_name == args.role_name:
|
||||
role_name_key = versioned_name + "::Flavor"
|
||||
parameters.append({'name': role_name_key,
|
||||
'value': args.flavor})
|
||||
old_val = [p['value'] for p in plan.parameters
|
||||
if p['name'] == role_name_key][0]
|
||||
|
||||
if old_val != args.flavor:
|
||||
print("Changing {role} flavor: {old_val} -> {new_val}".format(
|
||||
role=args.role_name, old_val=old_val, new_val=args.flavor
|
||||
), file=outfile)
|
||||
else:
|
||||
print("Keeping flavor {role} unchanged: {flavor}".format(
|
||||
role=args.role_name, flavor=old_val), file=outfile)
|
||||
return
|
||||
|
||||
if parameters:
|
||||
return tuskar.plans.patch(args.plan_uuid, parameters)
|
||||
else:
|
||||
print("ERROR: no roles were found in the Plan with the name {0}".
|
||||
format(args.role_name), file=sys.stderr)
|
||||
|
||||
|
||||
@utils.arg('plan_uuid', help="UUID of the Plan to modify.")
|
||||
@utils.arg('-A', '--attribute', dest='attributes', metavar='<KEY1=VALUE1>',
|
||||
help=('This can be specified multiple times. This argument is '
|
||||
'deprecated, use -P and --parameter instead.'),
|
||||
action='append')
|
||||
@utils.arg('-P', '--parameter', dest='parameters', metavar='<KEY1=VALUE1>',
|
||||
help='This can be specified multiple times.',
|
||||
action='append')
|
||||
def do_plan_update(tuskar, args, outfile=sys.stdout):
|
||||
"""Change an existing plan."""
|
||||
|
||||
parameters = args.parameters
|
||||
|
||||
if args.attributes:
|
||||
print("WARNING: The attribute flags -A and --attribute are"
|
||||
" deprecated and will be removed in a later release."
|
||||
" Use -P and --parameter instead.", file=sys.stderr)
|
||||
parameters = args.attributes
|
||||
|
||||
parameters = [{'name': pair[0], 'value': pair[1]}
|
||||
for pair
|
||||
in utils.format_key_value_args(parameters).items()]
|
||||
return tuskar.plans.patch(args.plan_uuid, parameters)
|
||||
|
||||
|
||||
@utils.arg('plan_uuid', help="UUID of the Plan to modify.")
|
||||
@utils.arg('-A', '--attribute', dest='attributes', metavar='<KEY1=VALUE1>',
|
||||
help='This can be specified multiple times.',
|
||||
action='append')
|
||||
def do_plan_patch(*args, **kwargs):
|
||||
"""Change an existing plan [Deprecated]."""
|
||||
print("WARNING: plan-patch method is deprecated"
|
||||
" and will be removed in a later release."
|
||||
" Use plan-update instead.", file=sys.stderr)
|
||||
do_plan_update(*args, **kwargs)
|
||||
|
||||
|
||||
@utils.arg('plan_uuid',
|
||||
help="UUID of the Plan whose Templates will be retrieved.")
|
||||
@utils.arg('-O', '--output-dir', metavar='<OUTPUT DIR>',
|
||||
required=True,
|
||||
help='Directory to write template files into. ' +
|
||||
'It will be created if it does not exist.')
|
||||
def do_plan_templates(tuskar, args, outfile=sys.stdout):
|
||||
"""Download the Heat templates for a Plan."""
|
||||
# check that output directory exists and we can write into it
|
||||
output_dir = args.output_dir
|
||||
|
||||
if not os.path.isdir(output_dir):
|
||||
os.mkdir(output_dir)
|
||||
|
||||
# retrieve templates
|
||||
templates = tuskar.plans.templates(args.plan_uuid)
|
||||
|
||||
# write file for each key-value in templates
|
||||
print("The following templates will be written:")
|
||||
for template_name, template_content in templates.items():
|
||||
|
||||
# It's possible to organize the role templates and their dependent
|
||||
# files into directories, in which case the template_name will carry
|
||||
# the directory information. If that's the case, first create the
|
||||
# directory structure (if it hasn't already been created by another
|
||||
# file in the templates list).
|
||||
template_dir = os.path.dirname(template_name)
|
||||
output_template_dir = os.path.join(output_dir, template_dir)
|
||||
if template_dir and not os.path.exists(output_template_dir):
|
||||
os.makedirs(output_template_dir)
|
||||
|
||||
filename = os.path.join(output_dir, template_name)
|
||||
with open(filename, 'w+') as template_file:
|
||||
template_file.write(template_content)
|
||||
print(filename)
|
@ -1,47 +0,0 @@
|
||||
# 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 tuskarclient.openstack.common.apiclient import base
|
||||
|
||||
|
||||
class Role(base.Resource):
|
||||
"""Represents an instance of a Role in the Tuskar API.
|
||||
|
||||
:param manager: Manager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
|
||||
|
||||
class RoleManager(base.BaseManager):
|
||||
"""RoleManager interacts with the Tuskar API and provides
|
||||
operations for adding/removing Roles to/from Plans.
|
||||
"""
|
||||
|
||||
# The class used to represent a Role instance
|
||||
resource_class = Role
|
||||
|
||||
@staticmethod
|
||||
def _path(role_id=None):
|
||||
|
||||
if role_id:
|
||||
return '/roles/%s' % role_id
|
||||
|
||||
return '/roles'
|
||||
|
||||
def list(self):
|
||||
"""Get a list of the existing Roles
|
||||
|
||||
:return: A list of Roles or an empty list if none are found.
|
||||
:rtype: [tuskarclient.v2.plans.Role] or []
|
||||
"""
|
||||
return self._list(self._path())
|
@ -1,31 +0,0 @@
|
||||
# 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 __future__ import print_function
|
||||
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
import tuskarclient.common.formatting as fmt
|
||||
|
||||
|
||||
def do_role_list(tuskar, args, outfile=sys.stdout):
|
||||
"""Show a list of the Roles."""
|
||||
roles = tuskar.roles.list()
|
||||
fields = ['uuid', 'name', 'version', 'description']
|
||||
|
||||
formatters = {
|
||||
'description': six.text_type.strip,
|
||||
}
|
||||
|
||||
fmt.print_list(roles, fields, formatters, outfile=outfile)
|
@ -1,32 +0,0 @@
|
||||
# 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 tuskarclient.common import utils
|
||||
from tuskarclient.v2 import plans_shell
|
||||
from tuskarclient.v2 import roles_shell
|
||||
|
||||
COMMAND_MODULES = [
|
||||
plans_shell,
|
||||
roles_shell
|
||||
]
|
||||
|
||||
|
||||
def enhance_parser(parser, subparsers):
|
||||
"""Take a basic (nonversioned) parser and enhance it with
|
||||
commands and options specific for this version of API.
|
||||
|
||||
:param parser: top level parser
|
||||
:param subparsers: top level parser's subparsers collection
|
||||
where subcommands will go
|
||||
"""
|
||||
for command_module in COMMAND_MODULES:
|
||||
utils.define_commands_from_module(subparsers, command_module)
|
Loading…
x
Reference in New Issue
Block a user