Initial commit.

This commit is contained in:
Gabriel Hurley 2011-10-25 16:50:08 -07:00
commit 17f6b83ee6
49 changed files with 3603 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
.coverage
.keystoneclient-venv
*,cover
cover
*.pyc
.idea
*.swp
*~
build
dist
python_keystoneclient.egg-info

18
AUTHORS Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
include README.rst
recursive-include docs *
recursive-include tests *

93
README.rst Normal file
View 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
View File

@ -0,0 +1 @@
_build/

89
docs/Makefile Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,8 @@
Exceptions
==========
.. currentmodule:: keystoneclient.exceptions
.. automodule:: keystoneclient.exceptions
:members:

9
docs/ref/index.rst Normal file
View File

@ -0,0 +1,9 @@
API Reference
=============
The following API reference documents are available:
.. toctree::
:maxdepth: 1
exceptions

106
docs/releases.rst Normal file
View 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
View 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.

View File

191
keystoneclient/base.py Normal file
View 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
View 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)

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

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

View File

@ -0,0 +1,2 @@
from keystoneclient.v2_0.client import Client

View 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

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

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

View 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

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

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

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

48
tests/test_base.py Normal file
View 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
View 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()

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

207
tests/v2_0/test_auth.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,3 @@
#!/bin/bash
git shortlog -se | cut -c8-

8
tox.ini Normal file
View File

@ -0,0 +1,8 @@
[tox]
envlist = py25,py26,py27
[testenv]
deps = nose
mock
mox
commands = nosetests