Retire the Tuskar Client codebase

Change-Id: Iee7117358100a1b5cfb365103e6e2ac247ea6178
Depends-On: I904b2f27591333e104bf9080bb8c3876fcb3596c
This commit is contained in:
Dougal Matthews 2016-01-21 15:15:13 +00:00
parent 8f9ccd7594
commit c14ba08d32
85 changed files with 10 additions and 8514 deletions

View File

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

View File

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

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

View File

@ -1,5 +0,0 @@
include AUTHORS
include ChangeLog
exclude .gitignore
exclude .gitreview

10
README Normal file
View 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.

View File

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

@ -1,2 +0,0 @@
build/
source/ref/

View File

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

View File

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

View File

@ -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(/(&lt;)+[3]/gi, "<tt class='heart'>&#x2665;</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);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
[theme]
inherit = basic
stylesheet = nature.css
pygments_style = tango

View File

@ -1,8 +0,0 @@
CLI Usage with version 2 API
===========================
.. toctree::
:maxdepth: 1
plans
roles

View File

@ -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 Plans 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 Plans role with specified count of nodes.
Output of this command is a short summary of changed values.
Show Plans 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 Plans 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 Plans 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
#!/bin/bash
TOOLS=`dirname $0`
VENV=$TOOLS/../.venv
source $VENV/bin/activate && python -m tuskarclient.shell $@

39
tox.ini
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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