Initial commit.
This commit is contained in:
commit
17f6b83ee6
|
@ -0,0 +1,11 @@
|
||||||
|
.coverage
|
||||||
|
.keystoneclient-venv
|
||||||
|
*,cover
|
||||||
|
cover
|
||||||
|
*.pyc
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*~
|
||||||
|
build
|
||||||
|
dist
|
||||||
|
python_keystoneclient.egg-info
|
|
@ -0,0 +1,18 @@
|
||||||
|
Andrey Brindeyev <abrindeyev@griddynamics.com>
|
||||||
|
Andy Smith <github@anarkystic.com>
|
||||||
|
Antony Messerli <amesserl@rackspace.com>
|
||||||
|
Brian Lamar <brian.lamar@rackspace.com>
|
||||||
|
Brian Waldon <brian.waldon@rackspace.com>
|
||||||
|
Chris Behrens <cbehrens+github@codestud.com>
|
||||||
|
Christopher MacGown <ignoti+github@gmail.com>
|
||||||
|
Ed Leafe <ed@leafe.com>
|
||||||
|
Eldar Nugaev <eldr@ya.ru>
|
||||||
|
Ilya Alekseyev <ilyaalekseyev@acm.org>
|
||||||
|
Johannes Erdfelt <johannes.erdfelt@rackspace.com>
|
||||||
|
Josh Kearney <josh@jk0.org>
|
||||||
|
Kevin L. Mitchell <kevin.mitchell@rackspace.com>
|
||||||
|
Kirill Shileev <kshileev@griddynamics.com>
|
||||||
|
Lvov Maxim <mlvov@mirantis.com>
|
||||||
|
Matt Dietz <matt.dietz@rackspace.com>
|
||||||
|
Sandy Walsh <sandy@darksecretsoftware.com>
|
||||||
|
Gabriel Hurley <gabriel@strikeawe.com>
|
|
@ -0,0 +1,77 @@
|
||||||
|
Keystone Style Commandments
|
||||||
|
===========================
|
||||||
|
|
||||||
|
Step 1: Read http://www.python.org/dev/peps/pep-0008/
|
||||||
|
Step 2: Read http://www.python.org/dev/peps/pep-0008/ again
|
||||||
|
Step 3: Read on
|
||||||
|
|
||||||
|
Imports
|
||||||
|
-------
|
||||||
|
- thou shalt not import objects, only modules
|
||||||
|
- thou shalt not import more than one module per line
|
||||||
|
- thou shalt not make relative imports
|
||||||
|
- thou shalt organize your imports according to the following template
|
||||||
|
|
||||||
|
::
|
||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
{{stdlib imports in human alphabetical order}}
|
||||||
|
\n
|
||||||
|
{{third-party library imports in human alphabetical order}}
|
||||||
|
\n
|
||||||
|
{{keystoneclient imports in human alphabetical order}}
|
||||||
|
\n
|
||||||
|
\n
|
||||||
|
{{begin your code}}
|
||||||
|
|
||||||
|
|
||||||
|
General
|
||||||
|
-------
|
||||||
|
- thou shalt put two newlines twixt toplevel code (funcs, classes, etc)
|
||||||
|
- thou shalt put one newline twixt methods in classes and anywhere else
|
||||||
|
- thou shalt not write "except:", use "except Exception:" at the very least
|
||||||
|
- thou shalt include your name with TODOs as in "TODO(termie)"
|
||||||
|
- thou shalt not name anything the same name as a builtin or reserved word
|
||||||
|
- thou shalt not violate causality in our time cone, or else
|
||||||
|
|
||||||
|
|
||||||
|
Human Alphabetical Order Examples
|
||||||
|
---------------------------------
|
||||||
|
::
|
||||||
|
import httplib
|
||||||
|
import logging
|
||||||
|
import random
|
||||||
|
import StringIO
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient import service_catalog
|
||||||
|
from keystoneclient.v2_0 import client
|
||||||
|
|
||||||
|
Docstrings
|
||||||
|
----------
|
||||||
|
"""A one line docstring looks like this and ends in a period."""
|
||||||
|
|
||||||
|
|
||||||
|
"""A multiline docstring has a one-line summary, less than 80 characters.
|
||||||
|
|
||||||
|
Then a new paragraph after a newline that explains in more detail any
|
||||||
|
general information about the function, class or method. Example usages
|
||||||
|
are also great to have here if it is a complex class for function. After
|
||||||
|
you have finished your descriptions add an extra newline and close the
|
||||||
|
quotations.
|
||||||
|
|
||||||
|
When writing the docstring for a class, an extra line should be placed
|
||||||
|
after the closing quotations. For more in-depth explanations for these
|
||||||
|
decisions see http://www.python.org/dev/peps/pep-0257/
|
||||||
|
|
||||||
|
If you are going to describe parameters and return values, use Sphinx, the
|
||||||
|
appropriate syntax is as follows.
|
||||||
|
|
||||||
|
:param foo: the foo parameter
|
||||||
|
:param bar: the bar parameter
|
||||||
|
:returns: description of the return value
|
||||||
|
|
||||||
|
"""
|
|
@ -0,0 +1,209 @@
|
||||||
|
Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1)
|
||||||
|
Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1)
|
||||||
|
Copyright (c) 2011 Nebula, Inc - Keystone refactor (>= v2.7)
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
--- License for python-keystoneclient versions prior to 2.1 ---
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer in the
|
||||||
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of this project nor the names of its contributors may
|
||||||
|
be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||||
|
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,3 @@
|
||||||
|
include README.rst
|
||||||
|
recursive-include docs *
|
||||||
|
recursive-include tests *
|
|
@ -0,0 +1,93 @@
|
||||||
|
Python bindings to the OpenStack Keystone API
|
||||||
|
=============================================
|
||||||
|
|
||||||
|
This is a client for the OpenStack Keystone API. There's a Python API (the
|
||||||
|
``keystoneclient`` module), and a command-line script (``keystone``). The
|
||||||
|
Keystone 2.0 API is still a moving target, so this module will remain in
|
||||||
|
"Beta" status until the API is finalized and fully implemented.
|
||||||
|
|
||||||
|
Development takes place on GitHub__. Bug reports and patches may be filed there.
|
||||||
|
|
||||||
|
__ https://github.com/4P/python-keystoneclient
|
||||||
|
|
||||||
|
This code a fork of `Rackspace's python-novaclient`__ which is in turn a fork of
|
||||||
|
`Jacobian's python-cloudservers`__. The python-keystoneclient is licensed under
|
||||||
|
the Apache License like the rest of OpenStack.
|
||||||
|
|
||||||
|
__ http://github.com/rackspace/python-novaclient
|
||||||
|
__ http://github.com/jacobian/python-cloudservers
|
||||||
|
|
||||||
|
.. contents:: Contents:
|
||||||
|
:local:
|
||||||
|
|
||||||
|
Python API
|
||||||
|
----------
|
||||||
|
|
||||||
|
By way of a quick-start::
|
||||||
|
|
||||||
|
# use v2.0 auth with http://example.com:5000/v2.0/")
|
||||||
|
>>> from keystoneclient.v2_0 import client
|
||||||
|
>>> keystone = client.Client(USERNAME, API_KEY, PROJECT_ID)
|
||||||
|
>>> keystone.tenants.list()
|
||||||
|
>>> tenant = keystone.tenants.create(name="test", descrption="My new tenant!", enabled=True)
|
||||||
|
>>> tenant.delete()
|
||||||
|
|
||||||
|
|
||||||
|
Command-line API
|
||||||
|
----------------
|
||||||
|
|
||||||
|
.. attention:: COMING SOON
|
||||||
|
|
||||||
|
The API is not yet implemented, but will follow the pattern laid
|
||||||
|
out below.
|
||||||
|
|
||||||
|
Installing this package gets you a shell command, ``keystone``, that you
|
||||||
|
can use to interact with Keystone's API.
|
||||||
|
|
||||||
|
You'll need to provide your OpenStack username and API key. You can do this
|
||||||
|
with the ``--username``, ``--apikey`` and ``--projectid`` params, but it's
|
||||||
|
easier to just set them as environment variables::
|
||||||
|
|
||||||
|
export KEYSTONE_USERNAME=openstack
|
||||||
|
export KEYSTONE_API_KEY=yadayada
|
||||||
|
export KEYSTONE_PROJECTID=yadayada
|
||||||
|
|
||||||
|
You will also need to define the authentication url with ``--url`` and the
|
||||||
|
version of the API with ``--version``. Or set them as an environment
|
||||||
|
variables as well::
|
||||||
|
|
||||||
|
export KEYSTONE_URL=http://example.com:5000/v2.0
|
||||||
|
export KEYSTONE_ADMIN_URL=http://example.com:35357/v2.0
|
||||||
|
export KEYSTONE_VERSION=2.0
|
||||||
|
|
||||||
|
Since Keystone can return multiple regions in the Service Catalog, you
|
||||||
|
can specify the one you want with ``--region_name`` (or
|
||||||
|
``export KEYSTONE_REGION_NAME``). It defaults to the first in the list returned.
|
||||||
|
|
||||||
|
You'll find complete documentation on the shell by running
|
||||||
|
``keystone help``::
|
||||||
|
|
||||||
|
usage: keystone [--username USERNAME] [--apikey APIKEY] [--projectid PROJECTID]
|
||||||
|
[--url URL] [--version VERSION] [--region_name NAME]
|
||||||
|
<subcommand> ...
|
||||||
|
|
||||||
|
Command-line interface to the OpenStack Keystone API.
|
||||||
|
|
||||||
|
Positional arguments:
|
||||||
|
<subcommand>
|
||||||
|
add-fixed-ip Add a new fixed IP address to a servers network.
|
||||||
|
|
||||||
|
|
||||||
|
Optional arguments:
|
||||||
|
--username USERNAME Defaults to env[KEYSTONE_USERNAME].
|
||||||
|
--apikey APIKEY Defaults to env[KEYSTONE_API_KEY].
|
||||||
|
--apikey PROJECTID Defaults to env[KEYSTONE_PROJECT_ID].
|
||||||
|
--url AUTH_URL Defaults to env[KEYSTONE_URL] or
|
||||||
|
--url ADMIN_URL Defaults to env[KEYSTONE_ADMIN_URL]
|
||||||
|
--version VERSION Defaults to env[KEYSTONE_VERSION] or 2.0.
|
||||||
|
--region_name NAME The region name in the Keystone Service Catalog
|
||||||
|
to use after authentication. Defaults to
|
||||||
|
env[KEYSTONE_REGION_NAME] or the first item
|
||||||
|
in the list returned.
|
||||||
|
|
||||||
|
See "keystone help COMMAND" for help on a specific command.
|
|
@ -0,0 +1 @@
|
||||||
|
_build/
|
|
@ -0,0 +1,89 @@
|
||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-keystoneclient.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-keystoneclient.qhc"
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
|
||||||
|
"run these through (pdf)latex."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
|
@ -0,0 +1,19 @@
|
||||||
|
The :mod:`keystoneclient` Python API
|
||||||
|
====================================
|
||||||
|
|
||||||
|
.. module:: keystoneclient
|
||||||
|
:synopsis: A client for the OpenStack Keystone API.
|
||||||
|
|
||||||
|
.. currentmodule:: keystoneclient.v2_0.client
|
||||||
|
|
||||||
|
.. autoclass:: Client
|
||||||
|
|
||||||
|
.. automethod:: authenticate
|
||||||
|
|
||||||
|
|
||||||
|
For more information, see the reference documentation:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
ref/index
|
|
@ -0,0 +1,200 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# python-keystoneclient 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, os
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||||||
|
|
||||||
|
# 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('.'))
|
||||||
|
|
||||||
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
|
||||||
|
|
||||||
|
# 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 = u'python-keystoneclient'
|
||||||
|
copyright = u'Rackspace, based on work by Jacob Kaplan-Moss'
|
||||||
|
|
||||||
|
# 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 = '2.7'
|
||||||
|
# The full version, including alpha/beta/rc tags.
|
||||||
|
release = '2.7.0'
|
||||||
|
|
||||||
|
# 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 = ['_build']
|
||||||
|
|
||||||
|
# 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 = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- 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 = 'nature'
|
||||||
|
|
||||||
|
# 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 = 'python-keystoneclientdoc'
|
||||||
|
|
||||||
|
|
||||||
|
# -- 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', 'python-keystoneclient.tex', u'python-keystoneclient Documentation',
|
||||||
|
u'Nebula Inc, based on work by Rackspace and Jacob Kaplan-Moss', '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
|
||||||
|
|
||||||
|
|
||||||
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
|
intersphinx_mapping = {'http://docs.python.org/': None}
|
|
@ -0,0 +1,36 @@
|
||||||
|
Python bindings to the OpenStack Keystone API
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
This is a client for OpenStack Keystone API. There's :doc:`a Python API
|
||||||
|
<api>` (the :mod:`keystoneclient` module), and a :doc:`command-line script
|
||||||
|
<shell>` (installed as :program:`keystone`).
|
||||||
|
|
||||||
|
You'll need an `OpenStack Keystone` account, which you can get by
|
||||||
|
using `keystone-manage`.
|
||||||
|
|
||||||
|
Contents:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
shell
|
||||||
|
api
|
||||||
|
ref/index
|
||||||
|
releases
|
||||||
|
|
||||||
|
Contributing
|
||||||
|
============
|
||||||
|
|
||||||
|
Development takes place `on GitHub`__; please file bugs/pull requests there.
|
||||||
|
|
||||||
|
__ https://github.com/4P/python-keystoneclient
|
||||||
|
|
||||||
|
Run tests with ``python setup.py test``.
|
||||||
|
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
Exceptions
|
||||||
|
==========
|
||||||
|
|
||||||
|
.. currentmodule:: keystoneclient.exceptions
|
||||||
|
|
||||||
|
.. automodule:: keystoneclient.exceptions
|
||||||
|
:members:
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
API Reference
|
||||||
|
=============
|
||||||
|
|
||||||
|
The following API reference documents are available:
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
exceptions
|
|
@ -0,0 +1,106 @@
|
||||||
|
=============
|
||||||
|
Release notes
|
||||||
|
=============
|
||||||
|
|
||||||
|
2.7.0 (October 21, 2011)
|
||||||
|
========================
|
||||||
|
* Forked from http://github.com/rackspace/python-novaclient
|
||||||
|
* Rebranded to python-keystoneclient
|
||||||
|
* Refactored to support Keystone API (auth, tokens, services, roles, tenants,
|
||||||
|
users, etc.)
|
||||||
|
|
||||||
|
2.5.8 (July 11, 2011)
|
||||||
|
=====================
|
||||||
|
* returns all public/private ips, not just first one
|
||||||
|
* better 'nova list' search options
|
||||||
|
|
||||||
|
2.5.7 - 2.5.6 = minor tweaks
|
||||||
|
|
||||||
|
2.5.5 (June 21, 2011)
|
||||||
|
=====================
|
||||||
|
* zone-boot min/max instance count added thanks to comstud
|
||||||
|
* create for user added thanks to cerberus
|
||||||
|
* fixed tests
|
||||||
|
|
||||||
|
2.5.3 (June 15, 2011)
|
||||||
|
=====================
|
||||||
|
* ProjectID can be None for backwards compatability.
|
||||||
|
* README/docs updated for projectId thanks to usrleon
|
||||||
|
|
||||||
|
2.5.1 (June 10, 2011)
|
||||||
|
=====================
|
||||||
|
* ProjectID now part of authentication
|
||||||
|
|
||||||
|
2.5.0 (June 3, 2011)
|
||||||
|
====================
|
||||||
|
|
||||||
|
* better logging thanks to GridDynamics
|
||||||
|
|
||||||
|
2.4.4 (June 1, 2011)
|
||||||
|
====================
|
||||||
|
|
||||||
|
* added support for GET /servers with reservation_id (and /servers/detail)
|
||||||
|
|
||||||
|
2.4.3 (May 27, 2011)
|
||||||
|
====================
|
||||||
|
|
||||||
|
* added support for POST /zones/select (client only, not cmdline)
|
||||||
|
|
||||||
|
2.4 (March 7, 2011)
|
||||||
|
===================
|
||||||
|
|
||||||
|
* added Jacob Kaplan-Moss copyright notices to older/untouched files.
|
||||||
|
|
||||||
|
|
||||||
|
2.3 (March 2, 2011)
|
||||||
|
===================
|
||||||
|
|
||||||
|
* package renamed to python-novaclient. Module to novaclient
|
||||||
|
|
||||||
|
|
||||||
|
2.2 (March 1, 2011)
|
||||||
|
===================
|
||||||
|
|
||||||
|
* removed some license/copywrite notices from source that wasn't
|
||||||
|
significantly changed.
|
||||||
|
|
||||||
|
|
||||||
|
2.1 (Feb 28, 2011)
|
||||||
|
==================
|
||||||
|
|
||||||
|
* shell renamed to nova from novatools
|
||||||
|
|
||||||
|
* license changed from BSD to Apache
|
||||||
|
|
||||||
|
2.0 (Feb 7, 2011)
|
||||||
|
=================
|
||||||
|
|
||||||
|
* Forked from https://github.com/jacobian/python-cloudservers
|
||||||
|
|
||||||
|
* Rebranded to python-novatools
|
||||||
|
|
||||||
|
* Auth URL support
|
||||||
|
|
||||||
|
* New OpenStack specific commands added (pause, suspend, etc)
|
||||||
|
|
||||||
|
1.2 (August 15, 2010)
|
||||||
|
=====================
|
||||||
|
|
||||||
|
* Support for Python 2.4 - 2.7.
|
||||||
|
|
||||||
|
* Improved output of :program:`cloudservers ipgroup-list`.
|
||||||
|
|
||||||
|
* Made ``cloudservers boot --ipgroup <name>`` work (as well as ``--ipgroup
|
||||||
|
<id>``).
|
||||||
|
|
||||||
|
1.1 (May 6, 2010)
|
||||||
|
=================
|
||||||
|
|
||||||
|
* Added a ``--files`` option to :program:`cloudservers boot` supporting
|
||||||
|
the upload of (up to five) files at boot time.
|
||||||
|
|
||||||
|
* Added a ``--key`` option to :program:`cloudservers boot` to key the server
|
||||||
|
with an SSH public key at boot time. This is just a shortcut for ``--files``,
|
||||||
|
but it's a useful shortcut.
|
||||||
|
|
||||||
|
* Changed the default server image to Ubuntu 10.04 LTS.
|
|
@ -0,0 +1,57 @@
|
||||||
|
The :program:`keystone` shell utility
|
||||||
|
=========================================
|
||||||
|
|
||||||
|
.. program:: keystone
|
||||||
|
.. highlight:: bash
|
||||||
|
|
||||||
|
.. warning:: COMING SOON
|
||||||
|
|
||||||
|
The command line interface is not yet completed. This document serves
|
||||||
|
as a reference for the implementation.
|
||||||
|
|
||||||
|
The :program:`keystone` shell utility interacts with OpenStack Keystone API
|
||||||
|
from the command line. It supports the entirety of the OpenStack Keystone API.
|
||||||
|
|
||||||
|
First, you'll need an OpenStack Keystone account and an API key. You get this
|
||||||
|
by using the `keystone-manage` command in OpenStack Keystone.
|
||||||
|
|
||||||
|
You'll need to provide :program:`keystone` with your OpenStack username and
|
||||||
|
API key. You can do this with the :option:`--username`, :option:`--apikey`
|
||||||
|
and :option:`--projectid` options, but it's easier to just set them as
|
||||||
|
environment variables by setting two environment variables:
|
||||||
|
|
||||||
|
.. envvar:: KEYSTONE_USERNAME
|
||||||
|
|
||||||
|
Your Keystone username.
|
||||||
|
|
||||||
|
.. envvar:: KEYSTONE_API_KEY
|
||||||
|
|
||||||
|
Your API key.
|
||||||
|
|
||||||
|
.. envvar:: KEYSTONE_PROJECT_ID
|
||||||
|
|
||||||
|
Project for work.
|
||||||
|
|
||||||
|
.. envvar:: KEYSTONE_URL
|
||||||
|
|
||||||
|
The OpenStack API server URL.
|
||||||
|
|
||||||
|
.. envvar:: KEYSTONE_VERSION
|
||||||
|
|
||||||
|
The OpenStack API version.
|
||||||
|
|
||||||
|
For example, in Bash you'd use::
|
||||||
|
|
||||||
|
export KEYSTONE_USERNAME=yourname
|
||||||
|
export KEYSTONE_API_KEY=yadayadayada
|
||||||
|
export KEYSTONE_PROJECT_ID=myproject
|
||||||
|
export KEYSTONE_URL=http://...
|
||||||
|
export KEYSTONE_VERSION=2.0
|
||||||
|
|
||||||
|
From there, all shell commands take the form::
|
||||||
|
|
||||||
|
keystone <command> [arguments...]
|
||||||
|
|
||||||
|
Run :program:`keystone help` to get a full list of all possible commands,
|
||||||
|
and run :program:`keystone help <command>` to get detailed help for that
|
||||||
|
command.
|
|
@ -0,0 +1,191 @@
|
||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base utilities to build API operation managers and objects on top of.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
# Python 2.4 compat
|
||||||
|
try:
|
||||||
|
all
|
||||||
|
except NameError:
|
||||||
|
def all(iterable):
|
||||||
|
return True not in (not x for x in iterable)
|
||||||
|
|
||||||
|
|
||||||
|
def getid(obj):
|
||||||
|
"""
|
||||||
|
Abstracts the common pattern of allowing both an object or an object's ID
|
||||||
|
(UUID) as a parameter when dealing with relationships.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Try to return the object's UUID first, if we have a UUID.
|
||||||
|
try:
|
||||||
|
if obj.uuid:
|
||||||
|
return obj.uuid
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return obj.id
|
||||||
|
except AttributeError:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(object):
|
||||||
|
"""
|
||||||
|
Managers interact with a particular type of API (servers, flavors, images,
|
||||||
|
etc.) and provide CRUD operations for them.
|
||||||
|
"""
|
||||||
|
resource_class = None
|
||||||
|
|
||||||
|
def __init__(self, api):
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
def _list(self, url, response_key, obj_class=None, body=None):
|
||||||
|
resp = None
|
||||||
|
if body:
|
||||||
|
resp, body = self.api.post(url, body=body)
|
||||||
|
else:
|
||||||
|
resp, body = self.api.get(url)
|
||||||
|
|
||||||
|
if obj_class is None:
|
||||||
|
obj_class = self.resource_class
|
||||||
|
|
||||||
|
data = body[response_key]
|
||||||
|
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||||
|
# unlike other services which just return the list...
|
||||||
|
if type(data) is dict:
|
||||||
|
data = data['values']
|
||||||
|
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||||
|
|
||||||
|
def _get(self, url, response_key):
|
||||||
|
resp, body = self.api.get(url)
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
|
||||||
|
def _create(self, url, body, response_key, return_raw=False):
|
||||||
|
resp, body = self.api.post(url, body=body)
|
||||||
|
if return_raw:
|
||||||
|
return body[response_key]
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
|
||||||
|
def _delete(self, url):
|
||||||
|
resp, body = self.api.delete(url)
|
||||||
|
|
||||||
|
def _update(self, url, body, response_key):
|
||||||
|
resp, body = self.api.put(url, body=body)
|
||||||
|
return self.resource_class(self, body[response_key])
|
||||||
|
|
||||||
|
|
||||||
|
class ManagerWithFind(Manager):
|
||||||
|
"""
|
||||||
|
Like a `Manager`, but with additional `find()`/`findall()` methods.
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
rl = self.findall(**kwargs)
|
||||||
|
try:
|
||||||
|
return rl[0]
|
||||||
|
except IndexError:
|
||||||
|
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||||
|
raise exceptions.NotFound(404, msg)
|
||||||
|
|
||||||
|
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 Resource(object):
|
||||||
|
"""
|
||||||
|
A resource represents a particular instance of an object (tenant, user,
|
||||||
|
etc). This is pretty much just a bag for attributes.
|
||||||
|
|
||||||
|
: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):
|
||||||
|
self.manager = manager
|
||||||
|
self._info = info
|
||||||
|
self._add_details(info)
|
||||||
|
self._loaded = loaded
|
||||||
|
|
||||||
|
def _add_details(self, info):
|
||||||
|
for (k, v) in info.iteritems():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
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 __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)
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
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
|
|
@ -0,0 +1,189 @@
|
||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
|
||||||
|
# All Rights Reserved.
|
||||||
|
"""
|
||||||
|
OpenStack Client interface. Handles the REST calls and responses.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import urllib
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
# Python 2.5 compat fix
|
||||||
|
if not hasattr(urlparse, 'parse_qsl'):
|
||||||
|
import cgi
|
||||||
|
urlparse.parse_qsl = cgi.parse_qsl
|
||||||
|
|
||||||
|
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPClient(httplib2.Http):
|
||||||
|
|
||||||
|
USER_AGENT = 'python-keystoneclient'
|
||||||
|
|
||||||
|
def __init__(self, username=None, password=None, token=None,
|
||||||
|
project_id=None, auth_url=None, region_name=None,
|
||||||
|
timeout=None, endpoint=None):
|
||||||
|
super(HTTPClient, self).__init__(timeout=timeout)
|
||||||
|
self.user = username
|
||||||
|
self.password = password
|
||||||
|
self.project_id = project_id
|
||||||
|
self.auth_url = auth_url
|
||||||
|
self.version = 'v2.0'
|
||||||
|
self.region_name = region_name
|
||||||
|
|
||||||
|
self.management_url = endpoint
|
||||||
|
self.auth_token = token or password
|
||||||
|
|
||||||
|
# httplib2 overrides
|
||||||
|
self.force_exception_to_status_code = True
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
""" Authenticate against the keystone API.
|
||||||
|
|
||||||
|
Not implemented here because auth protocols should be API
|
||||||
|
version-specific.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _extract_service_catalog(self, url, body):
|
||||||
|
""" Set the client's service catalog from the response data.
|
||||||
|
|
||||||
|
Not implemented here because data returned may be API
|
||||||
|
version-specific.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def http_log(self, args, kwargs, resp, body):
|
||||||
|
if os.environ.get('KEYSTONECLIENT_DEBUG', False):
|
||||||
|
ch = logging.StreamHandler()
|
||||||
|
_logger.setLevel(logging.DEBUG)
|
||||||
|
_logger.addHandler(ch)
|
||||||
|
elif not _logger.isEnabledFor(logging.DEBUG):
|
||||||
|
return
|
||||||
|
|
||||||
|
string_parts = ['curl -i']
|
||||||
|
for element in args:
|
||||||
|
if element in ('GET', 'POST'):
|
||||||
|
string_parts.append(' -X %s' % element)
|
||||||
|
else:
|
||||||
|
string_parts.append(' %s' % element)
|
||||||
|
|
||||||
|
for element in kwargs['headers']:
|
||||||
|
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||||
|
string_parts.append(header)
|
||||||
|
|
||||||
|
_logger.debug("REQ: %s\n" % "".join(string_parts))
|
||||||
|
if 'body' in kwargs:
|
||||||
|
_logger.debug("REQ BODY: %s\n" % (kwargs['body']))
|
||||||
|
_logger.debug("RESP: %s\nRESP BODY: %s\n", resp, body)
|
||||||
|
|
||||||
|
def request(self, url, method, **kwargs):
|
||||||
|
""" Send an http request with the specified characteristics.
|
||||||
|
|
||||||
|
Wrapper around httplib2.Http.request to handle tasks such as
|
||||||
|
setting headers, JSON encoding/decoding, and error handling.
|
||||||
|
"""
|
||||||
|
# Copy the kwargs so we can reuse the original in case of redirects
|
||||||
|
request_kwargs = copy.copy(kwargs)
|
||||||
|
request_kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||||
|
request_kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||||
|
if 'body' in kwargs:
|
||||||
|
request_kwargs['headers']['Content-Type'] = 'application/json'
|
||||||
|
request_kwargs['body'] = json.dumps(kwargs['body'])
|
||||||
|
|
||||||
|
resp, body = super(HTTPClient, self).request(url, method, **request_kwargs)
|
||||||
|
|
||||||
|
self.http_log((url, method,), request_kwargs, resp, body)
|
||||||
|
|
||||||
|
if body:
|
||||||
|
try:
|
||||||
|
body = json.loads(body)
|
||||||
|
except ValueError, e:
|
||||||
|
_logger.debug("Could not decode JSON from body: %s" % body)
|
||||||
|
else:
|
||||||
|
_logger.debug("No body was returned.")
|
||||||
|
body = None
|
||||||
|
|
||||||
|
if resp.status in (400, 401, 403, 404, 408, 409, 413, 500, 501):
|
||||||
|
raise exceptions.from_response(resp, body)
|
||||||
|
elif resp.status in (301, 302, 305):
|
||||||
|
# Redirected. Reissue the request to the new location.
|
||||||
|
return self.request(resp['location'], method, **kwargs)
|
||||||
|
|
||||||
|
return resp, body
|
||||||
|
|
||||||
|
def _cs_request(self, url, method, **kwargs):
|
||||||
|
if not self.management_url:
|
||||||
|
self.authenticate()
|
||||||
|
|
||||||
|
kwargs.setdefault('headers', {})
|
||||||
|
if self.auth_token and self.auth_token != self.password:
|
||||||
|
kwargs['headers']['X-Auth-Token'] = self.auth_token
|
||||||
|
if self.project_id:
|
||||||
|
kwargs['headers']['X-Auth-Project-Id'] = self.project_id
|
||||||
|
|
||||||
|
# Perform the request once. If we get a 401 back then it
|
||||||
|
# might be because the auth token expired, so try to
|
||||||
|
# re-authenticate and try again. If it still fails, bail.
|
||||||
|
try:
|
||||||
|
resp, body = self.request(self.management_url + url, method,
|
||||||
|
**kwargs)
|
||||||
|
return resp, body
|
||||||
|
except exceptions.Unauthorized:
|
||||||
|
try:
|
||||||
|
if getattr(self, '_failures', 0) < 1:
|
||||||
|
self._failures = getattr(self, '_failures', 0) + 1
|
||||||
|
self.authenticate()
|
||||||
|
resp, body = self.request(self.management_url + url,
|
||||||
|
method, **kwargs)
|
||||||
|
return resp, body
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except exceptions.Unauthorized:
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _munge_get_url(self, url):
|
||||||
|
"""
|
||||||
|
Munge GET URLs to always return uncached content.
|
||||||
|
|
||||||
|
The OpenStack Compute API caches data *very* agressively and doesn't
|
||||||
|
respect cache headers. To avoid stale data, then, we append a little
|
||||||
|
bit of nonsense onto GET parameters; this appears to force the data not
|
||||||
|
to be cached.
|
||||||
|
"""
|
||||||
|
scheme, netloc, path, query, frag = urlparse.urlsplit(url)
|
||||||
|
query = urlparse.parse_qsl(query)
|
||||||
|
query.append(('fresh', str(time.time())))
|
||||||
|
query = urllib.urlencode(query)
|
||||||
|
return urlparse.urlunsplit((scheme, netloc, path, query, frag))
|
||||||
|
|
||||||
|
def get(self, url, **kwargs):
|
||||||
|
url = self._munge_get_url(url)
|
||||||
|
return self._cs_request(url, 'GET', **kwargs)
|
||||||
|
|
||||||
|
def post(self, url, **kwargs):
|
||||||
|
return self._cs_request(url, 'POST', **kwargs)
|
||||||
|
|
||||||
|
def put(self, url, **kwargs):
|
||||||
|
return self._cs_request(url, 'PUT', **kwargs)
|
||||||
|
|
||||||
|
def delete(self, url, **kwargs):
|
||||||
|
return self._cs_request(url, 'DELETE', **kwargs)
|
|
@ -0,0 +1,129 @@
|
||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
"""
|
||||||
|
Exception definitions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class CommandError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AuthorizationFailure(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NoTokenLookupException(Exception):
|
||||||
|
"""This form of authentication does not support looking up
|
||||||
|
endpoints from an existing token."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNotFound(Exception):
|
||||||
|
"""Could not find Service or Region in Service Catalog."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ClientException(Exception):
|
||||||
|
"""
|
||||||
|
The base exception class for all exceptions this library raises.
|
||||||
|
"""
|
||||||
|
def __init__(self, code, message=None, details=None):
|
||||||
|
self.code = code
|
||||||
|
self.message = message or self.__class__.message
|
||||||
|
self.details = details
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s (HTTP %s)" % (self.message, self.code)
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequest(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 400 - Bad request: you sent some malformed data.
|
||||||
|
"""
|
||||||
|
http_status = 400
|
||||||
|
message = "Bad request"
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 401 - Unauthorized: bad credentials.
|
||||||
|
"""
|
||||||
|
http_status = 401
|
||||||
|
message = "Unauthorized"
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 403 - Forbidden: your credentials don't give you access to this
|
||||||
|
resource.
|
||||||
|
"""
|
||||||
|
http_status = 403
|
||||||
|
message = "Forbidden"
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 404 - Not found
|
||||||
|
"""
|
||||||
|
http_status = 404
|
||||||
|
message = "Not found"
|
||||||
|
|
||||||
|
|
||||||
|
class Conflict(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 409 - Conflict
|
||||||
|
"""
|
||||||
|
http_status = 409
|
||||||
|
message = "Conflict"
|
||||||
|
|
||||||
|
|
||||||
|
class OverLimit(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 413 - Over limit: you're over the API limits for this time period.
|
||||||
|
"""
|
||||||
|
http_status = 413
|
||||||
|
message = "Over limit"
|
||||||
|
|
||||||
|
|
||||||
|
# NotImplemented is a python keyword.
|
||||||
|
class HTTPNotImplemented(ClientException):
|
||||||
|
"""
|
||||||
|
HTTP 501 - Not Implemented: the server does not support this operation.
|
||||||
|
"""
|
||||||
|
http_status = 501
|
||||||
|
message = "Not Implemented"
|
||||||
|
|
||||||
|
|
||||||
|
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
||||||
|
# so we can do this:
|
||||||
|
# _code_map = dict((c.http_status, c)
|
||||||
|
# for c in ClientException.__subclasses__())
|
||||||
|
#
|
||||||
|
# Instead, we have to hardcode it:
|
||||||
|
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||||
|
Forbidden, NotFound, OverLimit, HTTPNotImplemented])
|
||||||
|
|
||||||
|
|
||||||
|
def from_response(response, body):
|
||||||
|
"""
|
||||||
|
Return an instance of an ClientException or subclass
|
||||||
|
based on an httplib2 response.
|
||||||
|
|
||||||
|
Usage::
|
||||||
|
|
||||||
|
resp, body = http.request(...)
|
||||||
|
if resp.status != 200:
|
||||||
|
raise exception_from_response(resp, body)
|
||||||
|
"""
|
||||||
|
cls = _code_map.get(response.status, ClientException)
|
||||||
|
if body:
|
||||||
|
message = "n/a"
|
||||||
|
details = "n/a"
|
||||||
|
if hasattr(body, 'keys'):
|
||||||
|
error = body[body.keys()[0]]
|
||||||
|
message = error.get('message', None)
|
||||||
|
details = error.get('details', None)
|
||||||
|
return cls(code=response.status, message=message, details=details)
|
||||||
|
else:
|
||||||
|
return cls(code=response.status)
|
|
@ -0,0 +1,53 @@
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011, Piston Cloud Computing, Inc.
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
#
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceCatalog(object):
|
||||||
|
"""Helper methods for dealing with a Keystone Service Catalog."""
|
||||||
|
|
||||||
|
def __init__(self, resource_dict):
|
||||||
|
self.catalog = resource_dict
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
return self.catalog['token']['id']
|
||||||
|
|
||||||
|
def url_for(self, attr=None, filter_value=None,
|
||||||
|
service_type='identity', endpoint_type='publicURL'):
|
||||||
|
"""Fetch an endpoint from the service catalog.
|
||||||
|
|
||||||
|
Fetch the specified endpoint from the service catalog for
|
||||||
|
a particular endpoint attribute. If no attribute is given, return
|
||||||
|
the first endpoint of the specified type.
|
||||||
|
|
||||||
|
See tests for a sample service catalog.
|
||||||
|
"""
|
||||||
|
catalog = self.catalog.get('serviceCatalog', [])
|
||||||
|
|
||||||
|
for service in catalog:
|
||||||
|
if service['type'] != service_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
endpoints = service['endpoints']
|
||||||
|
for endpoint in endpoints:
|
||||||
|
if not filter_value or endpoint[attr] == filter_value:
|
||||||
|
return endpoint[endpoint_type]
|
||||||
|
|
||||||
|
raise exceptions.EndpointNotFound('Endpoint not found.')
|
|
@ -0,0 +1,228 @@
|
||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Command-line interface to the OpenStack Keystone API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import httplib2
|
||||||
|
import os
|
||||||
|
import prettytable
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from keystoneclient import exceptions as exc
|
||||||
|
from keystoneclient import utils
|
||||||
|
from keystoneclient.v2_0 import shell as shell_v2_0
|
||||||
|
|
||||||
|
|
||||||
|
def env(e):
|
||||||
|
return os.environ.get(e, '')
|
||||||
|
|
||||||
|
|
||||||
|
class OpenStackIdentityShell(object):
|
||||||
|
|
||||||
|
def get_base_parser(self):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='keystone',
|
||||||
|
description=__doc__.strip(),
|
||||||
|
epilog='See "keystone help COMMAND" '\
|
||||||
|
'for help on a specific command.',
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=OpenStackHelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Global arguments
|
||||||
|
parser.add_argument('-h', '--help',
|
||||||
|
action='help',
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('--debug',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--username',
|
||||||
|
default=env('KEYSTONE_USERNAME'),
|
||||||
|
help='Defaults to env[KEYSTONE_USERNAME].')
|
||||||
|
|
||||||
|
parser.add_argument('--apikey',
|
||||||
|
default=env('KEYSTONE_API_KEY'),
|
||||||
|
help='Defaults to env[KEYSTONE_API_KEY].')
|
||||||
|
|
||||||
|
parser.add_argument('--projectid',
|
||||||
|
default=env('KEYSTONE_PROJECT_ID'),
|
||||||
|
help='Defaults to env[KEYSTONE_PROJECT_ID].')
|
||||||
|
|
||||||
|
parser.add_argument('--url',
|
||||||
|
default=env('KEYSTONE_URL'),
|
||||||
|
help='Defaults to env[KEYSTONE_URL].')
|
||||||
|
|
||||||
|
parser.add_argument('--region_name',
|
||||||
|
default=env('KEYSTONE_REGION_NAME'),
|
||||||
|
help='Defaults to env[KEYSTONE_REGION_NAME].')
|
||||||
|
|
||||||
|
parser.add_argument('--version',
|
||||||
|
default=env('KEYSTONE_VERSION'),
|
||||||
|
help='Accepts 1.0 or 1.1, defaults to env[KEYSTONE_VERSION].')
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def get_subcommand_parser(self, version):
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
|
||||||
|
self.subcommands = {}
|
||||||
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||||
|
|
||||||
|
try:
|
||||||
|
actions_module = {
|
||||||
|
'2.0': shell_v2_0,
|
||||||
|
}[version]
|
||||||
|
except KeyError:
|
||||||
|
actions_module = shell_v2_0
|
||||||
|
|
||||||
|
self._find_actions(subparsers, actions_module)
|
||||||
|
self._find_actions(subparsers, self)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def _find_actions(self, subparsers, actions_module):
|
||||||
|
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||||
|
# I prefer to be hypen-separated instead of underscores.
|
||||||
|
command = attr[3:].replace('_', '-')
|
||||||
|
callback = getattr(actions_module, attr)
|
||||||
|
desc = callback.__doc__ or ''
|
||||||
|
help = desc.strip().split('\n')[0]
|
||||||
|
arguments = getattr(callback, 'arguments', [])
|
||||||
|
|
||||||
|
subparser = subparsers.add_parser(command,
|
||||||
|
help=help,
|
||||||
|
description=desc,
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=OpenStackHelpFormatter
|
||||||
|
)
|
||||||
|
subparser.add_argument('-h', '--help',
|
||||||
|
action='help',
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
self.subcommands[command] = subparser
|
||||||
|
for (args, kwargs) in arguments:
|
||||||
|
subparser.add_argument(*args, **kwargs)
|
||||||
|
subparser.set_defaults(func=callback)
|
||||||
|
|
||||||
|
def main(self, argv):
|
||||||
|
# Parse args once to find version
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
(options, args) = parser.parse_known_args(argv)
|
||||||
|
|
||||||
|
# build available subcommands based on version
|
||||||
|
subcommand_parser = self.get_subcommand_parser(options.version)
|
||||||
|
self.parser = subcommand_parser
|
||||||
|
|
||||||
|
# Parse args again and call whatever callback was selected
|
||||||
|
args = subcommand_parser.parse_args(argv)
|
||||||
|
|
||||||
|
# Deal with global arguments
|
||||||
|
if args.debug:
|
||||||
|
httplib2.debuglevel = 1
|
||||||
|
|
||||||
|
# Short-circuit and deal with help right away.
|
||||||
|
if args.func == self.do_help:
|
||||||
|
self.do_help(args)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
user, apikey, projectid, url, region_name = \
|
||||||
|
args.username, args.apikey, args.projectid, args.url, \
|
||||||
|
args.region_name
|
||||||
|
|
||||||
|
#FIXME(usrleon): Here should be restrict for project id same as
|
||||||
|
# for username or apikey but for compatibility it is not.
|
||||||
|
|
||||||
|
if not user:
|
||||||
|
raise exc.CommandError("You must provide a username, either"
|
||||||
|
"via --username or via "
|
||||||
|
"env[KEYSTONE_USERNAME]")
|
||||||
|
if not apikey:
|
||||||
|
raise exc.CommandError("You must provide an API key, either"
|
||||||
|
"via --apikey or via"
|
||||||
|
"env[KEYSTONE_API_KEY]")
|
||||||
|
if options.version and options.version != '1.0':
|
||||||
|
if not projectid:
|
||||||
|
raise exc.CommandError("You must provide an projectid, either"
|
||||||
|
"via --projectid or via"
|
||||||
|
"env[KEYSTONE_PROJECT_ID")
|
||||||
|
|
||||||
|
if not url:
|
||||||
|
raise exc.CommandError("You must provide a auth url, either"
|
||||||
|
"via --url or via"
|
||||||
|
"env[KEYSTONE_URL")
|
||||||
|
|
||||||
|
self.cs = self.get_api_class(options.version) \
|
||||||
|
(user, apikey, projectid, url,
|
||||||
|
region_name=region_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.cs.authenticate()
|
||||||
|
except exc.Unauthorized:
|
||||||
|
raise exc.CommandError("Invalid OpenStack Keystone credentials.")
|
||||||
|
except exc.AuthorizationFailure:
|
||||||
|
raise exc.CommandError("Unable to authorize user")
|
||||||
|
|
||||||
|
args.func(self.cs, args)
|
||||||
|
|
||||||
|
def get_api_class(self, version):
|
||||||
|
try:
|
||||||
|
return {
|
||||||
|
"2.0": shell_v2_0.CLIENT_CLASS,
|
||||||
|
}[version]
|
||||||
|
except KeyError:
|
||||||
|
return shell_v2_0.CLIENT_CLASS
|
||||||
|
|
||||||
|
@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 args.command:
|
||||||
|
if args.command in self.subcommands:
|
||||||
|
self.subcommands[args.command].print_help()
|
||||||
|
else:
|
||||||
|
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||||
|
args.command)
|
||||||
|
else:
|
||||||
|
self.parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
# I'm picky about my shell help.
|
||||||
|
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||||
|
def start_section(self, heading):
|
||||||
|
# Title-case the headings
|
||||||
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||||
|
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
OpenStackIdentityShell().main(sys.argv[1:])
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
if httplib2.debuglevel == 1:
|
||||||
|
raise # dump stack.
|
||||||
|
else:
|
||||||
|
print >> sys.stderr, e
|
||||||
|
sys.exit(1)
|
|
@ -0,0 +1,69 @@
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import prettytable
|
||||||
|
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
|
||||||
|
|
||||||
|
# Decorator for cli-args
|
||||||
|
def arg(*args, **kwargs):
|
||||||
|
def _decorator(func):
|
||||||
|
# Because of the sematics of decorator composition if we just append
|
||||||
|
# to the options list positional options will appear to be backwards.
|
||||||
|
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||||
|
return func
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_choice_list(l):
|
||||||
|
return ', '.join("'%s'" % i for i in l)
|
||||||
|
|
||||||
|
|
||||||
|
def print_list(objs, fields, formatters={}):
|
||||||
|
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||||
|
pt.aligns = ['l' for f in fields]
|
||||||
|
|
||||||
|
for o in objs:
|
||||||
|
row = []
|
||||||
|
for field in fields:
|
||||||
|
if field in formatters:
|
||||||
|
row.append(formatters[field](o))
|
||||||
|
else:
|
||||||
|
field_name = field.lower().replace(' ', '_')
|
||||||
|
data = getattr(o, field_name, '')
|
||||||
|
row.append(data)
|
||||||
|
pt.add_row(row)
|
||||||
|
|
||||||
|
pt.printt(sortby=fields[0])
|
||||||
|
|
||||||
|
|
||||||
|
def print_dict(d):
|
||||||
|
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
||||||
|
pt.aligns = ['l', 'l']
|
||||||
|
[pt.add_row(list(r)) for r in d.iteritems()]
|
||||||
|
pt.printt(sortby='Property')
|
||||||
|
|
||||||
|
|
||||||
|
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 exceptions.NotFound:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# now try to get entity as uuid
|
||||||
|
try:
|
||||||
|
uuid.UUID(str(name_or_id))
|
||||||
|
return manager.get(name_or_id)
|
||||||
|
except (ValueError, exceptions.NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# finally try to find entity by name
|
||||||
|
try:
|
||||||
|
return manager.find(name=name_or_id)
|
||||||
|
except exceptions.NotFound:
|
||||||
|
msg = "No %s with a name or ID of '%s' exists." % \
|
||||||
|
(manager.resource_class.__name__.lower(), name_or_id)
|
||||||
|
raise exceptions.CommandError(msg)
|
|
@ -0,0 +1,2 @@
|
||||||
|
from keystoneclient.v2_0.client import Client
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
import urlparse
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from keystoneclient import client
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient import service_catalog
|
||||||
|
from keystoneclient.v2_0 import roles
|
||||||
|
from keystoneclient.v2_0 import services
|
||||||
|
from keystoneclient.v2_0 import tenants
|
||||||
|
from keystoneclient.v2_0 import tokens
|
||||||
|
from keystoneclient.v2_0 import users
|
||||||
|
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class Client(client.HTTPClient):
|
||||||
|
"""Client for the OpenStack Keystone v2.0 API.
|
||||||
|
|
||||||
|
:param string username: Username for authentication. (optional)
|
||||||
|
:param string password: Password for authentication. (optional)
|
||||||
|
:param string token: Token for authentication. (optional)
|
||||||
|
:param string project_id: Tenant/Project id. (optional)
|
||||||
|
:param string auth_url: Keystone service endpoint for authorization.
|
||||||
|
:param string region_name: Name of a region to select when choosing an
|
||||||
|
endpoint from the service catalog.
|
||||||
|
:param string endpoint: A user-supplied endpoint URL for the keystone service.
|
||||||
|
Lazy-authentication is possible for API service calls
|
||||||
|
if endpoint is set at instantiation.(optional)
|
||||||
|
:param integer timeout: Allows customization of the timeout for client
|
||||||
|
http requests. (optional)
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
>>> from keystoneclient.v2_0 import client
|
||||||
|
>>> keystone = client.Client(username=USER, password=PASS, project_id=TENANT, auth_url=KEYSTONE_URL)
|
||||||
|
>>> keystone.tenants.list()
|
||||||
|
...
|
||||||
|
>>> user = keystone.users.get(USER_ID)
|
||||||
|
>>> user.delete()
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, endpoint=None, **kwargs):
|
||||||
|
""" Initialize a new client for the Keystone v2.0 API. """
|
||||||
|
super(Client, self).__init__(endpoint=endpoint, **kwargs)
|
||||||
|
self.roles = roles.RoleManager(self)
|
||||||
|
self.services = services.ServiceManager(self)
|
||||||
|
self.tenants = tenants.TenantManager(self)
|
||||||
|
self.tokens = tokens.TokenManager(self)
|
||||||
|
self.users = users.UserManager(self)
|
||||||
|
# NOTE(gabriel): If we have a pre-defined endpoint then we can
|
||||||
|
# get away with lazy auth. Otherwise auth immediately.
|
||||||
|
if endpoint is None:
|
||||||
|
self.authenticate()
|
||||||
|
else:
|
||||||
|
self.management_url = endpoint
|
||||||
|
|
||||||
|
def authenticate(self):
|
||||||
|
""" Authenticate against the Keystone API.
|
||||||
|
|
||||||
|
Uses the data provided at instantiation to authenticate against
|
||||||
|
the Keystone server. This may use either a username and password
|
||||||
|
or token for authentication. If a tenant id was provided
|
||||||
|
then the resulting authenticated client will be scoped to that
|
||||||
|
tenant and contain a service catalog of available endpoints.
|
||||||
|
|
||||||
|
Returns ``True`` if authentication was successful.
|
||||||
|
"""
|
||||||
|
self.management_url = self.auth_url
|
||||||
|
try:
|
||||||
|
raw_token = self.tokens.authenticate(username=self.user,
|
||||||
|
password=self.password,
|
||||||
|
tenant=self.project_id,
|
||||||
|
token=self.auth_token,
|
||||||
|
return_raw=True)
|
||||||
|
self._extract_service_catalog(self.auth_url, raw_token)
|
||||||
|
return True
|
||||||
|
except (exceptions.AuthorizationFailure, exceptions.Unauthorized):
|
||||||
|
raise
|
||||||
|
except Exception, e:
|
||||||
|
_logger.exception("Authorization Failed.")
|
||||||
|
raise exceptions.AuthorizationFailure("Authorization Failed: %s" % e)
|
||||||
|
|
||||||
|
def _extract_service_catalog(self, url, body):
|
||||||
|
""" Set the client's service catalog from the response data. """
|
||||||
|
self.service_catalog = service_catalog.ServiceCatalog(body)
|
||||||
|
try:
|
||||||
|
self.auth_token = self.service_catalog.get_token()
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.AuthorizationFailure()
|
||||||
|
if self.project_id:
|
||||||
|
# Unscoped tokens don't return a service catalog
|
||||||
|
self.management_url = self.service_catalog.url_for(
|
||||||
|
attr='region',
|
||||||
|
filter_value=self.region_name)
|
||||||
|
return self.service_catalog
|
|
@ -0,0 +1,65 @@
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from keystoneclient import base
|
||||||
|
|
||||||
|
|
||||||
|
class Role(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Role %s>" % self._info
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
return self.manager.delete(self)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleManager(base.ManagerWithFind):
|
||||||
|
resource_class = Role
|
||||||
|
|
||||||
|
def get(self, role):
|
||||||
|
return self._get("/OS-KSADM/roles/%s" % base.getid(role), "role")
|
||||||
|
|
||||||
|
def create(self, name):
|
||||||
|
"""
|
||||||
|
Create a role.
|
||||||
|
"""
|
||||||
|
params = {"role": {"name": name}}
|
||||||
|
return self._create('/OS-KSADM/roles', params, "role")
|
||||||
|
|
||||||
|
def delete(self, role):
|
||||||
|
"""
|
||||||
|
Delete a role.
|
||||||
|
"""
|
||||||
|
return self._delete("/OS-KSADM/roles/%s" % base.getid(role))
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
"""
|
||||||
|
List all available roles.
|
||||||
|
"""
|
||||||
|
return self._list("/OS-KSADM/roles", "roles")
|
||||||
|
|
||||||
|
# FIXME(ja): finialize roles once finalized in keystone
|
||||||
|
# right now the only way to add/remove a tenant is to
|
||||||
|
# give them a role within a project
|
||||||
|
def get_user_role_refs(self, user_id):
|
||||||
|
return self._list("/users/%s/roleRefs" % user_id, "roles")
|
||||||
|
|
||||||
|
def add_user_to_tenant(self, tenant_id, user_id, role_id):
|
||||||
|
params = {"role": {"tenantId": tenant_id, "roleId": role_id}}
|
||||||
|
return self._create("/users/%s/roleRefs" % user_id, params, "role")
|
||||||
|
|
||||||
|
def remove_user_from_tenant(self, tenant_id, user_id, role_id):
|
||||||
|
params = {"role": {"tenantId": tenant_id, "roleId": role_id}}
|
||||||
|
return self._delete("/users/%s/roleRefs/%s" % (user_id, role_id))
|
|
@ -0,0 +1,39 @@
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from keystoneclient import base
|
||||||
|
|
||||||
|
|
||||||
|
class Service(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Service %s>" % self._info
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceManager(base.ManagerWithFind):
|
||||||
|
resource_class = Service
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
return self._list("/OS-KSADM/services", "OS-KSADM:services")
|
||||||
|
|
||||||
|
def get(self, id):
|
||||||
|
return self._get("/OS-KSADM/services/%s" % id, "OS-KSADM:service")
|
||||||
|
|
||||||
|
def create(self, name, service_type, description):
|
||||||
|
body = {"OS-KSADM:service": {'name': name, 'type':service_type, 'description': description}}
|
||||||
|
return self._create("/OS-KSADM/services", body, "OS-KSADM:service")
|
||||||
|
|
||||||
|
def delete(self, id):
|
||||||
|
return self._delete("/OS-KSADM/services/%s" % id)
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Copyright 2010 Jacob Kaplan-Moss
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
import os
|
||||||
|
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient import utils
|
||||||
|
from keystoneclient.v2_0 import client
|
||||||
|
|
||||||
|
|
||||||
|
CLIENT_CLASS = client.Client
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from keystoneclient import base
|
||||||
|
|
||||||
|
|
||||||
|
class Tenant(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Tenant %s>" % self._info
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
return self.manager.delete(self)
|
||||||
|
|
||||||
|
def update(self, description=None, enabled=None):
|
||||||
|
# FIXME(ja): set the attributes in this object if successful
|
||||||
|
return self.manager.update(self.id, description, enabled)
|
||||||
|
|
||||||
|
def add_user(self, user):
|
||||||
|
return self.manager.add_user_to_tenant(self.id, base.getid(user))
|
||||||
|
|
||||||
|
|
||||||
|
class TenantManager(base.ManagerWithFind):
|
||||||
|
resource_class = Tenant
|
||||||
|
|
||||||
|
def get(self, tenant_id):
|
||||||
|
return self._get("/tenants/%s" % tenant_id, "tenant")
|
||||||
|
|
||||||
|
def create(self, tenant_name, description=None, enabled=True):
|
||||||
|
"""
|
||||||
|
Create a new tenant.
|
||||||
|
"""
|
||||||
|
params = {"tenant": {"name": tenant_name,
|
||||||
|
"description": description,
|
||||||
|
"enabled": enabled}}
|
||||||
|
|
||||||
|
return self._create('/tenants', params, "tenant")
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
"""
|
||||||
|
Get a list of tenants.
|
||||||
|
:rtype: list of :class:`Tenant`
|
||||||
|
"""
|
||||||
|
return self._list("/tenants", "tenants")
|
||||||
|
|
||||||
|
def update(self, tenant_id, tenant_name=None, description=None, enabled=None):
|
||||||
|
"""
|
||||||
|
update a tenant with a new name and description
|
||||||
|
"""
|
||||||
|
body = {"tenant": {'id': tenant_id }}
|
||||||
|
if tenant_name is not None:
|
||||||
|
body['tenant']['name'] = tenant_name
|
||||||
|
if enabled is not None:
|
||||||
|
body['tenant']['enabled'] = enabled
|
||||||
|
if description:
|
||||||
|
body['tenant']['description'] = description
|
||||||
|
|
||||||
|
return self._update("/tenants/%s" % tenant_id, body, "tenant")
|
||||||
|
|
||||||
|
def delete(self, tenant):
|
||||||
|
"""
|
||||||
|
Delete a tenant.
|
||||||
|
"""
|
||||||
|
return self._delete("/tenants/%s" % (base.getid(tenant)))
|
|
@ -0,0 +1,37 @@
|
||||||
|
from keystoneclient import base
|
||||||
|
|
||||||
|
class Token(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Token %s>" % self._info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def id(self):
|
||||||
|
return self._info['token']['id']
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
return self._info['user'].get('username', None)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tenant_id(self):
|
||||||
|
return self._info['user'].get('tenantId', None)
|
||||||
|
|
||||||
|
|
||||||
|
class TokenManager(base.ManagerWithFind):
|
||||||
|
resource_class = Token
|
||||||
|
|
||||||
|
def authenticate(self, username=None, password=None, tenant=None,
|
||||||
|
token=None, return_raw=False):
|
||||||
|
if token and token != password:
|
||||||
|
params = {"auth": {"token": {"id": token}}}
|
||||||
|
elif username and password:
|
||||||
|
params = {"auth": {"passwordCredentials": {"username": username,
|
||||||
|
"password": password}}}
|
||||||
|
else:
|
||||||
|
raise ValueError('A username and password or token is required.')
|
||||||
|
if tenant:
|
||||||
|
params['auth']['tenantId'] = tenant
|
||||||
|
return self._create('/tokens', params, "access", return_raw=return_raw)
|
||||||
|
|
||||||
|
def endpoints(self, token):
|
||||||
|
return self._get("/tokens/%s/endpoints" % base.getid(token), "token")
|
|
@ -0,0 +1,101 @@
|
||||||
|
# Copyright 2011 OpenStack LLC.
|
||||||
|
# Copyright 2011 Nebula, Inc.
|
||||||
|
# All Rights Reserved.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
from keystoneclient import base
|
||||||
|
|
||||||
|
|
||||||
|
class User(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<User %s>" % self._info
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
return self.manager.delete(self)
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(base.ManagerWithFind):
|
||||||
|
resource_class = User
|
||||||
|
|
||||||
|
def get(self, user):
|
||||||
|
return self._get("/users/%s" % base.getid(user), "user")
|
||||||
|
|
||||||
|
def update_email(self, user, email):
|
||||||
|
"""
|
||||||
|
Update email
|
||||||
|
"""
|
||||||
|
# FIXME(ja): why do we have to send id in params and url?
|
||||||
|
params = {"user": {"id": base.getid(user),
|
||||||
|
"email": email }}
|
||||||
|
|
||||||
|
return self._update("/users/%s" % base.getid(user), params, "user")
|
||||||
|
|
||||||
|
def update_enabled(self, user, enabled):
|
||||||
|
"""
|
||||||
|
Update enabled-ness
|
||||||
|
"""
|
||||||
|
params = {"user": {"id": base.getid(user),
|
||||||
|
"enabled": enabled }}
|
||||||
|
|
||||||
|
self._update("/users/%s/enabled" % base.getid(user), params, "user")
|
||||||
|
|
||||||
|
def update_password(self, user, password):
|
||||||
|
"""
|
||||||
|
Update password
|
||||||
|
"""
|
||||||
|
params = {"user": {"id": base.getid(user),
|
||||||
|
"password": password }}
|
||||||
|
|
||||||
|
return self._update("/users/%s/password" % base.getid(user), params, "user")
|
||||||
|
|
||||||
|
def update_tenant(self, user, tenant):
|
||||||
|
"""
|
||||||
|
Update default tenant.
|
||||||
|
"""
|
||||||
|
params = {"user": {"id": base.getid(user),
|
||||||
|
"tenantId": base.getid(tenant) }}
|
||||||
|
|
||||||
|
# FIXME(ja): seems like a bad url - default tenant is an attribute
|
||||||
|
# not a subresource!???
|
||||||
|
return self._update("/users/%s/tenant" % base.getid(user), params, "user")
|
||||||
|
|
||||||
|
def create(self, name, password, email, tenant_id=None, enabled=True):
|
||||||
|
"""
|
||||||
|
Create a user.
|
||||||
|
"""
|
||||||
|
# FIXME(ja): email should be optional but keystone currently requires it
|
||||||
|
params = {"user": {"name": name,
|
||||||
|
"password": password,
|
||||||
|
"tenantId": tenant_id,
|
||||||
|
"email": email,
|
||||||
|
"enabled": enabled}}
|
||||||
|
return self._create('/users', params, "user")
|
||||||
|
|
||||||
|
def delete(self, user):
|
||||||
|
"""
|
||||||
|
Delete a user.
|
||||||
|
"""
|
||||||
|
return self._delete("/users/%s" % base.getid(user))
|
||||||
|
|
||||||
|
def list(self, tenant_id=None):
|
||||||
|
"""
|
||||||
|
Get a list of users (optionally limited to a tenant)
|
||||||
|
|
||||||
|
:rtype: list of :class:`User`
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not tenant_id:
|
||||||
|
return self._list("/users", "users")
|
||||||
|
else:
|
||||||
|
return self._list("/tenants/%s/users" % tenant_id, "users")
|
|
@ -0,0 +1,116 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright 2011, Piston Cloud Computing, 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.
|
||||||
|
|
||||||
|
function usage {
|
||||||
|
echo "Usage: $0 [OPTION] [nosearg1[=val]] [nosearg2[=val]]..."
|
||||||
|
echo
|
||||||
|
echo "Run python-keystoneclient test suite"
|
||||||
|
echo
|
||||||
|
echo " -f, --force Delete the virtualenv before running tests."
|
||||||
|
echo " -h, --help Print this usage message"
|
||||||
|
echo " -N, --no-virtual-env Don't use a virtualenv"
|
||||||
|
echo " -p, --pep8 Run pep8 in addition"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function die {
|
||||||
|
echo $@
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function process_args {
|
||||||
|
case "$1" in
|
||||||
|
-h|--help) usage && exit ;;
|
||||||
|
-p|--pep8) let just_pep8=1; let use_venv=0 ;;
|
||||||
|
-N|--no-virtual-env) let use_venv=0;;
|
||||||
|
-f|--force) let force=1;;
|
||||||
|
*) noseargs="$noseargs $1"
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function run-command {
|
||||||
|
res=$($@)
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
die "Command failed:", $res
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function install-dependency {
|
||||||
|
echo -n "installing $@..."
|
||||||
|
run-command "pip install -E $venv $@"
|
||||||
|
echo done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function build-venv-if-necessary {
|
||||||
|
if [ $force -eq 1 ]; then
|
||||||
|
echo -n "Removing virtualenv..."
|
||||||
|
rm -rf $venv
|
||||||
|
echo done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d $venv ]; then
|
||||||
|
echo -n # nothing to be done
|
||||||
|
else
|
||||||
|
if [ -z $(which virtualenv) ]; then
|
||||||
|
echo "Installing virtualenv"
|
||||||
|
run-command "easy_install virtualenv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -n "creating virtualenv..."
|
||||||
|
run-command "virtualenv -q --no-site-packages ${venv}"
|
||||||
|
echo done
|
||||||
|
|
||||||
|
for dep in $dependencies; do
|
||||||
|
install-dependency $dep
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function wrapper {
|
||||||
|
if [ $use_venv -eq 1 ]; then
|
||||||
|
build-venv-if-necessary
|
||||||
|
source "$(dirname $0)/${venv}/bin/activate" && $@
|
||||||
|
else
|
||||||
|
$@
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
dependencies="httplib2 argparse prettytable simplejson nose mock coverage mox"
|
||||||
|
force=0
|
||||||
|
venv=.keystoneclient-venv
|
||||||
|
use_venv=1
|
||||||
|
verbose=0
|
||||||
|
noseargs=
|
||||||
|
just_pep8=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
process_args $arg
|
||||||
|
done
|
||||||
|
|
||||||
|
NOSETESTS="nosetests ${noseargs}"
|
||||||
|
|
||||||
|
if [ $just_pep8 -ne 0 ]; then
|
||||||
|
wrapper "pep8 -r --show-pep8 keystoneclient tests"
|
||||||
|
else
|
||||||
|
wrapper $NOSETESTS
|
||||||
|
fi
|
|
@ -0,0 +1,13 @@
|
||||||
|
[nosetests]
|
||||||
|
cover-package = keystoneclient
|
||||||
|
cover-html = true
|
||||||
|
cover-erase = true
|
||||||
|
cover-inclusive = true
|
||||||
|
|
||||||
|
[build_sphinx]
|
||||||
|
source-dir = docs/
|
||||||
|
build-dir = docs/_build
|
||||||
|
all_files = 1
|
||||||
|
|
||||||
|
[upload_sphinx]
|
||||||
|
upload-dir = docs/_build/html
|
|
@ -0,0 +1,40 @@
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
|
|
||||||
|
def read(fname):
|
||||||
|
return open(os.path.join(os.path.dirname(__file__), fname)).read()
|
||||||
|
|
||||||
|
requirements = ['httplib2', 'argparse', 'prettytable']
|
||||||
|
if sys.version_info < (2, 6):
|
||||||
|
requirements.append('simplejson')
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name = "python-keystoneclient",
|
||||||
|
version = "2.7",
|
||||||
|
description = "Client library for OpenStack Keystone API",
|
||||||
|
long_description = read('README.rst'),
|
||||||
|
url = 'https://github.com/4P/python-keystoneclient',
|
||||||
|
license = 'Apache',
|
||||||
|
author = 'Nebula Inc, based on work by Rackspace and Jacob Kaplan-Moss',
|
||||||
|
author_email = 'gabriel.hurley@nebula.com',
|
||||||
|
packages = find_packages(exclude=['tests', 'tests.*']),
|
||||||
|
classifiers = [
|
||||||
|
'Development Status :: 4 - Beta',
|
||||||
|
'Environment :: Console',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'Intended Audience :: Information Technology',
|
||||||
|
'License :: OSI Approved :: Apache Software License',
|
||||||
|
'Operating System :: OS Independent',
|
||||||
|
'Programming Language :: Python',
|
||||||
|
],
|
||||||
|
install_requires = requirements,
|
||||||
|
|
||||||
|
tests_require = ["nose", "mock", "mox"],
|
||||||
|
test_suite = "nose.collector",
|
||||||
|
|
||||||
|
entry_points = {
|
||||||
|
'console_scripts': ['keystone = keystoneclient.shell:main']
|
||||||
|
}
|
||||||
|
)
|
|
@ -0,0 +1,48 @@
|
||||||
|
import mock
|
||||||
|
import mox
|
||||||
|
|
||||||
|
from keystoneclient import base
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient.v2_0 import roles
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class BaseTest(utils.TestCase):
|
||||||
|
|
||||||
|
def test_resource_repr(self):
|
||||||
|
r = base.Resource(None, dict(foo="bar", baz="spam"))
|
||||||
|
self.assertEqual(repr(r), "<Resource baz=spam, foo=bar>")
|
||||||
|
|
||||||
|
def test_getid(self):
|
||||||
|
self.assertEqual(base.getid(4), 4)
|
||||||
|
|
||||||
|
class TmpObject(object):
|
||||||
|
id = 4
|
||||||
|
self.assertEqual(base.getid(TmpObject), 4)
|
||||||
|
|
||||||
|
def test_resource_lazy_getattr(self):
|
||||||
|
self.client.get = self.mox.CreateMockAnything()
|
||||||
|
self.client.get('/OS-KSADM/roles/1').AndRaise(AttributeError)
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
f = roles.Role(self.client.roles, {'id': 1, 'name': 'Member'})
|
||||||
|
self.assertEqual(f.name, 'Member')
|
||||||
|
|
||||||
|
# Missing stuff still fails after a second get
|
||||||
|
self.assertRaises(AttributeError, getattr, f, 'blahblah')
|
||||||
|
|
||||||
|
def test_eq(self):
|
||||||
|
# Two resources of the same type with the same id: equal
|
||||||
|
r1 = base.Resource(None, {'id': 1, 'name': 'hi'})
|
||||||
|
r2 = base.Resource(None, {'id': 1, 'name': 'hello'})
|
||||||
|
self.assertEqual(r1, r2)
|
||||||
|
|
||||||
|
# Two resoruces of different types: never equal
|
||||||
|
r1 = base.Resource(None, {'id': 1})
|
||||||
|
r2 = roles.Role(None, {'id': 1})
|
||||||
|
self.assertNotEqual(r1, r2)
|
||||||
|
|
||||||
|
# Two resources with no ID: equal if their info is equal
|
||||||
|
r1 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||||
|
r2 = base.Resource(None, {'name': 'joe', 'age': 12})
|
||||||
|
self.assertEqual(r1, r2)
|
|
@ -0,0 +1,63 @@
|
||||||
|
import httplib2
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from keystoneclient import client
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
fake_response = httplib2.Response({"status": 200})
|
||||||
|
fake_body = '{"hi": "there"}'
|
||||||
|
mock_request = mock.Mock(return_value=(fake_response, fake_body))
|
||||||
|
|
||||||
|
|
||||||
|
def get_client():
|
||||||
|
cl = client.HTTPClient(username="username", password="apikey",
|
||||||
|
project_id="project_id", auth_url="auth_test")
|
||||||
|
return cl
|
||||||
|
|
||||||
|
|
||||||
|
def get_authed_client():
|
||||||
|
cl = get_client()
|
||||||
|
cl.management_url = "http://127.0.0.1:5000"
|
||||||
|
cl.auth_token = "token"
|
||||||
|
return cl
|
||||||
|
|
||||||
|
|
||||||
|
class ClientTest(utils.TestCase):
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
cl = get_authed_client()
|
||||||
|
|
||||||
|
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||||
|
@mock.patch('time.time', mock.Mock(return_value=1234))
|
||||||
|
def test_get_call():
|
||||||
|
resp, body = cl.get("/hi")
|
||||||
|
headers = {"X-Auth-Token": "token",
|
||||||
|
"X-Auth-Project-Id": "project_id",
|
||||||
|
"User-Agent": cl.USER_AGENT,
|
||||||
|
}
|
||||||
|
mock_request.assert_called_with("http://127.0.0.1:5000/hi?fresh=1234",
|
||||||
|
"GET", headers=headers)
|
||||||
|
# Automatic JSON parsing
|
||||||
|
self.assertEqual(body, {"hi": "there"})
|
||||||
|
|
||||||
|
test_get_call()
|
||||||
|
|
||||||
|
def test_post(self):
|
||||||
|
cl = get_authed_client()
|
||||||
|
|
||||||
|
@mock.patch.object(httplib2.Http, "request", mock_request)
|
||||||
|
def test_post_call():
|
||||||
|
cl.post("/hi", body=[1, 2, 3])
|
||||||
|
headers = {
|
||||||
|
"X-Auth-Token": "token",
|
||||||
|
"X-Auth-Project-Id": "project_id",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"User-Agent": cl.USER_AGENT
|
||||||
|
}
|
||||||
|
mock_request.assert_called_with("http://127.0.0.1:5000/hi", "POST",
|
||||||
|
headers=headers, body='[1, 2, 3]')
|
||||||
|
|
||||||
|
test_post_call()
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient import service_catalog
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
# Taken directly from keystone/content/common/samples/auth.json
|
||||||
|
# Do not edit this structure. Instead, grab the latest from there.
|
||||||
|
|
||||||
|
SERVICE_CATALOG = {
|
||||||
|
"access":{
|
||||||
|
"token":{
|
||||||
|
"id":"ab48a9efdfedb23ty3494",
|
||||||
|
"expires":"2010-11-01T03:32:15-05:00",
|
||||||
|
"tenant":{
|
||||||
|
"id": "345",
|
||||||
|
"name": "My Project"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"user":{
|
||||||
|
"id":"123",
|
||||||
|
"name":"jqsmith",
|
||||||
|
"roles":[{
|
||||||
|
"id":"234",
|
||||||
|
"name":"compute:admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id":"235",
|
||||||
|
"name":"object-store:admin",
|
||||||
|
"tenantId":"1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles_links":[]
|
||||||
|
},
|
||||||
|
"serviceCatalog":[{
|
||||||
|
"name":"Cloud Servers",
|
||||||
|
"type":"compute",
|
||||||
|
"endpoints":[{
|
||||||
|
"tenantId":"1",
|
||||||
|
"publicURL":"https://compute.north.host/v1/1234",
|
||||||
|
"internalURL":"https://compute.north.host/v1/1234",
|
||||||
|
"region":"North",
|
||||||
|
"versionId":"1.0",
|
||||||
|
"versionInfo":"https://compute.north.host/v1.0/",
|
||||||
|
"versionList":"https://compute.north.host/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tenantId":"2",
|
||||||
|
"publicURL":"https://compute.north.host/v1.1/3456",
|
||||||
|
"internalURL":"https://compute.north.host/v1.1/3456",
|
||||||
|
"region":"North",
|
||||||
|
"versionId":"1.1",
|
||||||
|
"versionInfo":"https://compute.north.host/v1.1/",
|
||||||
|
"versionList":"https://compute.north.host/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endpoints_links":[]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name":"Cloud Files",
|
||||||
|
"type":"object-store",
|
||||||
|
"endpoints":[{
|
||||||
|
"tenantId":"11",
|
||||||
|
"publicURL":"https://compute.north.host/v1/blah-blah",
|
||||||
|
"internalURL":"https://compute.north.host/v1/blah-blah",
|
||||||
|
"region":"South",
|
||||||
|
"versionId":"1.0",
|
||||||
|
"versionInfo":"uri",
|
||||||
|
"versionList":"uri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tenantId":"2",
|
||||||
|
"publicURL":"https://compute.north.host/v1.1/blah-blah",
|
||||||
|
"internalURL":"https://compute.north.host/v1.1/blah-blah",
|
||||||
|
"region":"South",
|
||||||
|
"versionId":"1.1",
|
||||||
|
"versionInfo":"https://compute.north.host/v1.1/",
|
||||||
|
"versionList":"https://compute.north.host/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"endpoints_links":[{
|
||||||
|
"rel":"next",
|
||||||
|
"href":"https://identity.north.host/v2.0/endpoints?marker=2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"serviceCatalog_links":[{
|
||||||
|
"rel":"next",
|
||||||
|
"href":"https://identity.host/v2.0/endpoints?session=2hfh8Ar&marker=2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceCatalogTest(utils.TestCase):
|
||||||
|
def test_building_a_service_catalog(self):
|
||||||
|
sc = service_catalog.ServiceCatalog(SERVICE_CATALOG['access'])
|
||||||
|
|
||||||
|
self.assertEquals(sc.url_for(service_type='compute'),
|
||||||
|
"https://compute.north.host/v1/1234")
|
||||||
|
self.assertEquals(sc.url_for('tenantId', '1', service_type='compute'),
|
||||||
|
"https://compute.north.host/v1/1234")
|
||||||
|
self.assertEquals(sc.url_for('tenantId', '2', service_type='compute'),
|
||||||
|
"https://compute.north.host/v1.1/3456")
|
||||||
|
|
||||||
|
self.assertRaises(exceptions.EndpointNotFound,
|
||||||
|
sc.url_for, "region", "South", service_type='compute')
|
|
@ -0,0 +1,39 @@
|
||||||
|
import os
|
||||||
|
import mock
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
from keystoneclient import shell as openstack_shell
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class ShellTest(utils.TestCase):
|
||||||
|
|
||||||
|
# Patch os.environ to avoid required auth info.
|
||||||
|
def setUp(self):
|
||||||
|
global _old_env
|
||||||
|
fake_env = {
|
||||||
|
'KEYSTONE_USERNAME': 'username',
|
||||||
|
'KEYSTONE_API_KEY': 'password',
|
||||||
|
'KEYSTONE_PROJECT_ID': 'project_id',
|
||||||
|
'KEYSTONE_URL': 'http://127.0.0.1:5000',
|
||||||
|
}
|
||||||
|
_old_env, os.environ = os.environ, fake_env.copy()
|
||||||
|
|
||||||
|
# Make a fake shell object, a helping wrapper to call it, and a quick
|
||||||
|
# way of asserting that certain API calls were made.
|
||||||
|
global shell, _shell, assert_called, assert_called_anytime
|
||||||
|
_shell = openstack_shell.OpenStackIdentityShell()
|
||||||
|
shell = lambda cmd: _shell.main(cmd.split())
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
global _old_env
|
||||||
|
os.environ = _old_env
|
||||||
|
|
||||||
|
def test_help_unknown_command(self):
|
||||||
|
self.assertRaises(exceptions.CommandError, shell, 'help foofoo')
|
||||||
|
|
||||||
|
def test_debug(self):
|
||||||
|
httplib2.debuglevel = 0
|
||||||
|
shell('--debug help')
|
||||||
|
assert httplib2.debuglevel == 1
|
|
@ -0,0 +1,64 @@
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from keystoneclient import utils
|
||||||
|
from tests import utils as test_utils
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResource(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class FakeManager(object):
|
||||||
|
|
||||||
|
resource_class = FakeResource
|
||||||
|
|
||||||
|
resources = {
|
||||||
|
'1234': {'name': 'entity_one'},
|
||||||
|
'8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0': {'name': 'entity_two'},
|
||||||
|
'5678': {'name': '9876'}
|
||||||
|
}
|
||||||
|
|
||||||
|
def get(self, resource_id):
|
||||||
|
try:
|
||||||
|
return self.resources[str(resource_id)]
|
||||||
|
except KeyError:
|
||||||
|
raise exceptions.NotFound(resource_id)
|
||||||
|
|
||||||
|
def find(self, name=None):
|
||||||
|
for resource_id, resource in self.resources.items():
|
||||||
|
if resource['name'] == str(name):
|
||||||
|
return resource
|
||||||
|
raise exceptions.NotFound(name)
|
||||||
|
|
||||||
|
|
||||||
|
class FindResourceTestCase(test_utils.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(FindResourceTestCase, self).setUp()
|
||||||
|
self.manager = FakeManager()
|
||||||
|
|
||||||
|
def test_find_none(self):
|
||||||
|
self.assertRaises(exceptions.CommandError,
|
||||||
|
utils.find_resource,
|
||||||
|
self.manager,
|
||||||
|
'asdf')
|
||||||
|
|
||||||
|
def test_find_by_integer_id(self):
|
||||||
|
output = utils.find_resource(self.manager, 1234)
|
||||||
|
self.assertEqual(output, self.manager.resources['1234'])
|
||||||
|
|
||||||
|
def test_find_by_str_id(self):
|
||||||
|
output = utils.find_resource(self.manager, '1234')
|
||||||
|
self.assertEqual(output, self.manager.resources['1234'])
|
||||||
|
|
||||||
|
def test_find_by_uuid(self):
|
||||||
|
uuid = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0'
|
||||||
|
output = utils.find_resource(self.manager, uuid)
|
||||||
|
self.assertEqual(output, self.manager.resources[uuid])
|
||||||
|
|
||||||
|
def test_find_by_str_name(self):
|
||||||
|
output = utils.find_resource(self.manager, 'entity_one')
|
||||||
|
self.assertEqual(output, self.manager.resources['1234'])
|
||||||
|
|
||||||
|
def test_find_by_int_name(self):
|
||||||
|
output = utils.find_resource(self.manager, 9876)
|
||||||
|
self.assertEqual(output, self.manager.resources['5678'])
|
|
@ -0,0 +1,81 @@
|
||||||
|
import time
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
import mox
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import client
|
||||||
|
|
||||||
|
|
||||||
|
class TestCase(unittest.TestCase):
|
||||||
|
TEST_TENANT = '1'
|
||||||
|
TEST_TENANT_NAME = 'aTenant'
|
||||||
|
TEST_TOKEN = 'aToken'
|
||||||
|
TEST_USER = 'test'
|
||||||
|
TEST_URL = 'http://127.0.0.1:5000/v2.0'
|
||||||
|
TEST_ADMIN_URL = 'http://127.0.0.1:35357/v2.0'
|
||||||
|
|
||||||
|
TEST_SERVICE_CATALOG = [{
|
||||||
|
"endpoints": [{
|
||||||
|
"adminURL": "http://cdn.admin-nets.local:8774/v1.0",
|
||||||
|
"region": "RegionOne",
|
||||||
|
"internalURL": "http://127.0.0.1:8774/v1.0",
|
||||||
|
"publicURL": "http://cdn.admin-nets.local:8774/v1.0/"
|
||||||
|
}],
|
||||||
|
"type": "nova_compat",
|
||||||
|
"name": "nova_compat"
|
||||||
|
}, {
|
||||||
|
"endpoints": [{
|
||||||
|
"adminURL": "http://nova/novapi/admin",
|
||||||
|
"region": "RegionOne",
|
||||||
|
"internalURL": "http://nova/novapi/internal",
|
||||||
|
"publicURL": "http://nova/novapi/public"
|
||||||
|
}],
|
||||||
|
"type": "compute",
|
||||||
|
"name": "nova"
|
||||||
|
}, {
|
||||||
|
"endpoints": [{
|
||||||
|
"adminURL": "http://glance/glanceapi/admin",
|
||||||
|
"region": "RegionOne",
|
||||||
|
"internalURL": "http://glance/glanceapi/internal",
|
||||||
|
"publicURL": "http://glance/glanceapi/public"
|
||||||
|
}],
|
||||||
|
"type": "image",
|
||||||
|
"name": "glance"
|
||||||
|
}, {
|
||||||
|
"endpoints": [{
|
||||||
|
"adminURL": "http://127.0.0.1:35357/v2.0",
|
||||||
|
"region": "RegionOne",
|
||||||
|
"internalURL": "http://127.0.0.1:5000/v2.0",
|
||||||
|
"publicURL": "http://127.0.0.1:5000/v2.0"
|
||||||
|
}],
|
||||||
|
"type": "identity",
|
||||||
|
"name": "keystone"
|
||||||
|
}, {
|
||||||
|
"endpoints": [{
|
||||||
|
"adminURL": "http://swift/swiftapi/admin",
|
||||||
|
"region": "RegionOne",
|
||||||
|
"internalURL": "http://swift/swiftapi/internal",
|
||||||
|
"publicURL": "http://swift/swiftapi/public"
|
||||||
|
}],
|
||||||
|
"type": "object-store",
|
||||||
|
"name": "swift"
|
||||||
|
}]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestCase, self).setUp()
|
||||||
|
self.mox = mox.Mox()
|
||||||
|
self._original_time = time.time
|
||||||
|
time.time = lambda: 1234
|
||||||
|
httplib2.Http.request = self.mox.CreateMockAnything()
|
||||||
|
self.client = client.Client(username=self.TEST_USER,
|
||||||
|
token=self.TEST_TOKEN,
|
||||||
|
project_id=self.TEST_TENANT,
|
||||||
|
auth_url=self.TEST_URL,
|
||||||
|
endpoint=self.TEST_URL)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
time.time = self._original_time
|
||||||
|
super(TestCase, self).tearDown()
|
||||||
|
self.mox.UnsetStubs()
|
||||||
|
self.mox.VerifyAll()
|
|
@ -0,0 +1,207 @@
|
||||||
|
import httplib2
|
||||||
|
import json
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import client
|
||||||
|
from keystoneclient import exceptions
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
def to_http_response(resp_dict):
|
||||||
|
"""
|
||||||
|
Utility function to convert a python dictionary
|
||||||
|
(e.g. {'status':status, 'body': body, 'headers':headers}
|
||||||
|
to an httplib2 response.
|
||||||
|
"""
|
||||||
|
resp = httplib2.Response(resp_dict)
|
||||||
|
for k, v in resp_dict['headers'].items():
|
||||||
|
resp[k] = v
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
class AuthenticateAgainstKeystoneTests(utils.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(AuthenticateAgainstKeystoneTests, self).setUp()
|
||||||
|
self.TEST_RESPONSE_DICT = {
|
||||||
|
"access": {
|
||||||
|
"token": {
|
||||||
|
"expires": "12345",
|
||||||
|
"id": self.TEST_TOKEN
|
||||||
|
},
|
||||||
|
"serviceCatalog": self.TEST_SERVICE_CATALOG
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.TEST_REQUEST_BODY = {
|
||||||
|
"auth": {
|
||||||
|
"passwordCredentials": {
|
||||||
|
"username": self.TEST_USER,
|
||||||
|
"password": self.TEST_TOKEN,
|
||||||
|
},
|
||||||
|
"tenantId": self.TEST_TENANT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.TEST_REQUEST_HEADERS = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'python-keystoneclient'
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_authenticate_failure(self):
|
||||||
|
self.TEST_REQUEST_BODY['auth']['passwordCredentials']['password'] = 'bad_key'
|
||||||
|
self.TEST_REQUEST_HEADERS['X-Auth-Project-Id'] = '1'
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 401,
|
||||||
|
"body": json.dumps({"unauthorized": {
|
||||||
|
"message": "Unauthorized", "code": "401"}}),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Implicit retry on API calls, so it gets called twice
|
||||||
|
httplib2.Http.request(self.TEST_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
httplib2.Http.request(self.TEST_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
with self.assertRaises(exceptions.Unauthorized):
|
||||||
|
client.Client(username=self.TEST_USER,
|
||||||
|
password="bad_key",
|
||||||
|
project_id=self.TEST_TENANT,
|
||||||
|
auth_url=self.TEST_URL)
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_redirect(self):
|
||||||
|
self.TEST_REQUEST_HEADERS['X-Auth-Project-Id'] = '1'
|
||||||
|
correct_response = json.dumps(self.TEST_RESPONSE_DICT)
|
||||||
|
dict_responses = [
|
||||||
|
{"headers": {'location': self.TEST_ADMIN_URL + "/tokens"},
|
||||||
|
"status": 305,
|
||||||
|
"body": "Use proxy"},
|
||||||
|
{"headers": {},
|
||||||
|
"status": 200,
|
||||||
|
"body": correct_response}
|
||||||
|
]
|
||||||
|
responses = [(to_http_response(resp), resp['body']) for
|
||||||
|
resp in dict_responses]
|
||||||
|
|
||||||
|
httplib2.Http.request(self.TEST_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn(responses[0])
|
||||||
|
httplib2.Http.request(self.TEST_ADMIN_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn(responses[1])
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
cs = client.Client(username=self.TEST_USER,
|
||||||
|
password=self.TEST_TOKEN,
|
||||||
|
project_id=self.TEST_TENANT,
|
||||||
|
auth_url=self.TEST_URL)
|
||||||
|
|
||||||
|
self.assertEqual(cs.management_url,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3]
|
||||||
|
['endpoints'][0]["publicURL"])
|
||||||
|
self.assertEqual(cs.auth_token,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["token"]["id"])
|
||||||
|
|
||||||
|
def test_authenticate_success_password_scoped(self):
|
||||||
|
self.TEST_REQUEST_HEADERS['X-Auth-Project-Id'] = '1'
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_RESPONSE_DICT),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(self.TEST_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
cs = client.Client(username=self.TEST_USER,
|
||||||
|
password=self.TEST_TOKEN,
|
||||||
|
project_id=self.TEST_TENANT,
|
||||||
|
auth_url=self.TEST_URL)
|
||||||
|
self.assertEqual(cs.management_url,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3]
|
||||||
|
['endpoints'][0]["publicURL"])
|
||||||
|
self.assertEqual(cs.auth_token,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["token"]["id"])
|
||||||
|
|
||||||
|
def test_authenticate_success_password_unscoped(self):
|
||||||
|
del self.TEST_RESPONSE_DICT['access']['serviceCatalog']
|
||||||
|
del self.TEST_REQUEST_BODY['auth']['tenantId']
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_RESPONSE_DICT),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(self.TEST_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
cs = client.Client(username=self.TEST_USER,
|
||||||
|
password=self.TEST_TOKEN,
|
||||||
|
auth_url=self.TEST_URL)
|
||||||
|
self.assertEqual(cs.auth_token,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["token"]["id"])
|
||||||
|
self.assertFalse(cs.service_catalog.catalog.has_key('serviceCatalog'))
|
||||||
|
|
||||||
|
def test_authenticate_success_token_scoped(self):
|
||||||
|
del self.TEST_REQUEST_BODY['auth']['passwordCredentials']
|
||||||
|
self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN}
|
||||||
|
self.TEST_REQUEST_HEADERS['X-Auth-Project-Id'] = '1'
|
||||||
|
self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_RESPONSE_DICT),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(self.TEST_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
cs = client.Client(token=self.TEST_TOKEN,
|
||||||
|
project_id=self.TEST_TENANT,
|
||||||
|
auth_url=self.TEST_URL)
|
||||||
|
self.assertEqual(cs.management_url,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["serviceCatalog"][3]
|
||||||
|
['endpoints'][0]["publicURL"])
|
||||||
|
self.assertEqual(cs.auth_token,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["token"]["id"])
|
||||||
|
|
||||||
|
def test_authenticate_success_token_unscoped(self):
|
||||||
|
del self.TEST_REQUEST_BODY['auth']['passwordCredentials']
|
||||||
|
del self.TEST_REQUEST_BODY['auth']['tenantId']
|
||||||
|
del self.TEST_RESPONSE_DICT['access']['serviceCatalog']
|
||||||
|
self.TEST_REQUEST_BODY['auth']['token'] = {'id': self.TEST_TOKEN}
|
||||||
|
self.TEST_REQUEST_HEADERS['X-Auth-Token'] = self.TEST_TOKEN
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_RESPONSE_DICT),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(self.TEST_URL + "/tokens",
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(self.TEST_REQUEST_BODY),
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
cs = client.Client(token=self.TEST_TOKEN,
|
||||||
|
auth_url=self.TEST_URL)
|
||||||
|
self.assertEqual(cs.auth_token,
|
||||||
|
self.TEST_RESPONSE_DICT["access"]["token"]["id"])
|
||||||
|
self.assertFalse(cs.service_catalog.catalog.has_key('serviceCatalog'))
|
|
@ -0,0 +1,99 @@
|
||||||
|
import urlparse
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import roles
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class RoleTests(utils.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(RoleTests, self).setUp()
|
||||||
|
self.TEST_REQUEST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_POST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_ROLES = {
|
||||||
|
"roles": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "member",
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
req_body = {"role": {"name": "sysadmin",}}
|
||||||
|
resp_body = {"role": {"name": "sysadmin", "id": 3,}}
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(resp_body),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/roles'),
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(req_body),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
role = self.client.roles.create(req_body['role']['name'])
|
||||||
|
self.assertTrue(isinstance(role, roles.Role))
|
||||||
|
self.assertEqual(role.id, 3)
|
||||||
|
self.assertEqual(role.name, req_body['role']['name'])
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": ""
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/roles/1'),
|
||||||
|
'DELETE',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
self.client.roles.delete(1)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps({'role':self.TEST_ROLES['roles']['values'][0]}),
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/roles/1?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
role = self.client.roles.get(1)
|
||||||
|
self.assertTrue(isinstance(role, roles.Role))
|
||||||
|
self.assertEqual(role.id, 1)
|
||||||
|
self.assertEqual(role.name, 'admin')
|
||||||
|
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_ROLES),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/roles?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
role_list = self.client.roles.list()
|
||||||
|
[self.assertTrue(isinstance(r, roles.Role)) for r in role_list]
|
||||||
|
|
|
@ -0,0 +1,111 @@
|
||||||
|
import urlparse
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import services
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceTests(utils.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ServiceTests, self).setUp()
|
||||||
|
self.TEST_REQUEST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_POST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_SERVICES = {
|
||||||
|
"OS-KSADM:services": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"name": "nova",
|
||||||
|
"type": "compute",
|
||||||
|
"description": "Nova-compatible service.",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "keystone",
|
||||||
|
"type": "identity",
|
||||||
|
"description": "Keystone-compatible service.",
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
req_body = {"OS-KSADM:service": {"name": "swift",
|
||||||
|
"type": "object-store",
|
||||||
|
"description": "Swift-compatible service.",}}
|
||||||
|
resp_body = {"OS-KSADM:service": {"name": "swift",
|
||||||
|
"type": "object-store",
|
||||||
|
"description": "Swift-compatible service.",
|
||||||
|
"id": 3}}
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(resp_body),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/services'),
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(req_body),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
service = self.client.services.create(req_body['OS-KSADM:service']['name'],
|
||||||
|
req_body['OS-KSADM:service']['type'],
|
||||||
|
req_body['OS-KSADM:service']['description'])
|
||||||
|
self.assertTrue(isinstance(service, services.Service))
|
||||||
|
self.assertEqual(service.id, 3)
|
||||||
|
self.assertEqual(service.name, req_body['OS-KSADM:service']['name'])
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": ""
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/services/1'),
|
||||||
|
'DELETE',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
self.client.services.delete(1)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps({'OS-KSADM:service':self.TEST_SERVICES['OS-KSADM:services']['values'][0]}),
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/services/1?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
service = self.client.services.get(1)
|
||||||
|
self.assertTrue(isinstance(service, services.Service))
|
||||||
|
self.assertEqual(service.id, 1)
|
||||||
|
self.assertEqual(service.name, 'nova')
|
||||||
|
self.assertEqual(service.type, 'compute')
|
||||||
|
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_SERVICES),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/services?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
service_list = self.client.services.list()
|
||||||
|
[self.assertTrue(isinstance(r, services.Service)) for r in service_list]
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
import urlparse
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import tenants
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class TenantTests(utils.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TenantTests, self).setUp()
|
||||||
|
self.TEST_REQUEST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_POST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_TENANTS = {
|
||||||
|
"tenants": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"enabled": True,
|
||||||
|
"description": "A description change!",
|
||||||
|
"name": "invisible_to_admin",
|
||||||
|
"id": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": True,
|
||||||
|
"description": "None",
|
||||||
|
"name": "demo",
|
||||||
|
"id": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"enabled": True,
|
||||||
|
"description": "None",
|
||||||
|
"name": "admin",
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
req_body = {"tenant": {"name": "tenantX",
|
||||||
|
"description": "Like tenant 9, but better.",
|
||||||
|
"enabled": True,}}
|
||||||
|
resp_body = {"tenant": {"name": "tenantX",
|
||||||
|
"enabled": True,
|
||||||
|
"id": 4,
|
||||||
|
"description": "Like tenant 9, but better.",}}
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(resp_body),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants'),
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(req_body),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
tenant = self.client.tenants.create(req_body['tenant']['name'],
|
||||||
|
req_body['tenant']['description'],
|
||||||
|
req_body['tenant']['enabled'])
|
||||||
|
self.assertTrue(isinstance(tenant, tenants.Tenant))
|
||||||
|
self.assertEqual(tenant.id, 4)
|
||||||
|
self.assertEqual(tenant.name, "tenantX")
|
||||||
|
self.assertEqual(tenant.description, "Like tenant 9, but better.")
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": ""
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/1'),
|
||||||
|
'DELETE',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
self.client.tenants.delete(1)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps({'tenant':self.TEST_TENANTS['tenants']['values'][2]}),
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/1?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
t = self.client.tenants.get(1)
|
||||||
|
self.assertTrue(isinstance(t, tenants.Tenant))
|
||||||
|
self.assertEqual(t.id, 1)
|
||||||
|
self.assertEqual(t.name, 'admin')
|
||||||
|
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_TENANTS),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
tenant_list = self.client.tenants.list()
|
||||||
|
[self.assertTrue(isinstance(t, tenants.Tenant)) for t in tenant_list]
|
||||||
|
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
req_body = {"tenant": {"id": 4,
|
||||||
|
"name": "tenantX",
|
||||||
|
"description": "I changed you!",
|
||||||
|
"enabled": False,}}
|
||||||
|
resp_body = {"tenant": {"name": "tenantX",
|
||||||
|
"enabled": False,
|
||||||
|
"id": 4,
|
||||||
|
"description": "I changed you!",}}
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(resp_body),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/tenants/4'),
|
||||||
|
'PUT',
|
||||||
|
body=json.dumps(req_body),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
tenant = self.client.tenants.update(req_body['tenant']['id'],
|
||||||
|
req_body['tenant']['name'],
|
||||||
|
req_body['tenant']['description'],
|
||||||
|
req_body['tenant']['enabled'])
|
||||||
|
print tenant
|
||||||
|
self.assertTrue(isinstance(tenant, tenants.Tenant))
|
||||||
|
self.assertEqual(tenant.id, 4)
|
||||||
|
self.assertEqual(tenant.name, "tenantX")
|
||||||
|
self.assertEqual(tenant.description, "I changed you!")
|
||||||
|
self.assertFalse(tenant.enabled)
|
|
@ -0,0 +1,47 @@
|
||||||
|
import urlparse
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import tokens
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class TokenTests(utils.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(ServiceTests, self).setUp()
|
||||||
|
self.TEST_REQUEST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_POST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
'''
|
||||||
|
def test_create(self):
|
||||||
|
req_body = {"OS-KSADM:service": {"name": "swift",
|
||||||
|
"type": "object-store",
|
||||||
|
"description": "Swift-compatible service.",}}
|
||||||
|
resp_body = {"OS-KSADM:service": {"name": "swift",
|
||||||
|
"type": "object-store",
|
||||||
|
"description": "Swift-compatible service.",
|
||||||
|
"id": 3}}
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(resp_body),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/OS-KSADM/services'),
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(req_body),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
service = self.client.services.create(req_body['OS-KSADM:service']['name'],
|
||||||
|
req_body['OS-KSADM:service']['type'],
|
||||||
|
req_body['OS-KSADM:service']['description'])
|
||||||
|
self.assertTrue(isinstance(service, services.Service))
|
||||||
|
self.assertEqual(service.id, 3)
|
||||||
|
self.assertEqual(service.name, req_body['OS-KSADM:service']['name'])
|
||||||
|
'''
|
|
@ -0,0 +1,153 @@
|
||||||
|
import urlparse
|
||||||
|
import json
|
||||||
|
|
||||||
|
import httplib2
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import users
|
||||||
|
from tests import utils
|
||||||
|
|
||||||
|
|
||||||
|
class UserTests(utils.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(UserTests, self).setUp()
|
||||||
|
self.TEST_REQUEST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_POST_HEADERS = {'X-Auth-Project-Id': '1',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Auth-Token': 'aToken',
|
||||||
|
'User-Agent': 'python-keystoneclient',}
|
||||||
|
self.TEST_USERS = {
|
||||||
|
"users": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"email": "None",
|
||||||
|
"enabled": True,
|
||||||
|
"id": 1,
|
||||||
|
"name": "admin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": "None",
|
||||||
|
"enabled": True,
|
||||||
|
"id": 2,
|
||||||
|
"name": "demo"
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
req_body = {"user": {"name": "gabriel",
|
||||||
|
"password": "test",
|
||||||
|
"tenantId": 2,
|
||||||
|
"email": "test@example.com",
|
||||||
|
"enabled": True,}}
|
||||||
|
resp_body = {"user": {"name": "gabriel",
|
||||||
|
"enabled": True,
|
||||||
|
"tenantId": 2,
|
||||||
|
"id": 3,
|
||||||
|
"password": "test",
|
||||||
|
"email": "test@example.com"}}
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(resp_body),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users'),
|
||||||
|
'POST',
|
||||||
|
body=json.dumps(req_body),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
user = self.client.users.create(req_body['user']['name'],
|
||||||
|
req_body['user']['password'],
|
||||||
|
req_body['user']['email'],
|
||||||
|
tenant_id=req_body['user']['tenantId'],
|
||||||
|
enabled=req_body['user']['enabled'])
|
||||||
|
self.assertTrue(isinstance(user, users.User))
|
||||||
|
self.assertEqual(user.id, 3)
|
||||||
|
self.assertEqual(user.name, "gabriel")
|
||||||
|
self.assertEqual(user.email, "test@example.com")
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": ""
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/1'),
|
||||||
|
'DELETE',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
self.client.users.delete(1)
|
||||||
|
|
||||||
|
def test_get(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps({'user':self.TEST_USERS['users']['values'][0]}),
|
||||||
|
})
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/1?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
u = self.client.users.get(1)
|
||||||
|
self.assertTrue(isinstance(u, users.User))
|
||||||
|
self.assertEqual(u.id, 1)
|
||||||
|
self.assertEqual(u.name, 'admin')
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
resp = httplib2.Response({
|
||||||
|
"status": 200,
|
||||||
|
"body": json.dumps(self.TEST_USERS),
|
||||||
|
})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users?fresh=1234'),
|
||||||
|
'GET',
|
||||||
|
headers=self.TEST_REQUEST_HEADERS) \
|
||||||
|
.AndReturn((resp, resp['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
user_list = self.client.users.list()
|
||||||
|
[self.assertTrue(isinstance(u, users.User)) for u in user_list]
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
req_1 = {"user": {"password": "swordfish", "id": 2}}
|
||||||
|
req_2 = {"user": {"id": 2, "email": "gabriel@example.com"}}
|
||||||
|
req_3 = {"user": {"tenantId": 1, "id": 2}}
|
||||||
|
req_4 = {"user": {"enabled": False, "id": 2}}
|
||||||
|
# Keystone basically echoes these back... including the password :-/
|
||||||
|
resp_1 = httplib2.Response({"status": 200, "body": json.dumps(req_1),})
|
||||||
|
resp_2 = httplib2.Response({"status": 200, "body": json.dumps(req_2),})
|
||||||
|
resp_3 = httplib2.Response({"status": 200, "body": json.dumps(req_3),})
|
||||||
|
resp_4 = httplib2.Response({"status": 200, "body": json.dumps(req_3),})
|
||||||
|
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/2/password'),
|
||||||
|
'PUT',
|
||||||
|
body=json.dumps(req_1),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp_1, resp_1['body']))
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/2'),
|
||||||
|
'PUT',
|
||||||
|
body=json.dumps(req_2),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp_2, resp_2['body']))
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/2/tenant'),
|
||||||
|
'PUT',
|
||||||
|
body=json.dumps(req_3),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp_3, resp_3['body']))
|
||||||
|
httplib2.Http.request(urlparse.urljoin(self.TEST_URL, 'v2.0/users/2/enabled'),
|
||||||
|
'PUT',
|
||||||
|
body=json.dumps(req_4),
|
||||||
|
headers=self.TEST_POST_HEADERS) \
|
||||||
|
.AndReturn((resp_4, resp_4['body']))
|
||||||
|
self.mox.ReplayAll()
|
||||||
|
|
||||||
|
user = self.client.users.update_password(2, 'swordfish')
|
||||||
|
user = self.client.users.update_email(2, 'gabriel@example.com')
|
||||||
|
user = self.client.users.update_tenant(2, 1)
|
||||||
|
user = self.client.users.update_enabled(2, False)
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
git shortlog -se | cut -c8-
|
Loading…
Reference in New Issue