Initial commit.
This commit is contained in:
commit
17f6b83ee6
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
.coverage
|
||||
.keystoneclient-venv
|
||||
*,cover
|
||||
cover
|
||||
*.pyc
|
||||
.idea
|
||||
*.swp
|
||||
*~
|
||||
build
|
||||
dist
|
||||
python_keystoneclient.egg-info
|
18
AUTHORS
Normal file
18
AUTHORS
Normal file
@ -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>
|
77
HACKING
Normal file
77
HACKING
Normal file
@ -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
|
||||
|
||||
"""
|
209
LICENSE
Normal file
209
LICENSE
Normal file
@ -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.
|
3
MANIFEST.in
Normal file
3
MANIFEST.in
Normal file
@ -0,0 +1,3 @@
|
||||
include README.rst
|
||||
recursive-include docs *
|
||||
recursive-include tests *
|
93
README.rst
Normal file
93
README.rst
Normal file
@ -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.
|
1
docs/.gitignore
vendored
Normal file
1
docs/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
_build/
|
89
docs/Makefile
Normal file
89
docs/Makefile
Normal file
@ -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."
|
19
docs/api.rst
Normal file
19
docs/api.rst
Normal file
@ -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
|
200
docs/conf.py
Normal file
200
docs/conf.py
Normal file
@ -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}
|
36
docs/index.rst
Normal file
36
docs/index.rst
Normal file
@ -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`
|
||||
|
8
docs/ref/exceptions.rst
Normal file
8
docs/ref/exceptions.rst
Normal file
@ -0,0 +1,8 @@
|
||||
Exceptions
|
||||
==========
|
||||
|
||||
.. currentmodule:: keystoneclient.exceptions
|
||||
|
||||
.. automodule:: keystoneclient.exceptions
|
||||
:members:
|
||||
|
9
docs/ref/index.rst
Normal file
9
docs/ref/index.rst
Normal file
@ -0,0 +1,9 @@
|
||||
API Reference
|
||||
=============
|
||||
|
||||
The following API reference documents are available:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
exceptions
|
106
docs/releases.rst
Normal file
106
docs/releases.rst
Normal file
@ -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.
|
57
docs/shell.rst
Normal file
57
docs/shell.rst
Normal file
@ -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
keystoneclient/__init__.py
Normal file
0
keystoneclient/__init__.py
Normal file
191
keystoneclient/base.py
Normal file
191
keystoneclient/base.py
Normal file
@ -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
|
189
keystoneclient/client.py
Normal file
189
keystoneclient/client.py
Normal file
@ -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)
|
129
keystoneclient/exceptions.py
Normal file
129
keystoneclient/exceptions.py
Normal file
@ -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)
|
53
keystoneclient/service_catalog.py
Normal file
53
keystoneclient/service_catalog.py
Normal file
@ -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.')
|
228
keystoneclient/shell.py
Normal file
228
keystoneclient/shell.py
Normal file
@ -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)
|
69
keystoneclient/utils.py
Normal file
69
keystoneclient/utils.py
Normal file
@ -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)
|
2
keystoneclient/v2_0/__init__.py
Normal file
2
keystoneclient/v2_0/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
from keystoneclient.v2_0.client import Client
|
||||
|
111
keystoneclient/v2_0/client.py
Normal file
111
keystoneclient/v2_0/client.py
Normal file
@ -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
|
65
keystoneclient/v2_0/roles.py
Normal file
65
keystoneclient/v2_0/roles.py
Normal file
@ -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))
|
39
keystoneclient/v2_0/services.py
Normal file
39
keystoneclient/v2_0/services.py
Normal file
@ -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)
|
27
keystoneclient/v2_0/shell.py
Normal file
27
keystoneclient/v2_0/shell.py
Normal file
@ -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
|
||||
|
76
keystoneclient/v2_0/tenants.py
Normal file
76
keystoneclient/v2_0/tenants.py
Normal file
@ -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)))
|
37
keystoneclient/v2_0/tokens.py
Normal file
37
keystoneclient/v2_0/tokens.py
Normal file
@ -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")
|
101
keystoneclient/v2_0/users.py
Normal file
101
keystoneclient/v2_0/users.py
Normal file
@ -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")
|
116
run_tests.sh
Executable file
116
run_tests.sh
Executable file
@ -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
|
13
setup.cfg
Normal file
13
setup.cfg
Normal file
@ -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
|
40
setup.py
Normal file
40
setup.py
Normal file
@ -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
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
48
tests/test_base.py
Normal file
48
tests/test_base.py
Normal file
@ -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)
|
63
tests/test_http.py
Normal file
63
tests/test_http.py
Normal file
@ -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()
|
||||
|
108
tests/test_service_catalog.py
Normal file
108
tests/test_service_catalog.py
Normal file
@ -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')
|
39
tests/test_shell.py
Normal file
39
tests/test_shell.py
Normal file
@ -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
|
64
tests/test_utils.py
Normal file
64
tests/test_utils.py
Normal file
@ -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'])
|
81
tests/utils.py
Normal file
81
tests/utils.py
Normal file
@ -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
tests/v2_0/__init__.py
Normal file
0
tests/v2_0/__init__.py
Normal file
207
tests/v2_0/test_auth.py
Normal file
207
tests/v2_0/test_auth.py
Normal file
@ -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'))
|
99
tests/v2_0/test_roles.py
Normal file
99
tests/v2_0/test_roles.py
Normal file
@ -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]
|
||||
|
111
tests/v2_0/test_services.py
Normal file
111
tests/v2_0/test_services.py
Normal file
@ -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]
|
||||
|
150
tests/v2_0/test_tenants.py
Normal file
150
tests/v2_0/test_tenants.py
Normal file
@ -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)
|
47
tests/v2_0/test_tokens.py
Normal file
47
tests/v2_0/test_tokens.py
Normal file
@ -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'])
|
||||
'''
|
153
tests/v2_0/test_users.py
Normal file
153
tests/v2_0/test_users.py
Normal file
@ -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)
|
3
tools/generate_authors.sh
Executable file
3
tools/generate_authors.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
|
||||
git shortlog -se | cut -c8-
|
Loading…
x
Reference in New Issue
Block a user