Imported os-client-config as a subtree

Change-Id: If3d145980e4212eab1f0beb78e413717134349b6
This commit is contained in:
Monty Taylor 2017-09-20 10:21:58 -05:00
commit 96fd7a7b84
No known key found for this signature in database
GPG Key ID: 7BAE94BC7141A594
93 changed files with 6977 additions and 0 deletions

View File

@ -0,0 +1,7 @@
[run]
branch = True
source = os_client_config
omit = os_client_config/tests/*,os_client_config/openstack/*
[report]
ignore_errors = True

55
os-client-config/.gitignore vendored Normal file
View File

@ -0,0 +1,55 @@
*.py[cod]
.venv
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
cover
.coverage
.tox
nosetests.xml
.stestr/
.testrepository
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Complexity
output/*.html
output/*/index.html
# Sphinx
doc/build
# pbr generates these
AUTHORS
ChangeLog
# Editors
*~
.*.swp
.*sw?

View File

@ -0,0 +1,4 @@
[gerrit]
host=review.openstack.org
port=29418
project=openstack/os-client-config.git

View File

@ -0,0 +1,3 @@
# Format is:
# <preferred e-mail> <other e-mail 1>
# <preferred e-mail> <other e-mail 2>

View File

@ -0,0 +1,3 @@
[DEFAULT]
test_path=.
top_dir=./

View File

@ -0,0 +1,7 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
${PYTHON:-python} -m subunit.run discover -t ./ . $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@ -0,0 +1,16 @@
If you would like to contribute to the development of OpenStack,
you must follow the steps in this page:
http://docs.openstack.org/infra/manual/developers.html
Once those steps have been completed, changes to OpenStack
should be submitted for review via the Gerrit tool, following
the workflow documented at:
http://docs.openstack.org/infra/manual/developers.html#development-workflow
Pull requests submitted through GitHub will be ignored.
Bugs should be filed on Launchpad, not GitHub:
https://bugs.launchpad.net/os-client-config

View File

@ -0,0 +1,4 @@
os-client-config Style Commandments
===============================================
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest

175
os-client-config/LICENSE Normal file
View File

@ -0,0 +1,175 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.

View File

@ -0,0 +1,25 @@
================
os-client-config
================
.. image:: http://governance.openstack.org/badges/os-client-config.svg
:target: http://governance.openstack.org/reference/tags/index.html
`os-client-config` is a library for collecting client configuration for
using an OpenStack cloud in a consistent and comprehensive manner. It
will find cloud config for as few as 1 cloud and as many as you want to
put in a config file. It will read environment variables and config files,
and it also contains some vendor specific default values so that you don't
have to know extra info to use OpenStack
* If you have a config file, you will get the clouds listed in it
* If you have environment variables, you will get a cloud named `envvars`
* If you have neither, you will get a cloud named `defaults` with base defaults
Source
------
* Free software: Apache license
* Documentation: http://docs.openstack.org/os-client-config/latest
* Source: http://git.openstack.org/cgit/openstack/os-client-config
* Bugs: http://bugs.launchpad.net/os-client-config

View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import sys
import openstackdocstheme
sys.path.insert(0, 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',
'reno.sphinxext',
'openstackdocstheme',
]
# openstackdocstheme options
repository_name = 'openstack/os-client-config'
bug_project = 'os-client-config'
bug_tag = ''
html_last_updated_fmt = '%Y-%m-%d %H:%M'
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
# execute "export SPHINX_DEBUG=1" in your terminal to disable
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'os-client-config'
copyright = u'2015, various OpenStack developers'
# 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
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# -- 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_path = ["."]
# html_theme = '_theme'
# html_static_path = ['static']
html_theme = 'openstackdocs'
html_theme_path = [openstackdocstheme.get_html_theme_path()]
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass
# [howto/manual]).
latex_documents = [
('index',
'%s.tex' % project,
u'%s Documentation' % project,
u'OpenStack Foundation', 'manual'),
]
# Example configuration for intersphinx: refer to the Python standard library.
#intersphinx_mapping = {'http://docs.python.org/': None}

View File

@ -0,0 +1,4 @@
============
Contributing
============
.. include:: ../../../CONTRIBUTING.rst

View File

@ -0,0 +1,32 @@
================
os-client-config
================
.. image:: http://governance.openstack.org/badges/os-client-config.svg
:target: http://governance.openstack.org/reference/tags/index.html
`os-client-config` is a library for collecting client configuration for
using an OpenStack cloud in a consistent and comprehensive manner. It
will find cloud config for as few as 1 cloud and as many as you want to
put in a config file. It will read environment variables and config files,
and it also contains some vendor specific default values so that you don't
have to know extra info to use OpenStack
* If you have a config file, you will get the clouds listed in it
* If you have environment variables, you will get a cloud named `envvars`
* If you have neither, you will get a cloud named `defaults` with base defaults
.. toctree::
:maxdepth: 2
install/index
user/index
reference/index
contributor/index
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@ -0,0 +1,12 @@
============
Installation
============
At the command line::
$ pip install os-client-config
Or, if you have virtualenvwrapper installed::
$ mkvirtualenv os-client-config
$ pip install os-client-config

View File

@ -0,0 +1,10 @@
=============
API Reference
=============
.. module:: os_client_config
:synopsis: OpenStack client configuration
.. autoclass:: os_client_config.OpenStackConfig
:members:
:inherited-members:

View File

@ -0,0 +1,303 @@
===========================================
Configuring os-client-config Applications
===========================================
Environment Variables
---------------------
`os-client-config` honors all of the normal `OS_*` variables. It does not
provide backwards compatibility to service-specific variables such as
`NOVA_USERNAME`.
If you have OpenStack environment variables set, `os-client-config` will produce
a cloud config object named `envvars` containing your values from the
environment. If you don't like the name `envvars`, that's ok, you can override
it by setting `OS_CLOUD_NAME`.
Service specific settings, like the nova service type, are set with the
default service type as a prefix. For instance, to set a special service_type
for trove set
.. code-block:: bash
export OS_DATABASE_SERVICE_TYPE=rax:database
Config Files
------------
`os-client-config` will look for a file called `clouds.yaml` in the following
locations:
* Current Directory
* ~/.config/openstack
* /etc/openstack
The first file found wins.
You can also set the environment variable `OS_CLIENT_CONFIG_FILE` to an
absolute path of a file to look for and that location will be inserted at the
front of the file search list.
The keys are all of the keys you'd expect from `OS_*` - except lower case
and without the OS prefix. So, region name is set with `region_name`.
Service specific settings, like the nova service type, are set with the
default service type as a prefix. For instance, to set a special service_type
for trove (because you're using Rackspace) set:
.. code-block:: yaml
database_service_type: 'rax:database'
Site Specific File Locations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In addition to `~/.config/openstack` and `/etc/openstack` - some platforms
have other locations they like to put things. `os-client-config` will also
look in an OS specific config dir
* `USER_CONFIG_DIR`
* `SITE_CONFIG_DIR`
`USER_CONFIG_DIR` is different on Linux, OSX and Windows.
* Linux: `~/.config/openstack`
* OSX: `~/Library/Application Support/openstack`
* Windows: `C:\\Users\\USERNAME\\AppData\\Local\\OpenStack\\openstack`
`SITE_CONFIG_DIR` is different on Linux, OSX and Windows.
* Linux: `/etc/openstack`
* OSX: `/Library/Application Support/openstack`
* Windows: `C:\\ProgramData\\OpenStack\\openstack`
An example config file is probably helpful:
.. code-block:: yaml
clouds:
mtvexx:
profile: vexxhost
auth:
username: mordred@inaugust.com
password: XXXXXXXXX
project_name: mordred@inaugust.com
region_name: ca-ymq-1
dns_api_version: 1
mordred:
region_name: RegionOne
auth:
username: 'mordred'
password: XXXXXXX
project_name: 'shade'
auth_url: 'https://montytaylor-sjc.openstack.blueboxgrid.com:5001/v2.0'
infra:
profile: rackspace
auth:
username: openstackci
password: XXXXXXXX
project_id: 610275
regions:
- DFW
- ORD
- IAD
You may note a few things. First, since `auth_url` settings are silly
and embarrassingly ugly, known cloud vendor profile information is included and
may be referenced by name. One of the benefits of that is that `auth_url`
isn't the only thing the vendor defaults contain. For instance, since
Rackspace lists `rax:database` as the service type for trove, `os-client-config`
knows that so that you don't have to. In case the cloud vendor profile is not
available, you can provide one called `clouds-public.yaml`, following the same
location rules previously mentioned for the config files.
`regions` can be a list of regions. When you call `get_all_clouds`,
you'll get a cloud config object for each cloud/region combo.
As seen with `dns_service_type`, any setting that makes sense to be per-service,
like `service_type` or `endpoint` or `api_version` can be set by prefixing
the setting with the default service type. That might strike you funny when
setting `service_type` and it does me too - but that's just the world we live
in.
Auth Settings
-------------
Keystone has auth plugins - which means it's not possible to know ahead of time
which auth settings are needed. `os-client-config` sets the default plugin type
to `password`, which is what things all were before plugins came about. In
order to facilitate validation of values, all of the parameters that exist
as a result of a chosen plugin need to go into the auth dict. For password
auth, this includes `auth_url`, `username` and `password` as well as anything
related to domains, projects and trusts.
Splitting Secrets
-----------------
In some scenarios, such as configuration management controlled environments,
it might be easier to have secrets in one file and non-secrets in another.
This is fully supported via an optional file `secure.yaml` which follows all
the same location rules as `clouds.yaml`. It can contain anything you put
in `clouds.yaml` and will take precedence over anything in the `clouds.yaml`
file.
.. code-block:: yaml
# clouds.yaml
clouds:
internap:
profile: internap
auth:
username: api-55f9a00fb2619
project_name: inap-17037
regions:
- ams01
- nyj01
# secure.yaml
clouds:
internap:
auth:
password: XXXXXXXXXXXXXXXXX
SSL Settings
------------
When the access to a cloud is done via a secure connection, `os-client-config`
will always verify the SSL cert by default. This can be disabled by setting
`verify` to `False`. In case the cert is signed by an unknown CA, a specific
cacert can be provided via `cacert`. **WARNING:** `verify` will always have
precedence over `cacert`, so when setting a CA cert but disabling `verify`, the
cloud cert will never be validated.
Client certs are also configurable. `cert` will be the client cert file
location. In case the cert key is not included within the client cert file,
its file location needs to be set via `key`.
.. code-block:: yaml
# clouds.yaml
clouds:
secure:
auth: ...
key: /home/myhome/client-cert.key
cert: /home/myhome/client-cert.crt
cacert: /home/myhome/ca.crt
insecure:
auth: ...
verify: False
Cache Settings
--------------
Accessing a cloud is often expensive, so it's quite common to want to do some
client-side caching of those operations. To facilitate that, `os-client-config`
understands passing through cache settings to dogpile.cache, with the following
behaviors:
* Listing no config settings means you get a null cache.
* `cache.expiration_time` and nothing else gets you memory cache.
* Otherwise, `cache.class` and `cache.arguments` are passed in
Different cloud behaviors are also differently expensive to deal with. If you
want to get really crazy and tweak stuff, you can specify different expiration
times on a per-resource basis by passing values, in seconds to an expiration
mapping keyed on the singular name of the resource. A value of `-1` indicates
that the resource should never expire.
`os-client-config` does not actually cache anything itself, but it collects
and presents the cache information so that your various applications that
are connecting to OpenStack can share a cache should you desire.
.. code-block:: yaml
cache:
class: dogpile.cache.pylibmc
expiration_time: 3600
arguments:
url:
- 127.0.0.1
expiration:
server: 5
flavor: -1
clouds:
mtvexx:
profile: vexxhost
auth:
username: mordred@inaugust.com
password: XXXXXXXXX
project_name: mordred@inaugust.com
region_name: ca-ymq-1
dns_api_version: 1
IPv6
----
IPv6 is the future, and you should always use it if your cloud supports it and
if your local network supports it. Both of those are easily detectable and all
friendly software should do the right thing. However, sometimes you might
exist in a location where you have an IPv6 stack, but something evil has
caused it to not actually function. In that case, there is a config option
you can set to unbreak you `force_ipv4`, or `OS_FORCE_IPV4` boolean
environment variable.
.. code-block:: yaml
client:
force_ipv4: true
clouds:
mtvexx:
profile: vexxhost
auth:
username: mordred@inaugust.com
password: XXXXXXXXX
project_name: mordred@inaugust.com
region_name: ca-ymq-1
dns_api_version: 1
monty:
profile: rax
auth:
username: mordred@inaugust.com
password: XXXXXXXXX
project_name: mordred@inaugust.com
region_name: DFW
The above snippet will tell client programs to prefer returning an IPv4
address.
Per-region settings
-------------------
Sometimes you have a cloud provider that has config that is common to the
cloud, but also with some things you might want to express on a per-region
basis. For instance, Internap provides a public and private network specific
to the user in each region, and putting the values of those networks into
config can make consuming programs more efficient.
To support this, the region list can actually be a list of dicts, and any
setting that can be set at the cloud level can be overridden for that
region.
.. code-block:: yaml
clouds:
internap:
profile: internap
auth:
password: XXXXXXXXXXXXXXXXX
username: api-55f9a00fb2619
project_name: inap-17037
regions:
- name: ams01
values:
networks:
- name: inap-17037-WAN1654
routes_externally: true
- name: inap-17037-LAN6745
- name: nyj01
values:
networks:
- name: inap-17037-WAN1654
routes_externally: true
- name: inap-17037-LAN6745

View File

@ -0,0 +1,12 @@
========================
Using os-client-config
========================
.. toctree::
:maxdepth: 2
configuration
using
vendor-support
network-config
releasenotes

View File

@ -0,0 +1,60 @@
==============
Network Config
==============
There are several different qualities that networks in OpenStack might have
that might not be able to be automatically inferred from the available
metadata. To help users navigate more complex setups, `os-client-config`
allows configuring a list of network metadata.
.. code-block:: yaml
clouds:
amazing:
networks:
- name: blue
routes_externally: true
- name: purple
routes_externally: true
default_interface: true
- name: green
routes_externally: false
- name: yellow
routes_externally: false
nat_destination: true
- name: chartreuse
routes_externally: false
routes_ipv6_externally: true
- name: aubergine
routes_ipv4_externally: false
routes_ipv6_externally: true
Every entry must have a name field, which can hold either the name or the id
of the network.
`routes_externally` is a boolean field that labels the network as handling
north/south traffic off of the cloud. In a public cloud this might be thought
of as the "public" network, but in private clouds it's possible it might
be an RFC1918 address. In either case, it's provides IPs to servers that
things not on the cloud can use. This value defaults to `false`, which
indicates only servers on the same network can talk to it.
`routes_ipv4_externally` and `routes_ipv6_externally` are boolean fields to
help handle `routes_externally` in the case where a network has a split stack
with different values for IPv4 and IPv6. Either entry, if not given, defaults
to the value of `routes_externally`.
`default_interface` is a boolean field that indicates that the network is the
one that programs should use. It defaults to false. An example of needing to
use this value is a cloud with two private networks, and where a user is
running ansible in one of the servers to talk to other servers on the private
network. Because both networks are private, there would otherwise be no way
to determine which one should be used for the traffic. There can only be one
`default_interface` per cloud.
`nat_destination` is a boolean field that indicates which network floating
ips should be attached to. It defaults to false. Normally this can be inferred
by looking for a network that has subnets that have a gateway_ip. But it's
possible to have more than one network that satisfies that condition, so the
user might want to tell programs which one to pick. There can be only one
`nat_destination` per cloud.

View File

@ -0,0 +1,6 @@
=============
Release Notes
=============
Release notes for `os-client-config` can be found at
http://docs.openstack.org/releasenotes/os-client-config/

View File

@ -0,0 +1,182 @@
==========================================
Using os-client-config in an Application
==========================================
Usage
-----
The simplest and least useful thing you can do is:
.. code-block:: python
python -m os_client_config.config
Which will print out whatever if finds for your config. If you want to use
it from python, which is much more likely what you want to do, things like:
Get a named cloud.
.. code-block:: python
import os_client_config
cloud_config = os_client_config.OpenStackConfig().get_one_cloud(
'internap', region_name='ams01')
print(cloud_config.name, cloud_config.region, cloud_config.config)
Or, get all of the clouds.
.. code-block:: python
import os_client_config
cloud_config = os_client_config.OpenStackConfig().get_all_clouds()
for cloud in cloud_config:
print(cloud.name, cloud.region, cloud.config)
argparse
--------
If you're using os-client-config from a program that wants to process
command line options, there is a registration function to register the
arguments that both os-client-config and keystoneauth know how to deal
with - as well as a consumption argument.
.. code-block:: python
import argparse
import sys
import os_client_config
cloud_config = os_client_config.OpenStackConfig()
parser = argparse.ArgumentParser()
cloud_config.register_argparse_arguments(parser, sys.argv)
options = parser.parse_args()
cloud = cloud_config.get_one_cloud(argparse=options)
Constructing OpenStack SDK object
---------------------------------
If what you want to do is get an OpenStack SDK Connection and you want it to
do all the normal things related to clouds.yaml, `OS_` environment variables,
a helper function is provided. The following will get you a fully configured
`openstacksdk` instance.
.. code-block:: python
import os_client_config
sdk = os_client_config.make_sdk()
If you want to do the same thing but on a named cloud.
.. code-block:: python
import os_client_config
sdk = os_client_config.make_sdk(cloud='mtvexx')
If you want to do the same thing but also support command line parsing.
.. code-block:: python
import argparse
import os_client_config
sdk = os_client_config.make_sdk(options=argparse.ArgumentParser())
It should be noted that OpenStack SDK has ways to construct itself that allow
for additional flexibility. If the helper function here does not meet your
needs, you should see the `from_config` method of
`openstack.connection.Connection <http://developer.openstack.org/sdks/python/openstacksdk/users/guides/connect_from_config.html>`_
Constructing shade objects
--------------------------
If what you want to do is get a
`shade <http://docs.openstack.org/infra/shade/>`_ OpenStackCloud object, a
helper function that honors clouds.yaml and `OS_` environment variables is
provided. The following will get you a fully configured `OpenStackCloud`
instance.
.. code-block:: python
import os_client_config
cloud = os_client_config.make_shade()
If you want to do the same thing but on a named cloud.
.. code-block:: python
import os_client_config
cloud = os_client_config.make_shade(cloud='mtvexx')
If you want to do the same thing but also support command line parsing.
.. code-block:: python
import argparse
import os_client_config
cloud = os_client_config.make_shade(options=argparse.ArgumentParser())
Constructing REST API Clients
-----------------------------
What if you want to make direct REST calls via a Session interface? You're
in luck. A similar interface is available as with `openstacksdk` and `shade`.
The main difference is that you need to specify which service you want to
talk to and `make_rest_client` will return you a keystoneauth Session object
that is mounted on the endpoint for the service you're looking for.
.. code-block:: python
import os_client_config
session = os_client_config.make_rest_client('compute', cloud='vexxhost')
response = session.get('/servers')
server_list = response.json()['servers']
Constructing Legacy Client objects
----------------------------------
If you want get an old-style Client object from a python-\*client library,
and you want it to do all the normal things related to clouds.yaml, `OS_`
environment variables, a helper function is also provided. The following
will get you a fully configured `novaclient` instance.
.. code-block:: python
import os_client_config
nova = os_client_config.make_client('compute')
If you want to do the same thing but on a named cloud.
.. code-block:: python
import os_client_config
nova = os_client_config.make_client('compute', cloud='mtvexx')
If you want to do the same thing but also support command line parsing.
.. code-block:: python
import argparse
import os_client_config
nova = os_client_config.make_client(
'compute', options=argparse.ArgumentParser())
If you want to get fancier than that in your python, then the rest of the
API is available to you. But often times, you just want to do the one thing.

View File

@ -0,0 +1,337 @@
==============
Vendor Support
==============
OpenStack presents deployers with many options, some of which can expose
differences to end users. `os-client-config` tries its best to collect
information about various things a user would need to know. The following
is a text representation of the vendor related defaults `os-client-config`
knows about.
Default Values
--------------
These are the default behaviors unless a cloud is configured differently.
* Identity uses `password` authentication
* Identity API Version is 2
* Image API Version is 2
* Volume API Version is 2
* Images must be in `qcow2` format
* Images are uploaded using PUT interface
* Public IPv4 is directly routable via DHCP from Neutron
* IPv6 is not provided
* Floating IPs are not required
* Floating IPs are provided by Neutron
* Security groups are provided by Neutron
* Vendor specific agents are not used
auro
----
https://api.auro.io:5000/v2.0
============== ================
Region Name Location
============== ================
van1 Vancouver, BC
============== ================
* Public IPv4 is provided via NAT with Neutron Floating IP
catalyst
--------
https://api.cloud.catalyst.net.nz:5000/v2.0
============== ================
Region Name Location
============== ================
nz-por-1 Porirua, NZ
nz_wlg_2 Wellington, NZ
============== ================
* Image API Version is 1
* Images must be in `raw` format
* Volume API Version is 1
citycloud
---------
https://identity1.citycloud.com:5000/v3/
============== ================
Region Name Location
============== ================
Buf1 Buffalo, NY
Fra1 Frankfurt, DE
Kna1 Karlskrona, SE
La1 Los Angeles, CA
Lon1 London, UK
Sto2 Stockholm, SE
============== ================
* Identity API Version is 3
* Public IPv4 is provided via NAT with Neutron Floating IP
* Volume API Version is 1
conoha
------
https://identity.%(region_name)s.conoha.io
============== ================
Region Name Location
============== ================
tyo1 Tokyo, JP
sin1 Singapore
sjc1 San Jose, CA
============== ================
* Image upload is not supported
datacentred
-----------
https://compute.datacentred.io:5000
============== ================
Region Name Location
============== ================
sal01 Manchester, UK
============== ================
* Image API Version is 1
dreamcompute
------------
https://iad2.dream.io:5000
============== ================
Region Name Location
============== ================
RegionOne Ashburn, VA
============== ================
* Identity API Version is 3
* Images must be in `raw` format
* IPv6 is provided to every server
dreamhost
---------
Deprecated, please use dreamcompute
https://keystone.dream.io/v2.0
============== ================
Region Name Location
============== ================
RegionOne Ashburn, VA
============== ================
* Images must be in `raw` format
* Public IPv4 is provided via NAT with Neutron Floating IP
* IPv6 is provided to every server
otc
---
https://iam.%(region_name)s.otc.t-systems.com/v3
============== ================
Region Name Location
============== ================
eu-de Germany
============== ================
* Identity API Version is 3
* Images must be in `vhd` format
* Public IPv4 is provided via NAT with Neutron Floating IP
elastx
------
https://ops.elastx.net:5000/v2.0
============== ================
Region Name Location
============== ================
regionOne Stockholm, SE
============== ================
* Public IPv4 is provided via NAT with Neutron Floating IP
entercloudsuite
---------------
https://api.entercloudsuite.com/v2.0
============== ================
Region Name Location
============== ================
nl-ams1 Amsterdam, NL
it-mil1 Milan, IT
de-fra1 Frankfurt, DE
============== ================
* Image API Version is 1
* Volume API Version is 1
fuga
----
https://identity.api.fuga.io:5000
============== ================
Region Name Location
============== ================
cystack Netherlands
============== ================
* Identity API Version is 3
* Volume API Version is 3
internap
--------
https://identity.api.cloud.iweb.com/v2.0
============== ================
Region Name Location
============== ================
ams01 Amsterdam, NL
da01 Dallas, TX
nyj01 New York, NY
sin01 Singapore
sjc01 San Jose, CA
============== ================
* Floating IPs are not supported
ovh
---
https://auth.cloud.ovh.net/v2.0
============== ================
Region Name Location
============== ================
BHS1 Beauharnois, QC
SBG1 Strassbourg, FR
GRA1 Gravelines, FR
============== ================
* Images may be in `raw` format. The `qcow2` default is also supported
* Floating IPs are not supported
rackspace
---------
https://identity.api.rackspacecloud.com/v2.0/
============== ================
Region Name Location
============== ================
DFW Dallas, TX
HKG Hong Kong
IAD Washington, D.C.
LON London, UK
ORD Chicago, IL
SYD Sydney, NSW
============== ================
* Database Service Type is `rax:database`
* Compute Service Name is `cloudServersOpenStack`
* Images must be in `vhd` format
* Images must be uploaded using the Glance Task Interface
* Floating IPs are not supported
* Public IPv4 is directly routable via static config by Nova
* IPv6 is provided to every server
* Security groups are not supported
* Uploaded Images need properties to not use vendor agent::
:vm_mode: hvm
:xenapi_use_agent: False
* Volume API Version is 1
* While passwords are recommended for use, API keys do work as well.
The `rackspaceauth` python package must be installed, and then the following
can be added to clouds.yaml::
auth:
username: myusername
api_key: myapikey
auth_type: rackspace_apikey
switchengines
-------------
https://keystone.cloud.switch.ch:5000/v2.0
============== ================
Region Name Location
============== ================
LS Lausanne, CH
ZH Zurich, CH
============== ================
* Images must be in `raw` format
* Images must be uploaded using the Glance Task Interface
* Volume API Version is 1
ultimum
-------
https://console.ultimum-cloud.com:5000/v2.0
============== ================
Region Name Location
============== ================
RegionOne Prague, CZ
============== ================
* Volume API Version is 1
unitedstack
-----------
https://identity.api.ustack.com/v3
============== ================
Region Name Location
============== ================
bj1 Beijing, CN
gd1 Guangdong, CN
============== ================
* Identity API Version is 3
* Images must be in `raw` format
* Volume API Version is 1
vexxhost
--------
http://auth.vexxhost.net
============== ================
Region Name Location
============== ================
ca-ymq-1 Montreal, QC
============== ================
* DNS API Version is 1
* Identity API Version is 3
zetta
-----
https://identity.api.zetta.io/v3
============== ================
Region Name Location
============== ================
no-osl1 Oslo, NO
============== ================
* DNS API Version is 2
* Identity API Version is 3

View File

@ -0,0 +1,111 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
import pbr.version
from os_client_config import cloud_config
from os_client_config.config import OpenStackConfig # noqa
__version__ = pbr.version.VersionInfo('os_client_config').version_string()
_config = None
def get_config(
service_key=None, options=None,
app_name=None, app_version=None,
**kwargs):
load_yaml_config = kwargs.pop('load_yaml_config', True)
global _config
if not _config:
_config = OpenStackConfig(
load_yaml_config=load_yaml_config,
app_name=app_name, app_version=app_version)
if options:
_config.register_argparse_arguments(options, sys.argv, service_key)
parsed_options = options.parse_known_args(sys.argv)
else:
parsed_options = None
return _config.get_one_cloud(options=parsed_options, **kwargs)
def make_rest_client(
service_key, options=None,
app_name=None, app_version=None,
**kwargs):
"""Simple wrapper function. It has almost no features.
This will get you a raw requests Session Adapter that is mounted
on the given service from the keystone service catalog. If you leave
off cloud and region_name, it will assume that you've got env vars
set, but if you give them, it'll use clouds.yaml as you'd expect.
This function is deliberately simple. It has no flexibility. If you
want flexibility, you can make a cloud config object and call
get_session_client on it. This function is to make it easy to poke
at OpenStack REST APIs with a properly configured keystone session.
"""
cloud = get_config(
service_key=service_key, options=options,
app_name=app_name, app_version=app_version,
**kwargs)
return cloud.get_session_client(service_key)
# Backwards compat - simple_client was a terrible name
simple_client = make_rest_client
# Backwards compat - session_client was a terrible name
session_client = make_rest_client
def make_client(service_key, constructor=None, options=None, **kwargs):
"""Simple wrapper for getting a client instance from a client lib.
OpenStack Client Libraries all have a fairly consistent constructor
interface which os-client-config supports. In the simple case, there
is one and only one right way to construct a client object. If as a user
you don't want to do fancy things, just use this. It honors OS_ environment
variables and clouds.yaml - and takes as **kwargs anything you'd expect
to pass in.
"""
cloud = get_config(service_key=service_key, options=options, **kwargs)
if not constructor:
constructor = cloud_config._get_client(service_key)
return cloud.get_legacy_client(service_key, constructor)
def make_sdk(options=None, **kwargs):
"""Simple wrapper for getting an OpenStack SDK Connection.
For completeness, provide a mechanism that matches make_client and
make_rest_client. The heavy lifting here is done in openstacksdk.
:rtype: :class:`~openstack.connection.Connection`
"""
from openstack import connection
cloud = get_config(options=options, **kwargs)
return connection.from_config(cloud_config=cloud, options=options)
def make_shade(options=None, **kwargs):
"""Simple wrapper for getting a Shade OpenStackCloud object
A mechanism that matches make_sdk, make_client and make_rest_client.
:rtype: :class:`~shade.OpenStackCloud`
"""
import shade
cloud = get_config(options=options, **kwargs)
return shade.OpenStackCloud(cloud_config=cloud, **kwargs)

View File

@ -0,0 +1,28 @@
# Copyright (c) 2015 IBM Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
class NullHandler(logging.Handler):
def emit(self, record):
pass
def setup_logging(name):
log = logging.getLogger(name)
if len(log.handlers) == 0:
h = NullHandler()
log.addHandler(h)
return log

View File

@ -0,0 +1,558 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import importlib
import math
import warnings
from keystoneauth1 import adapter
import keystoneauth1.exceptions.catalog
from keystoneauth1 import session
import requestsexceptions
import os_client_config
from os_client_config import _log
from os_client_config import constructors
from os_client_config import exceptions
def _get_client(service_key):
class_mapping = constructors.get_constructor_mapping()
if service_key not in class_mapping:
raise exceptions.OpenStackConfigException(
"Service {service_key} is unkown. Please pass in a client"
" constructor or submit a patch to os-client-config".format(
service_key=service_key))
mod_name, ctr_name = class_mapping[service_key].rsplit('.', 1)
lib_name = mod_name.split('.')[0]
try:
mod = importlib.import_module(mod_name)
except ImportError:
raise exceptions.OpenStackConfigException(
"Client for '{service_key}' was requested, but"
" {mod_name} was unable to be imported. Either import"
" the module yourself and pass the constructor in as an argument,"
" or perhaps you do not have python-{lib_name} installed.".format(
service_key=service_key,
mod_name=mod_name,
lib_name=lib_name))
try:
ctr = getattr(mod, ctr_name)
except AttributeError:
raise exceptions.OpenStackConfigException(
"Client for '{service_key}' was requested, but although"
" {mod_name} imported fine, the constructor at {fullname}"
" as not found. Please check your installation, we have no"
" clue what is wrong with your computer.".format(
service_key=service_key,
mod_name=mod_name,
fullname=class_mapping[service_key]))
return ctr
def _make_key(key, service_type):
if not service_type:
return key
else:
service_type = service_type.lower().replace('-', '_')
return "_".join([service_type, key])
class CloudConfig(object):
def __init__(self, name, region, config,
force_ipv4=False, auth_plugin=None,
openstack_config=None, session_constructor=None,
app_name=None, app_version=None):
self.name = name
self.region = region
self.config = config
self.log = _log.setup_logging(__name__)
self._force_ipv4 = force_ipv4
self._auth = auth_plugin
self._openstack_config = openstack_config
self._keystone_session = None
self._session_constructor = session_constructor or session.Session
self._app_name = app_name
self._app_version = app_version
def __getattr__(self, key):
"""Return arbitrary attributes."""
if key.startswith('os_'):
key = key[3:]
if key in [attr.replace('-', '_') for attr in self.config]:
return self.config[key]
else:
return None
def __iter__(self):
return self.config.__iter__()
def __eq__(self, other):
return (self.name == other.name and self.region == other.region
and self.config == other.config)
def __ne__(self, other):
return not self == other
def set_session_constructor(self, session_constructor):
"""Sets the Session constructor."""
self._session_constructor = session_constructor
def get_requests_verify_args(self):
"""Return the verify and cert values for the requests library."""
if self.config['verify'] and self.config['cacert']:
verify = self.config['cacert']
else:
verify = self.config['verify']
if self.config['cacert']:
warnings.warn(
"You are specifying a cacert for the cloud {0} but "
"also to ignore the host verification. The host SSL cert "
"will not be verified.".format(self.name))
cert = self.config.get('cert', None)
if cert:
if self.config['key']:
cert = (cert, self.config['key'])
return (verify, cert)
def get_services(self):
"""Return a list of service types we know something about."""
services = []
for key, val in self.config.items():
if (key.endswith('api_version')
or key.endswith('service_type')
or key.endswith('service_name')):
services.append("_".join(key.split('_')[:-2]))
return list(set(services))
def get_auth_args(self):
return self.config['auth']
def get_interface(self, service_type=None):
key = _make_key('interface', service_type)
interface = self.config.get('interface')
return self.config.get(key, interface)
def get_region_name(self, service_type=None):
if not service_type:
return self.region
key = _make_key('region_name', service_type)
return self.config.get(key, self.region)
def get_api_version(self, service_type):
key = _make_key('api_version', service_type)
return self.config.get(key, None)
def get_service_type(self, service_type):
key = _make_key('service_type', service_type)
# Cinder did an evil thing where they defined a second service
# type in the catalog. Of course, that's insane, so let's hide this
# atrocity from the as-yet-unsullied eyes of our users.
# Of course, if the user requests a volumev2, that structure should
# still work.
# What's even more amazing is that they did it AGAIN with cinder v3
# And then I learned that mistral copied it.
if service_type == 'volume':
if self.get_api_version(service_type).startswith('2'):
service_type = 'volumev2'
elif self.get_api_version(service_type).startswith('3'):
service_type = 'volumev3'
elif service_type == 'workflow':
if self.get_api_version(service_type).startswith('2'):
service_type = 'workflowv2'
return self.config.get(key, service_type)
def get_service_name(self, service_type):
key = _make_key('service_name', service_type)
return self.config.get(key, None)
def get_endpoint(self, service_type):
key = _make_key('endpoint_override', service_type)
old_key = _make_key('endpoint', service_type)
return self.config.get(key, self.config.get(old_key, None))
@property
def prefer_ipv6(self):
return not self._force_ipv4
@property
def force_ipv4(self):
return self._force_ipv4
def get_auth(self):
"""Return a keystoneauth plugin from the auth credentials."""
return self._auth
def get_session(self):
"""Return a keystoneauth session based on the auth credentials."""
if self._keystone_session is None:
if not self._auth:
raise exceptions.OpenStackConfigException(
"Problem with auth parameters")
(verify, cert) = self.get_requests_verify_args()
# Turn off urllib3 warnings about insecure certs if we have
# explicitly configured requests to tell it we do not want
# cert verification
if not verify:
self.log.debug(
"Turning off SSL warnings for {cloud}:{region}"
" since verify=False".format(
cloud=self.name, region=self.region))
requestsexceptions.squelch_warnings(insecure_requests=not verify)
self._keystone_session = self._session_constructor(
auth=self._auth,
verify=verify,
cert=cert,
timeout=self.config['api_timeout'])
if hasattr(self._keystone_session, 'additional_user_agent'):
self._keystone_session.additional_user_agent.append(
('os-client-config', os_client_config.__version__))
# Using old keystoneauth with new os-client-config fails if
# we pass in app_name and app_version. Those are not essential,
# nor a reason to bump our minimum, so just test for the session
# having the attribute post creation and set them then.
if hasattr(self._keystone_session, 'app_name'):
self._keystone_session.app_name = self._app_name
if hasattr(self._keystone_session, 'app_version'):
self._keystone_session.app_version = self._app_version
return self._keystone_session
def get_service_catalog(self):
"""Helper method to grab the service catalog."""
return self._auth.get_access(self.get_session()).service_catalog
def get_session_client(self, service_key):
"""Return a prepped requests adapter for a given service.
This is useful for making direct requests calls against a
'mounted' endpoint. That is, if you do:
client = get_session_client('compute')
then you can do:
client.get('/flavors')
and it will work like you think.
"""
return adapter.Adapter(
session=self.get_session(),
service_type=self.get_service_type(service_key),
service_name=self.get_service_name(service_key),
interface=self.get_interface(service_key),
region_name=self.region)
def _get_highest_endpoint(self, service_types, kwargs):
session = self.get_session()
for service_type in service_types:
kwargs['service_type'] = service_type
try:
# Return the highest version we find that matches
# the request
return session.get_endpoint(**kwargs)
except keystoneauth1.exceptions.catalog.EndpointNotFound:
pass
def get_session_endpoint(
self, service_key, min_version=None, max_version=None):
"""Return the endpoint from config or the catalog.
If a configuration lists an explicit endpoint for a service,
return that. Otherwise, fetch the service catalog from the
keystone session and return the appropriate endpoint.
:param service_key: Generic key for service, such as 'compute' or
'network'
"""
override_endpoint = self.get_endpoint(service_key)
if override_endpoint:
return override_endpoint
endpoint = None
kwargs = {
'service_name': self.get_service_name(service_key),
'region_name': self.region
}
kwargs['interface'] = self.get_interface(service_key)
if service_key == 'volume' and not self.get_api_version('volume'):
# If we don't have a configured cinder version, we can't know
# to request a different service_type
min_version = float(min_version or 1)
max_version = float(max_version or 3)
min_major = math.trunc(float(min_version))
max_major = math.trunc(float(max_version))
versions = range(int(max_major) + 1, int(min_major), -1)
service_types = []
for version in versions:
if version == 1:
service_types.append('volume')
else:
service_types.append('volumev{v}'.format(v=version))
else:
service_types = [self.get_service_type(service_key)]
endpoint = self._get_highest_endpoint(service_types, kwargs)
if not endpoint:
self.log.warning(
"Keystone catalog entry not found ("
"service_type=%s,service_name=%s"
"interface=%s,region_name=%s)",
service_key,
kwargs['service_name'],
kwargs['interface'],
kwargs['region_name'])
return endpoint
def get_legacy_client(
self, service_key, client_class=None, interface_key=None,
pass_version_arg=True, version=None, min_version=None,
max_version=None, **kwargs):
"""Return a legacy OpenStack client object for the given config.
Most of the OpenStack python-*client libraries have the same
interface for their client constructors, but there are several
parameters one wants to pass given a :class:`CloudConfig` object.
In the future, OpenStack API consumption should be done through
the OpenStack SDK, but that's not ready yet. This is for getting
Client objects from python-*client only.
:param service_key: Generic key for service, such as 'compute' or
'network'
:param client_class: Class of the client to be instantiated. This
should be the unversioned version if there
is one, such as novaclient.client.Client, or
the versioned one, such as
neutronclient.v2_0.client.Client if there isn't
:param interface_key: (optional) Some clients, such as glanceclient
only accept the parameter 'interface' instead
of 'endpoint_type' - this is a get-out-of-jail
parameter for those until they can be aligned.
os-client-config understands this to be the
case if service_key is image, so this is really
only for use with other unknown broken clients.
:param pass_version_arg: (optional) If a versioned Client constructor
was passed to client_class, set this to
False, which will tell get_client to not
pass a version parameter. os-client-config
already understand that this is the
case for network, so it can be omitted in
that case.
:param version: (optional) Version string to override the configured
version string.
:param min_version: (options) Minimum version acceptable.
:param max_version: (options) Maximum version acceptable.
:param kwargs: (optional) keyword args are passed through to the
Client constructor, so this is in case anything
additional needs to be passed in.
"""
if not client_class:
client_class = _get_client(service_key)
interface = self.get_interface(service_key)
# trigger exception on lack of service
endpoint = self.get_session_endpoint(
service_key, min_version=min_version, max_version=max_version)
endpoint_override = self.get_endpoint(service_key)
if service_key == 'object-store':
constructor_kwargs = dict(
session=self.get_session(),
os_options=dict(
service_type=self.get_service_type(service_key),
object_storage_url=endpoint_override,
region_name=self.region))
else:
constructor_kwargs = dict(
session=self.get_session(),
service_name=self.get_service_name(service_key),
service_type=self.get_service_type(service_key),
endpoint_override=endpoint_override,
region_name=self.region)
if service_key == 'image':
# os-client-config does not depend on glanceclient, but if
# the user passed in glanceclient.client.Client, which they
# would need to do if they were requesting 'image' - then
# they necessarily have glanceclient installed
from glanceclient.common import utils as glance_utils
endpoint, detected_version = glance_utils.strip_version(endpoint)
# If the user has passed in a version, that's explicit, use it
if not version:
version = detected_version
# If the user has passed in or configured an override, use it.
# Otherwise, ALWAYS pass in an endpoint_override becuase
# we've already done version stripping, so we don't want version
# reconstruction to happen twice
if not endpoint_override:
constructor_kwargs['endpoint_override'] = endpoint
constructor_kwargs.update(kwargs)
if pass_version_arg and service_key != 'object-store':
if not version:
version = self.get_api_version(service_key)
if not version and service_key == 'volume':
from cinderclient import client as cinder_client
version = cinder_client.get_volume_api_from_url(endpoint)
# Temporary workaround while we wait for python-openstackclient
# to be able to handle 2.0 which is what neutronclient expects
if service_key == 'network' and version == '2':
version = '2.0'
if service_key == 'identity':
# Workaround for bug#1513839
if 'endpoint' not in constructor_kwargs:
endpoint = self.get_session_endpoint('identity')
constructor_kwargs['endpoint'] = endpoint
if service_key == 'network':
constructor_kwargs['api_version'] = version
elif service_key == 'baremetal':
if version != '1':
# Set Ironic Microversion
constructor_kwargs['os_ironic_api_version'] = version
# Version arg is the major version, not the full microstring
constructor_kwargs['version'] = version[0]
else:
constructor_kwargs['version'] = version
if min_version and min_version > float(version):
raise exceptions.OpenStackConfigVersionException(
"Minimum version {min_version} requested but {version}"
" found".format(min_version=min_version, version=version),
version=version)
if max_version and max_version < float(version):
raise exceptions.OpenStackConfigVersionException(
"Maximum version {max_version} requested but {version}"
" found".format(max_version=max_version, version=version),
version=version)
if service_key == 'database':
# TODO(mordred) Remove when https://review.openstack.org/314032
# has landed and released. We're passing in a Session, but the
# trove Client object has username and password as required
# args
constructor_kwargs['username'] = None
constructor_kwargs['password'] = None
if not interface_key:
if service_key in ('image', 'key-manager'):
interface_key = 'interface'
elif (service_key == 'identity'
and version and version.startswith('3')):
interface_key = 'interface'
else:
interface_key = 'endpoint_type'
if service_key == 'object-store':
constructor_kwargs['os_options'][interface_key] = interface
else:
constructor_kwargs[interface_key] = interface
return client_class(**constructor_kwargs)
def get_cache_expiration_time(self):
if self._openstack_config:
return self._openstack_config.get_cache_expiration_time()
def get_cache_path(self):
if self._openstack_config:
return self._openstack_config.get_cache_path()
def get_cache_class(self):
if self._openstack_config:
return self._openstack_config.get_cache_class()
def get_cache_arguments(self):
if self._openstack_config:
return self._openstack_config.get_cache_arguments()
def get_cache_expiration(self):
if self._openstack_config:
return self._openstack_config.get_cache_expiration()
def get_cache_resource_expiration(self, resource, default=None):
"""Get expiration time for a resource
:param resource: Name of the resource type
:param default: Default value to return if not found (optional,
defaults to None)
:returns: Expiration time for the resource type as float or default
"""
if self._openstack_config:
expiration = self._openstack_config.get_cache_expiration()
if resource not in expiration:
return default
return float(expiration[resource])
def requires_floating_ip(self):
"""Return whether or not this cloud requires floating ips.
:returns: True of False if know, None if discovery is needed.
If requires_floating_ip is not configured but the cloud is
known to not provide floating ips, will return False.
"""
if self.config['floating_ip_source'] == "None":
return False
return self.config.get('requires_floating_ip')
def get_external_networks(self):
"""Get list of network names for external networks."""
return [
net['name'] for net in self.config['networks']
if net['routes_externally']]
def get_external_ipv4_networks(self):
"""Get list of network names for external IPv4 networks."""
return [
net['name'] for net in self.config['networks']
if net['routes_ipv4_externally']]
def get_external_ipv6_networks(self):
"""Get list of network names for external IPv6 networks."""
return [
net['name'] for net in self.config['networks']
if net['routes_ipv6_externally']]
def get_internal_networks(self):
"""Get list of network names for internal networks."""
return [
net['name'] for net in self.config['networks']
if not net['routes_externally']]
def get_internal_ipv4_networks(self):
"""Get list of network names for internal IPv4 networks."""
return [
net['name'] for net in self.config['networks']
if not net['routes_ipv4_externally']]
def get_internal_ipv6_networks(self):
"""Get list of network names for internal IPv6 networks."""
return [
net['name'] for net in self.config['networks']
if not net['routes_ipv6_externally']]
def get_default_network(self):
"""Get network used for default interactions."""
for net in self.config['networks']:
if net['default_interface']:
return net['name']
return None
def get_nat_destination(self):
"""Get network used for NAT destination."""
for net in self.config['networks']:
if net['nat_destination']:
return net['name']
return None

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,16 @@
{
"application-catalog": "muranoclient.client.Client",
"baremetal": "ironicclient.client.Client",
"compute": "novaclient.client.Client",
"container-infra": "magnumclient.client.Client",
"database": "troveclient.client.Client",
"dns": "designateclient.client.Client",
"identity": "keystoneclient.client.Client",
"image": "glanceclient.Client",
"key-manager": "barbicanclient.client.Client",
"metering": "ceilometerclient.client.Client",
"network": "neutronclient.neutron.client.Client",
"object-store": "swiftclient.client.Connection",
"orchestration": "heatclient.client.Client",
"volume": "cinderclient.client.Client"
}

View File

@ -0,0 +1,36 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
import threading
_json_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'constructors.json')
_class_mapping = None
_class_mapping_lock = threading.Lock()
def get_constructor_mapping():
global _class_mapping
if _class_mapping is not None:
return _class_mapping.copy()
with _class_mapping_lock:
if _class_mapping is not None:
return _class_mapping.copy()
tmp_class_mapping = {}
with open(_json_path, 'r') as json_file:
tmp_class_mapping.update(json.load(json_file))
_class_mapping = tmp_class_mapping
return tmp_class_mapping.copy()

View File

@ -0,0 +1,27 @@
{
"application_catalog_api_version": "1",
"auth_type": "password",
"baremetal_api_version": "1",
"container_api_version": "1",
"container_infra_api_version": "1",
"compute_api_version": "2",
"database_api_version": "1.0",
"disable_vendor_agent": {},
"dns_api_version": "2",
"interface": "public",
"floating_ip_source": "neutron",
"identity_api_version": "2.0",
"image_api_use_tasks": false,
"image_api_version": "2",
"image_format": "qcow2",
"key_manager_api_version": "v1",
"message": "",
"metering_api_version": "2",
"network_api_version": "2",
"object_store_api_version": "1",
"orchestration_api_version": "1",
"secgroup_source": "neutron",
"status": "active",
"volume_api_version": "2",
"workflow_api_version": "2"
}

View File

@ -0,0 +1,52 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import os
import threading
_json_path = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'defaults.json')
_defaults = None
_defaults_lock = threading.Lock()
def get_defaults():
global _defaults
if _defaults is not None:
return _defaults.copy()
with _defaults_lock:
if _defaults is not None:
# Did someone else just finish filling it?
return _defaults.copy()
# Python language specific defaults
# These are defaults related to use of python libraries, they are
# not qualities of a cloud.
#
# NOTE(harlowja): update a in-memory dict, before updating
# the global one so that other callers of get_defaults do not
# see the partially filled one.
tmp_defaults = dict(
api_timeout=None,
verify=True,
cacert=None,
cert=None,
key=None,
)
with open(_json_path, 'r') as json_file:
updates = json.load(json_file)
if updates is not None:
tmp_defaults.update(updates)
_defaults = tmp_defaults
return tmp_defaults.copy()

View File

@ -0,0 +1,25 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
class OpenStackConfigException(Exception):
"""Something went wrong with parsing your OpenStack Config."""
class OpenStackConfigVersionException(OpenStackConfigException):
"""A version was requested that is different than what was found."""
def __init__(self, version):
super(OpenStackConfigVersionException, self).__init__()
self.version = version

View File

@ -0,0 +1,121 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "https://git.openstack.org/cgit/openstack/cloud-data/plain/schema.json#",
"type": "object",
"properties": {
"auth_type": {
"name": "Auth Type",
"description": "Name of authentication plugin to be used",
"default": "password",
"type": "string"
},
"disable_vendor_agent": {
"name": "Disable Vendor Agent Properties",
"description": "Image properties required to disable vendor agent",
"type": "object",
"properties": {}
},
"floating_ip_source": {
"name": "Floating IP Source",
"description": "Which service provides Floating IPs",
"enum": [ "neutron", "nova", "None" ],
"default": "neutron"
},
"image_api_use_tasks": {
"name": "Image Task API",
"description": "Does the cloud require the Image Task API",
"default": false,
"type": "boolean"
},
"image_format": {
"name": "Image Format",
"description": "Format for uploaded Images",
"default": "qcow2",
"type": "string"
},
"interface": {
"name": "API Interface",
"description": "Which API Interface should connections hit",
"default": "public",
"enum": [ "public", "internal", "admin" ]
},
"secgroup_source": {
"name": "Security Group Source",
"description": "Which service provides security groups",
"default": "neutron",
"enum": [ "neutron", "nova", "None" ]
},
"baremetal_api_version": {
"name": "Baremetal API Service Type",
"description": "Baremetal API Service Type",
"default": "1",
"type": "string"
},
"compute_api_version": {
"name": "Compute API Version",
"description": "Compute API Version",
"default": "2",
"type": "string"
},
"database_api_version": {
"name": "Database API Version",
"description": "Database API Version",
"default": "1.0",
"type": "string"
},
"dns_api_version": {
"name": "DNS API Version",
"description": "DNS API Version",
"default": "2",
"type": "string"
},
"identity_api_version": {
"name": "Identity API Version",
"description": "Identity API Version",
"default": "2",
"type": "string"
},
"image_api_version": {
"name": "Image API Version",
"description": "Image API Version",
"default": "1",
"type": "string"
},
"network_api_version": {
"name": "Network API Version",
"description": "Network API Version",
"default": "2",
"type": "string"
},
"object_store_api_version": {
"name": "Object Storage API Version",
"description": "Object Storage API Version",
"default": "1",
"type": "string"
},
"volume_api_version": {
"name": "Volume API Version",
"description": "Volume API Version",
"default": "2",
"type": "string"
}
},
"required": [
"auth_type",
"baremetal_api_version",
"compute_api_version",
"database_api_version",
"disable_vendor_agent",
"dns_api_version",
"floating_ip_source",
"identity_api_version",
"image_api_use_tasks",
"image_api_version",
"image_format",
"interface",
"network_api_version",
"object_store_api_version",
"secgroup_source",
"volume_api_version"
]
}

View File

@ -0,0 +1,244 @@
# -*- coding: utf-8 -*-
# Copyright 2010-2011 OpenStack Foundation
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import copy
import os
import tempfile
from os_client_config import cloud_config
import extras
import fixtures
from oslotest import base
import yaml
VENDOR_CONF = {
'public-clouds': {
'_test_cloud_in_our_cloud': {
'auth': {
'auth_url': 'http://example.com/v2',
'username': 'testotheruser',
'project_name': 'testproject',
},
},
}
}
USER_CONF = {
'cache': {
'max_age': '1',
'expiration': {
'server': 5,
'image': '7',
},
},
'client': {
'force_ipv4': True,
},
'clouds': {
'_test-cloud_': {
'profile': '_test_cloud_in_our_cloud',
'auth': {
'auth_url': 'http://example.com/v2',
'username': 'testuser',
'password': 'testpass',
},
'region_name': 'test-region',
},
'_test_cloud_no_vendor': {
'profile': '_test_non_existant_cloud',
'auth': {
'auth_url': 'http://example.com/v2',
'username': 'testuser',
'project_name': 'testproject',
},
'region-name': 'test-region',
},
'_test-cloud-int-project_': {
'auth': {
'username': 'testuser',
'password': 'testpass',
'domain_id': 'awesome-domain',
'project_id': 12345,
'auth_url': 'http://example.com/v2',
},
'region_name': 'test-region',
},
'_test-cloud-domain-id_': {
'auth': {
'username': 'testuser',
'password': 'testpass',
'project_id': 12345,
'auth_url': 'http://example.com/v2',
'domain_id': '6789',
'project_domain_id': '123456789',
},
'region_name': 'test-region',
},
'_test-cloud-networks_': {
'auth': {
'username': 'testuser',
'password': 'testpass',
'project_id': 12345,
'auth_url': 'http://example.com/v2',
'domain_id': '6789',
'project_domain_id': '123456789',
},
'networks': [{
'name': 'a-public',
'routes_externally': True,
}, {
'name': 'another-public',
'routes_externally': True,
'default_interface': True,
}, {
'name': 'a-private',
'routes_externally': False,
}, {
'name': 'another-private',
'routes_externally': False,
'nat_destination': True,
}, {
'name': 'split-default',
'routes_externally': True,
'routes_ipv4_externally': False,
}, {
'name': 'split-no-default',
'routes_ipv6_externally': False,
'routes_ipv4_externally': True,
}],
'region_name': 'test-region',
},
'_test_cloud_regions': {
'auth': {
'username': 'testuser',
'password': 'testpass',
'project-id': 'testproject',
'auth_url': 'http://example.com/v2',
},
'regions': [
{
'name': 'region1',
'values': {
'external_network': 'region1-network',
}
},
{
'name': 'region2',
'values': {
'external_network': 'my-network',
}
}
],
},
'_test_cloud_hyphenated': {
'auth': {
'username': 'testuser',
'password': 'testpass',
'project-id': '12345',
'auth_url': 'http://example.com/v2',
},
'region_name': 'test-region',
},
'_test-cloud_no_region': {
'profile': '_test_cloud_in_our_cloud',
'auth': {
'auth_url': 'http://example.com/v2',
'username': 'testuser',
'password': 'testpass',
},
},
'_test-cloud-domain-scoped_': {
'auth': {
'auth_url': 'http://example.com/v2',
'username': 'testuser',
'password': 'testpass',
'domain-id': '12345',
},
},
},
'ansible': {
'expand-hostvars': False,
'use_hostnames': True,
},
}
SECURE_CONF = {
'clouds': {
'_test_cloud_no_vendor': {
'auth': {
'password': 'testpass',
},
}
}
}
NO_CONF = {
'cache': {'max_age': 1},
}
def _write_yaml(obj):
# Assume NestedTempfile so we don't have to cleanup
with tempfile.NamedTemporaryFile(delete=False) as obj_yaml:
obj_yaml.write(yaml.safe_dump(obj).encode('utf-8'))
return obj_yaml.name
class TestCase(base.BaseTestCase):
"""Test case base class for all unit tests."""
def setUp(self):
super(TestCase, self).setUp()
self.useFixture(fixtures.NestedTempfile())
conf = copy.deepcopy(USER_CONF)
tdir = self.useFixture(fixtures.TempDir())
conf['cache']['path'] = tdir.path
self.cloud_yaml = _write_yaml(conf)
self.secure_yaml = _write_yaml(SECURE_CONF)
self.vendor_yaml = _write_yaml(VENDOR_CONF)
self.no_yaml = _write_yaml(NO_CONF)
self.useFixture(fixtures.MonkeyPatch(
'os_client_config.__version__', '1.2.3'))
# Isolate the test runs from the environment
# Do this as two loops because you can't modify the dict in a loop
# over the dict in 3.4
keys_to_isolate = []
for env in os.environ.keys():
if env.startswith('OS_'):
keys_to_isolate.append(env)
for env in keys_to_isolate:
self.useFixture(fixtures.EnvironmentVariable(env))
def _assert_cloud_details(self, cc):
self.assertIsInstance(cc, cloud_config.CloudConfig)
self.assertTrue(extras.safe_hasattr(cc, 'auth'))
self.assertIsInstance(cc.auth, dict)
self.assertIsNone(cc.cloud)
self.assertIn('username', cc.auth)
self.assertEqual('testuser', cc.auth['username'])
self.assertEqual('testpass', cc.auth['password'])
self.assertFalse(cc.config['image_api_use_tasks'])
self.assertTrue('project_name' in cc.auth or 'project_id' in cc.auth)
if 'project_name' in cc.auth:
self.assertEqual('testproject', cc.auth['project_name'])
elif 'project_id' in cc.auth:
self.assertEqual('testproject', cc.auth['project_id'])
self.assertEqual(cc.get_cache_expiration_time(), 1)
self.assertEqual(cc.get_cache_resource_expiration('server'), 5.0)
self.assertEqual(cc.get_cache_resource_expiration('image'), 7.0)

View File

@ -0,0 +1,578 @@
# 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 copy
from keystoneauth1 import exceptions as ksa_exceptions
from keystoneauth1 import session as ksa_session
import mock
from os_client_config import cloud_config
from os_client_config import defaults
from os_client_config import exceptions
from os_client_config.tests import base
fake_config_dict = {'a': 1, 'os_b': 2, 'c': 3, 'os_c': 4}
fake_services_dict = {
'compute_api_version': '2',
'compute_endpoint_override': 'http://compute.example.com',
'compute_region_name': 'region-bl',
'telemetry_endpoint': 'http://telemetry.example.com',
'interface': 'public',
'image_service_type': 'mage',
'identity_interface': 'admin',
'identity_service_name': 'locks',
'volume_api_version': '1',
'auth': {'password': 'hunter2', 'username': 'AzureDiamond'},
}
class TestCloudConfig(base.TestCase):
def test_arbitrary_attributes(self):
cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict)
self.assertEqual("test1", cc.name)
self.assertEqual("region-al", cc.region)
# Look up straight value
self.assertEqual(1, cc.a)
# Look up prefixed attribute, fail - returns None
self.assertIsNone(cc.os_b)
# Look up straight value, then prefixed value
self.assertEqual(3, cc.c)
self.assertEqual(3, cc.os_c)
# Lookup mystery attribute
self.assertIsNone(cc.x)
# Test default ipv6
self.assertFalse(cc.force_ipv4)
def test_iteration(self):
cc = cloud_config.CloudConfig("test1", "region-al", fake_config_dict)
self.assertTrue('a' in cc)
self.assertFalse('x' in cc)
def test_equality(self):
cc1 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict)
cc2 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict)
self.assertEqual(cc1, cc2)
def test_inequality(self):
cc1 = cloud_config.CloudConfig("test1", "region-al", fake_config_dict)
cc2 = cloud_config.CloudConfig("test2", "region-al", fake_config_dict)
self.assertNotEqual(cc1, cc2)
cc2 = cloud_config.CloudConfig("test1", "region-xx", fake_config_dict)
self.assertNotEqual(cc1, cc2)
cc2 = cloud_config.CloudConfig("test1", "region-al", {})
self.assertNotEqual(cc1, cc2)
def test_verify(self):
config_dict = copy.deepcopy(fake_config_dict)
config_dict['cacert'] = None
config_dict['verify'] = False
cc = cloud_config.CloudConfig("test1", "region-xx", config_dict)
(verify, cert) = cc.get_requests_verify_args()
self.assertFalse(verify)
config_dict['verify'] = True
cc = cloud_config.CloudConfig("test1", "region-xx", config_dict)
(verify, cert) = cc.get_requests_verify_args()
self.assertTrue(verify)
def test_verify_cacert(self):
config_dict = copy.deepcopy(fake_config_dict)
config_dict['cacert'] = "certfile"
config_dict['verify'] = False
cc = cloud_config.CloudConfig("test1", "region-xx", config_dict)
(verify, cert) = cc.get_requests_verify_args()
self.assertFalse(verify)
config_dict['verify'] = True
cc = cloud_config.CloudConfig("test1", "region-xx", config_dict)
(verify, cert) = cc.get_requests_verify_args()
self.assertEqual("certfile", verify)
def test_cert_with_key(self):
config_dict = copy.deepcopy(fake_config_dict)
config_dict['cacert'] = None
config_dict['verify'] = False
config_dict['cert'] = 'cert'
config_dict['key'] = 'key'
cc = cloud_config.CloudConfig("test1", "region-xx", config_dict)
(verify, cert) = cc.get_requests_verify_args()
self.assertEqual(("cert", "key"), cert)
def test_ipv6(self):
cc = cloud_config.CloudConfig(
"test1", "region-al", fake_config_dict, force_ipv4=True)
self.assertTrue(cc.force_ipv4)
def test_getters(self):
cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict)
self.assertEqual(['compute', 'identity', 'image', 'volume'],
sorted(cc.get_services()))
self.assertEqual({'password': 'hunter2', 'username': 'AzureDiamond'},
cc.get_auth_args())
self.assertEqual('public', cc.get_interface())
self.assertEqual('public', cc.get_interface('compute'))
self.assertEqual('admin', cc.get_interface('identity'))
self.assertEqual('region-al', cc.get_region_name())
self.assertEqual('region-al', cc.get_region_name('image'))
self.assertEqual('region-bl', cc.get_region_name('compute'))
self.assertIsNone(cc.get_api_version('image'))
self.assertEqual('2', cc.get_api_version('compute'))
self.assertEqual('mage', cc.get_service_type('image'))
self.assertEqual('compute', cc.get_service_type('compute'))
self.assertEqual('1', cc.get_api_version('volume'))
self.assertEqual('volume', cc.get_service_type('volume'))
self.assertEqual('http://compute.example.com',
cc.get_endpoint('compute'))
self.assertIsNone(cc.get_endpoint('image'))
self.assertIsNone(cc.get_service_name('compute'))
self.assertEqual('locks', cc.get_service_name('identity'))
def test_volume_override(self):
cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict)
cc.config['volume_api_version'] = '2'
self.assertEqual('volumev2', cc.get_service_type('volume'))
def test_volume_override_v3(self):
cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict)
cc.config['volume_api_version'] = '3'
self.assertEqual('volumev3', cc.get_service_type('volume'))
def test_workflow_override_v2(self):
cc = cloud_config.CloudConfig("test1", "region-al", fake_services_dict)
cc.config['workflow_api_version'] = '2'
self.assertEqual('workflowv2', cc.get_service_type('workflow'))
def test_get_session_no_auth(self):
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig("test1", "region-al", config_dict)
self.assertRaises(
exceptions.OpenStackConfigException,
cc.get_session)
@mock.patch.object(ksa_session, 'Session')
def test_get_session(self, mock_session):
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
fake_session = mock.Mock()
fake_session.additional_user_agent = []
mock_session.return_value = fake_session
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_session()
mock_session.assert_called_with(
auth=mock.ANY,
verify=True, cert=None, timeout=None)
self.assertEqual(
fake_session.additional_user_agent,
[('os-client-config', '1.2.3')])
@mock.patch.object(ksa_session, 'Session')
def test_get_session_with_app_name(self, mock_session):
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
fake_session = mock.Mock()
fake_session.additional_user_agent = []
fake_session.app_name = None
fake_session.app_version = None
mock_session.return_value = fake_session
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock(),
app_name="test_app", app_version="test_version")
cc.get_session()
mock_session.assert_called_with(
auth=mock.ANY,
verify=True, cert=None, timeout=None)
self.assertEqual(fake_session.app_name, "test_app")
self.assertEqual(fake_session.app_version, "test_version")
self.assertEqual(
fake_session.additional_user_agent,
[('os-client-config', '1.2.3')])
@mock.patch.object(ksa_session, 'Session')
def test_get_session_with_timeout(self, mock_session):
fake_session = mock.Mock()
fake_session.additional_user_agent = []
mock_session.return_value = fake_session
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
config_dict['api_timeout'] = 9
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_session()
mock_session.assert_called_with(
auth=mock.ANY,
verify=True, cert=None, timeout=9)
self.assertEqual(
fake_session.additional_user_agent,
[('os-client-config', '1.2.3')])
@mock.patch.object(ksa_session, 'Session')
def test_override_session_endpoint_override(self, mock_session):
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
self.assertEqual(
cc.get_session_endpoint('compute'),
fake_services_dict['compute_endpoint_override'])
@mock.patch.object(ksa_session, 'Session')
def test_override_session_endpoint(self, mock_session):
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
self.assertEqual(
cc.get_session_endpoint('telemetry'),
fake_services_dict['telemetry_endpoint'])
@mock.patch.object(cloud_config.CloudConfig, 'get_session')
def test_session_endpoint(self, mock_get_session):
mock_session = mock.Mock()
mock_get_session.return_value = mock_session
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_session_endpoint('orchestration')
mock_session.get_endpoint.assert_called_with(
interface='public',
service_name=None,
region_name='region-al',
service_type='orchestration')
@mock.patch.object(cloud_config.CloudConfig, 'get_session')
def test_session_endpoint_not_found(self, mock_get_session):
exc_to_raise = ksa_exceptions.catalog.EndpointNotFound
mock_get_session.return_value.get_endpoint.side_effect = exc_to_raise
cc = cloud_config.CloudConfig(
"test1", "region-al", {}, auth_plugin=mock.Mock())
self.assertIsNone(cc.get_session_endpoint('notfound'))
@mock.patch.object(cloud_config.CloudConfig, 'get_api_version')
@mock.patch.object(cloud_config.CloudConfig, 'get_auth_args')
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_object_store_password(
self,
mock_get_session_endpoint,
mock_get_auth_args,
mock_get_api_version):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://swift.example.com'
mock_get_api_version.return_value = '3'
mock_get_auth_args.return_value = dict(
username='testuser',
password='testpassword',
project_name='testproject',
auth_url='http://example.com',
)
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('object-store', mock_client)
mock_client.assert_called_with(
session=mock.ANY,
os_options={
'region_name': 'region-al',
'service_type': 'object-store',
'object_storage_url': None,
'endpoint_type': 'public',
})
@mock.patch.object(cloud_config.CloudConfig, 'get_auth_args')
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_object_store_password_v2(
self, mock_get_session_endpoint, mock_get_auth_args):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://swift.example.com'
mock_get_auth_args.return_value = dict(
username='testuser',
password='testpassword',
project_name='testproject',
auth_url='http://example.com',
)
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('object-store', mock_client)
mock_client.assert_called_with(
session=mock.ANY,
os_options={
'region_name': 'region-al',
'service_type': 'object-store',
'object_storage_url': None,
'endpoint_type': 'public',
})
@mock.patch.object(cloud_config.CloudConfig, 'get_auth_args')
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_object_store(
self, mock_get_session_endpoint, mock_get_auth_args):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
mock_get_auth_args.return_value = {}
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('object-store', mock_client)
mock_client.assert_called_with(
session=mock.ANY,
os_options={
'region_name': 'region-al',
'service_type': 'object-store',
'object_storage_url': None,
'endpoint_type': 'public',
})
@mock.patch.object(cloud_config.CloudConfig, 'get_auth_args')
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_object_store_timeout(
self, mock_get_session_endpoint, mock_get_auth_args):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
mock_get_auth_args.return_value = {}
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
config_dict['api_timeout'] = 9
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('object-store', mock_client)
mock_client.assert_called_with(
session=mock.ANY,
os_options={
'region_name': 'region-al',
'service_type': 'object-store',
'object_storage_url': None,
'endpoint_type': 'public',
})
@mock.patch.object(cloud_config.CloudConfig, 'get_auth_args')
def test_legacy_client_object_store_endpoint(
self, mock_get_auth_args):
mock_client = mock.Mock()
mock_get_auth_args.return_value = {}
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
config_dict['object_store_endpoint'] = 'http://example.com/swift'
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('object-store', mock_client)
mock_client.assert_called_with(
session=mock.ANY,
os_options={
'region_name': 'region-al',
'service_type': 'object-store',
'object_storage_url': 'http://example.com/swift',
'endpoint_type': 'public',
})
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_image(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('image', mock_client)
mock_client.assert_called_with(
version=2.0,
service_name=None,
endpoint_override='http://example.com',
region_name='region-al',
interface='public',
session=mock.ANY,
# Not a typo - the config dict above overrides this
service_type='mage'
)
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_image_override(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
config_dict['image_endpoint_override'] = 'http://example.com/override'
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('image', mock_client)
mock_client.assert_called_with(
version=2.0,
service_name=None,
endpoint_override='http://example.com/override',
region_name='region-al',
interface='public',
session=mock.ANY,
# Not a typo - the config dict above overrides this
service_type='mage'
)
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_image_versioned(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
# v2 endpoint was passed, 1 requested in config, endpoint wins
config_dict['image_api_version'] = '1'
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('image', mock_client)
mock_client.assert_called_with(
version=2.0,
service_name=None,
endpoint_override='http://example.com',
region_name='region-al',
interface='public',
session=mock.ANY,
# Not a typo - the config dict above overrides this
service_type='mage'
)
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_image_unversioned(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
# Versionless endpoint, config wins
config_dict['image_api_version'] = '1'
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('image', mock_client)
mock_client.assert_called_with(
version='1',
service_name=None,
endpoint_override='http://example.com',
region_name='region-al',
interface='public',
session=mock.ANY,
# Not a typo - the config dict above overrides this
service_type='mage'
)
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_image_argument(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v3'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
# Versionless endpoint, config wins
config_dict['image_api_version'] = '6'
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('image', mock_client, version='beef')
mock_client.assert_called_with(
version='beef',
service_name=None,
endpoint_override='http://example.com',
region_name='region-al',
interface='public',
session=mock.ANY,
# Not a typo - the config dict above overrides this
service_type='mage'
)
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_network(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('network', mock_client)
mock_client.assert_called_with(
api_version='2.0',
endpoint_type='public',
endpoint_override=None,
region_name='region-al',
service_type='network',
session=mock.ANY,
service_name=None)
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_compute(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('compute', mock_client)
mock_client.assert_called_with(
version='2',
endpoint_type='public',
endpoint_override='http://compute.example.com',
region_name='region-al',
service_type='compute',
session=mock.ANY,
service_name=None)
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_identity(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com/v2'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('identity', mock_client)
mock_client.assert_called_with(
version='2.0',
endpoint='http://example.com/v2',
endpoint_type='admin',
endpoint_override=None,
region_name='region-al',
service_type='identity',
session=mock.ANY,
service_name='locks')
@mock.patch.object(cloud_config.CloudConfig, 'get_session_endpoint')
def test_legacy_client_identity_v3(self, mock_get_session_endpoint):
mock_client = mock.Mock()
mock_get_session_endpoint.return_value = 'http://example.com'
config_dict = defaults.get_defaults()
config_dict.update(fake_services_dict)
config_dict['identity_api_version'] = '3'
cc = cloud_config.CloudConfig(
"test1", "region-al", config_dict, auth_plugin=mock.Mock())
cc.get_legacy_client('identity', mock_client)
mock_client.assert_called_with(
version='3',
endpoint='http://example.com',
interface='admin',
endpoint_override=None,
region_name='region-al',
service_type='identity',
session=mock.ANY,
service_name='locks')

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,185 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from os_client_config import cloud_config
from os_client_config import config
from os_client_config import exceptions
from os_client_config.tests import base
import fixtures
class TestEnviron(base.TestCase):
def setUp(self):
super(TestEnviron, self).setUp()
self.useFixture(
fixtures.EnvironmentVariable('OS_AUTH_URL', 'https://example.com'))
self.useFixture(
fixtures.EnvironmentVariable('OS_USERNAME', 'testuser'))
self.useFixture(
fixtures.EnvironmentVariable('OS_PASSWORD', 'testpass'))
self.useFixture(
fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'testproject'))
self.useFixture(
fixtures.EnvironmentVariable('NOVA_PROJECT_ID', 'testnova'))
def test_get_one_cloud(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
self.assertIsInstance(c.get_one_cloud(), cloud_config.CloudConfig)
def test_no_fallthrough(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
self.assertRaises(
exceptions.OpenStackConfigException, c.get_one_cloud, 'openstack')
def test_envvar_name_override(self):
self.useFixture(
fixtures.EnvironmentVariable('OS_CLOUD_NAME', 'override'))
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
cc = c.get_one_cloud('override')
self._assert_cloud_details(cc)
def test_envvar_prefer_ipv6_override(self):
self.useFixture(
fixtures.EnvironmentVariable('OS_PREFER_IPV6', 'false'))
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml],
secure_files=[self.secure_yaml])
cc = c.get_one_cloud('_test-cloud_')
self.assertFalse(cc.prefer_ipv6)
def test_environ_exists(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml],
secure_files=[self.secure_yaml])
cc = c.get_one_cloud('envvars')
self._assert_cloud_details(cc)
self.assertNotIn('auth_url', cc.config)
self.assertIn('auth_url', cc.config['auth'])
self.assertNotIn('project_id', cc.config['auth'])
self.assertNotIn('auth_url', cc.config)
cc = c.get_one_cloud('_test-cloud_')
self._assert_cloud_details(cc)
cc = c.get_one_cloud('_test_cloud_no_vendor')
self._assert_cloud_details(cc)
def test_environ_prefix(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml],
envvar_prefix='NOVA_',
secure_files=[self.secure_yaml])
cc = c.get_one_cloud('envvars')
self._assert_cloud_details(cc)
self.assertNotIn('auth_url', cc.config)
self.assertIn('auth_url', cc.config['auth'])
self.assertIn('project_id', cc.config['auth'])
self.assertNotIn('auth_url', cc.config)
cc = c.get_one_cloud('_test-cloud_')
self._assert_cloud_details(cc)
cc = c.get_one_cloud('_test_cloud_no_vendor')
self._assert_cloud_details(cc)
def test_get_one_cloud_with_config_files(self):
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml],
secure_files=[self.secure_yaml])
self.assertIsInstance(c.cloud_config, dict)
self.assertIn('cache', c.cloud_config)
self.assertIsInstance(c.cloud_config['cache'], dict)
self.assertIn('max_age', c.cloud_config['cache'])
self.assertIn('path', c.cloud_config['cache'])
cc = c.get_one_cloud('_test-cloud_')
self._assert_cloud_details(cc)
cc = c.get_one_cloud('_test_cloud_no_vendor')
self._assert_cloud_details(cc)
def test_config_file_override(self):
self.useFixture(
fixtures.EnvironmentVariable(
'OS_CLIENT_CONFIG_FILE', self.cloud_yaml))
c = config.OpenStackConfig(config_files=[],
vendor_files=[self.vendor_yaml])
cc = c.get_one_cloud('_test-cloud_')
self._assert_cloud_details(cc)
class TestEnvvars(base.TestCase):
def test_no_envvars(self):
self.useFixture(
fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova'))
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
self.assertRaises(
exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars')
def test_test_envvars(self):
self.useFixture(
fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova'))
self.useFixture(
fixtures.EnvironmentVariable('OS_STDERR_CAPTURE', 'True'))
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
self.assertRaises(
exceptions.OpenStackConfigException, c.get_one_cloud, 'envvars')
def test_incomplete_envvars(self):
self.useFixture(
fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova'))
self.useFixture(
fixtures.EnvironmentVariable('OS_USERNAME', 'user'))
config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
# This is broken due to an issue that's fixed in a subsequent patch
# commenting it out in this patch to keep the patch size reasonable
# self.assertRaises(
# keystoneauth1.exceptions.auth_plugins.MissingRequiredOptions,
# c.get_one_cloud, 'envvars')
def test_have_envvars(self):
self.useFixture(
fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova'))
self.useFixture(
fixtures.EnvironmentVariable('OS_AUTH_URL', 'http://example.com'))
self.useFixture(
fixtures.EnvironmentVariable('OS_USERNAME', 'user'))
self.useFixture(
fixtures.EnvironmentVariable('OS_PASSWORD', 'password'))
self.useFixture(
fixtures.EnvironmentVariable('OS_PROJECT_NAME', 'project'))
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml])
cc = c.get_one_cloud('envvars')
self.assertEqual(cc.config['auth']['username'], 'user')
def test_old_envvars(self):
self.useFixture(
fixtures.EnvironmentVariable('NOVA_USERNAME', 'nova'))
self.useFixture(
fixtures.EnvironmentVariable(
'NOVA_AUTH_URL', 'http://example.com'))
self.useFixture(
fixtures.EnvironmentVariable('NOVA_PASSWORD', 'password'))
self.useFixture(
fixtures.EnvironmentVariable('NOVA_PROJECT_NAME', 'project'))
c = config.OpenStackConfig(config_files=[self.cloud_yaml],
vendor_files=[self.vendor_yaml],
envvar_prefix='NOVA_')
cc = c.get_one_cloud('envvars')
self.assertEqual(cc.config['auth']['username'], 'nova')

View File

@ -0,0 +1,35 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import argparse
import os_client_config
from os_client_config.tests import base
class TestInit(base.TestCase):
def test_get_config_without_arg_parser(self):
cloud_config = os_client_config.get_config(
options=None, validate=False)
self.assertIsInstance(
cloud_config,
os_client_config.cloud_config.CloudConfig
)
def test_get_config_with_arg_parser(self):
cloud_config = os_client_config.get_config(
options=argparse.ArgumentParser(),
validate=False)
self.assertIsInstance(
cloud_config,
os_client_config.cloud_config.CloudConfig
)

View File

@ -0,0 +1,62 @@
# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import glob
import json
import os
import jsonschema
from testtools import content
from os_client_config import defaults
from os_client_config.tests import base
class TestConfig(base.TestCase):
def json_diagnostics(self, exc_info):
self.addDetail('filename', content.text_content(self.filename))
for error in sorted(self.validator.iter_errors(self.json_data)):
self.addDetail('jsonschema', content.text_content(str(error)))
def test_defaults_valid_json(self):
_schema_path = os.path.join(
os.path.dirname(os.path.realpath(defaults.__file__)),
'schema.json')
schema = json.load(open(_schema_path, 'r'))
self.validator = jsonschema.Draft4Validator(schema)
self.addOnException(self.json_diagnostics)
self.filename = os.path.join(
os.path.dirname(os.path.realpath(defaults.__file__)),
'defaults.json')
self.json_data = json.load(open(self.filename, 'r'))
self.assertTrue(self.validator.is_valid(self.json_data))
def test_vendors_valid_json(self):
_schema_path = os.path.join(
os.path.dirname(os.path.realpath(defaults.__file__)),
'vendor-schema.json')
schema = json.load(open(_schema_path, 'r'))
self.validator = jsonschema.Draft4Validator(schema)
self.addOnException(self.json_diagnostics)
_vendors_path = os.path.join(
os.path.dirname(os.path.realpath(defaults.__file__)),
'vendors')
for self.filename in glob.glob(os.path.join(_vendors_path, '*.json')):
self.json_data = json.load(open(self.filename, 'r'))
self.assertTrue(self.validator.is_valid(self.json_data))

View File

@ -0,0 +1,223 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"id": "https://git.openstack.org/cgit/openstack/cloud-data/plain/vendor-schema.json#",
"type": "object",
"properties": {
"name": {
"type": "string"
},
"profile": {
"type": "object",
"properties": {
"auth": {
"type": "object",
"properties": {
"auth_url": {
"name": "Auth URL",
"description": "URL of the primary Keystone endpoint",
"type": "string"
}
}
},
"auth_type": {
"name": "Auth Type",
"description": "Name of authentication plugin to be used",
"default": "password",
"type": "string"
},
"disable_vendor_agent": {
"name": "Disable Vendor Agent Properties",
"description": "Image properties required to disable vendor agent",
"type": "object",
"properties": {}
},
"floating_ip_source": {
"name": "Floating IP Source",
"description": "Which service provides Floating IPs",
"enum": [ "neutron", "nova", "None" ],
"default": "neutron"
},
"image_api_use_tasks": {
"name": "Image Task API",
"description": "Does the cloud require the Image Task API",
"default": false,
"type": "boolean"
},
"image_format": {
"name": "Image Format",
"description": "Format for uploaded Images",
"default": "qcow2",
"type": "string"
},
"interface": {
"name": "API Interface",
"description": "Which API Interface should connections hit",
"default": "public",
"enum": [ "public", "internal", "admin" ]
},
"message": {
"name": "Status message",
"description": "Optional message with information related to status",
"type": "string"
},
"requires_floating_ip": {
"name": "Requires Floating IP",
"description": "Whether the cloud requires a floating IP to route traffic off of the cloud",
"default": null,
"type": ["boolean", "null"]
},
"secgroup_source": {
"name": "Security Group Source",
"description": "Which service provides security groups",
"enum": [ "neutron", "nova", "None" ],
"default": "neutron"
},
"status": {
"name": "Vendor status",
"description": "Status of the vendor's cloud",
"enum": [ "active", "deprecated", "shutdown"],
"default": "active"
},
"compute_service_name": {
"name": "Compute API Service Name",
"description": "Compute API Service Name",
"type": "string"
},
"database_service_name": {
"name": "Database API Service Name",
"description": "Database API Service Name",
"type": "string"
},
"dns_service_name": {
"name": "DNS API Service Name",
"description": "DNS API Service Name",
"type": "string"
},
"identity_service_name": {
"name": "Identity API Service Name",
"description": "Identity API Service Name",
"type": "string"
},
"image_service_name": {
"name": "Image API Service Name",
"description": "Image API Service Name",
"type": "string"
},
"volume_service_name": {
"name": "Volume API Service Name",
"description": "Volume API Service Name",
"type": "string"
},
"network_service_name": {
"name": "Network API Service Name",
"description": "Network API Service Name",
"type": "string"
},
"object_service_name": {
"name": "Object Storage API Service Name",
"description": "Object Storage API Service Name",
"type": "string"
},
"baremetal_service_name": {
"name": "Baremetal API Service Name",
"description": "Baremetal API Service Name",
"type": "string"
},
"compute_service_type": {
"name": "Compute API Service Type",
"description": "Compute API Service Type",
"type": "string"
},
"database_service_type": {
"name": "Database API Service Type",
"description": "Database API Service Type",
"type": "string"
},
"dns_service_type": {
"name": "DNS API Service Type",
"description": "DNS API Service Type",
"type": "string"
},
"identity_service_type": {
"name": "Identity API Service Type",
"description": "Identity API Service Type",
"type": "string"
},
"image_service_type": {
"name": "Image API Service Type",
"description": "Image API Service Type",
"type": "string"
},
"volume_service_type": {
"name": "Volume API Service Type",
"description": "Volume API Service Type",
"type": "string"
},
"network_service_type": {
"name": "Network API Service Type",
"description": "Network API Service Type",
"type": "string"
},
"object_service_type": {
"name": "Object Storage API Service Type",
"description": "Object Storage API Service Type",
"type": "string"
},
"baremetal_service_type": {
"name": "Baremetal API Service Type",
"description": "Baremetal API Service Type",
"type": "string"
},
"compute_api_version": {
"name": "Compute API Version",
"description": "Compute API Version",
"type": "string"
},
"database_api_version": {
"name": "Database API Version",
"description": "Database API Version",
"type": "string"
},
"dns_api_version": {
"name": "DNS API Version",
"description": "DNS API Version",
"type": "string"
},
"identity_api_version": {
"name": "Identity API Version",
"description": "Identity API Version",
"type": "string"
},
"image_api_version": {
"name": "Image API Version",
"description": "Image API Version",
"type": "string"
},
"volume_api_version": {
"name": "Volume API Version",
"description": "Volume API Version",
"type": "string"
},
"network_api_version": {
"name": "Network API Version",
"description": "Network API Version",
"type": "string"
},
"object_api_version": {
"name": "Object Storage API Version",
"description": "Object Storage API Version",
"type": "string"
},
"baremetal_api_version": {
"name": "Baremetal API Version",
"description": "Baremetal API Version",
"type": "string"
}
}
}
},
"required": [
"name",
"profile"
]
}

View File

@ -0,0 +1,37 @@
# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import glob
import json
import os
import yaml
_vendors_path = os.path.dirname(os.path.realpath(__file__))
_vendor_defaults = None
def get_profile(profile_name):
global _vendor_defaults
if _vendor_defaults is None:
_vendor_defaults = {}
for vendor in glob.glob(os.path.join(_vendors_path, '*.yaml')):
with open(vendor, 'r') as f:
vendor_data = yaml.safe_load(f)
_vendor_defaults[vendor_data['name']] = vendor_data['profile']
for vendor in glob.glob(os.path.join(_vendors_path, '*.json')):
with open(vendor, 'r') as f:
vendor_data = json.load(f)
_vendor_defaults[vendor_data['name']] = vendor_data['profile']
return _vendor_defaults.get(profile_name)

View File

@ -0,0 +1,11 @@
{
"name": "auro",
"profile": {
"auth": {
"auth_url": "https://api.van1.auro.io:5000/v2.0"
},
"identity_api_version": "2",
"region_name": "van1",
"requires_floating_ip": true
}
}

View File

@ -0,0 +1,7 @@
{
"name": "bluebox",
"profile": {
"volume_api_version": "1",
"region_name": "RegionOne"
}
}

View File

@ -0,0 +1,15 @@
{
"name": "catalyst",
"profile": {
"auth": {
"auth_url": "https://api.cloud.catalyst.net.nz:5000/v2.0"
},
"regions": [
"nz-por-1",
"nz_wlg_2"
],
"image_api_version": "1",
"volume_api_version": "1",
"image_format": "raw"
}
}

View File

@ -0,0 +1,19 @@
{
"name": "citycloud",
"profile": {
"auth": {
"auth_url": "https://identity1.citycloud.com:5000/v3/"
},
"regions": [
"Buf1",
"La1",
"Fra1",
"Lon1",
"Sto2",
"Kna1"
],
"requires_floating_ip": true,
"volume_api_version": "1",
"identity_api_version": "3"
}
}

View File

@ -0,0 +1,14 @@
{
"name": "conoha",
"profile": {
"auth": {
"auth_url": "https://identity.{region_name}.conoha.io"
},
"regions": [
"sin1",
"sjc1",
"tyo1"
],
"identity_api_version": "2"
}
}

View File

@ -0,0 +1,11 @@
{
"name": "datacentred",
"profile": {
"auth": {
"auth_url": "https://compute.datacentred.io:5000"
},
"region-name": "sal01",
"identity_api_version": "3",
"image_api_version": "2"
}
}

View File

@ -0,0 +1,11 @@
{
"name": "dreamcompute",
"profile": {
"auth": {
"auth_url": "https://iad2.dream.io:5000"
},
"identity_api_version": "3",
"region_name": "RegionOne",
"image_format": "raw"
}
}

View File

@ -0,0 +1,13 @@
{
"name": "dreamhost",
"profile": {
"status": "deprecated",
"message": "The dreamhost profile is deprecated. Please use the dreamcompute profile instead",
"auth": {
"auth_url": "https://keystone.dream.io"
},
"identity_api_version": "3",
"region_name": "RegionOne",
"image_format": "raw"
}
}

View File

@ -0,0 +1,10 @@
{
"name": "elastx",
"profile": {
"auth": {
"auth_url": "https://ops.elastx.net:5000"
},
"identity_api_version": "3",
"region_name": "regionOne"
}
}

View File

@ -0,0 +1,16 @@
{
"name": "entercloudsuite",
"profile": {
"auth": {
"auth_url": "https://api.entercloudsuite.com/"
},
"identity_api_version": "3",
"image_api_version": "1",
"volume_api_version": "1",
"regions": [
"it-mil1",
"nl-ams1",
"de-fra1"
]
}
}

View File

@ -0,0 +1,15 @@
{
"name": "fuga",
"profile": {
"auth": {
"auth_url": "https://identity.api.fuga.io:5000",
"user_domain_name": "Default",
"project_domain_name": "Default"
},
"regions": [
"cystack"
],
"identity_api_version": "3",
"volume_api_version": "3"
}
}

View File

@ -0,0 +1,13 @@
{
"name": "ibmcloud",
"profile": {
"auth": {
"auth_url": "https://identity.open.softlayer.com"
},
"volume_api_version": "2",
"identity_api_version": "3",
"regions": [
"london"
]
}
}

View File

@ -0,0 +1,17 @@
{
"name": "internap",
"profile": {
"auth": {
"auth_url": "https://identity.api.cloud.iweb.com"
},
"regions": [
"ams01",
"da01",
"nyj01",
"sin01",
"sjc01"
],
"identity_api_version": "3",
"floating_ip_source": "None"
}
}

View File

@ -0,0 +1,13 @@
{
"name": "otc",
"profile": {
"auth": {
"auth_url": "https://iam.%(region_name)s.otc.t-systems.com/v3"
},
"regions": [
"eu-de"
],
"identity_api_version": "3",
"image_format": "vhd"
}
}

View File

@ -0,0 +1,15 @@
{
"name": "ovh",
"profile": {
"auth": {
"auth_url": "https://auth.cloud.ovh.net/"
},
"regions": [
"BHS1",
"GRA1",
"SBG1"
],
"identity_api_version": "3",
"floating_ip_source": "None"
}
}

View File

@ -0,0 +1,29 @@
{
"name": "rackspace",
"profile": {
"auth": {
"auth_url": "https://identity.api.rackspacecloud.com/v2.0/"
},
"regions": [
"DFW",
"HKG",
"IAD",
"ORD",
"SYD",
"LON"
],
"database_service_type": "rax:database",
"compute_service_name": "cloudServersOpenStack",
"image_api_use_tasks": true,
"image_format": "vhd",
"floating_ip_source": "None",
"secgroup_source": "None",
"requires_floating_ip": false,
"volume_api_version": "1",
"disable_vendor_agent": {
"vm_mode": "hvm",
"xenapi_use_agent": false
},
"has_network": false
}
}

View File

@ -0,0 +1,15 @@
{
"name": "switchengines",
"profile": {
"auth": {
"auth_url": "https://keystone.cloud.switch.ch:5000/v2.0"
},
"regions": [
"LS",
"ZH"
],
"volume_api_version": "1",
"image_api_use_tasks": true,
"image_format": "raw"
}
}

View File

@ -0,0 +1,11 @@
{
"name": "ultimum",
"profile": {
"auth": {
"auth_url": "https://console.ultimum-cloud.com:5000/"
},
"identity_api_version": "3",
"volume_api_version": "1",
"region-name": "RegionOne"
}
}

View File

@ -0,0 +1,16 @@
{
"name": "unitedstack",
"profile": {
"auth": {
"auth_url": "https://identity.api.ustack.com/v3"
},
"regions": [
"bj1",
"gd1"
],
"volume_api_version": "1",
"identity_api_version": "3",
"image_format": "raw",
"floating_ip_source": "None"
}
}

View File

@ -0,0 +1,15 @@
{
"name": "vexxhost",
"profile": {
"auth": {
"auth_url": "https://auth.vexxhost.net"
},
"regions": [
"ca-ymq-1"
],
"dns_api_version": "1",
"identity_api_version": "3",
"floating_ip_source": "None",
"requires_floating_ip": false
}
}

View File

@ -0,0 +1,13 @@
{
"name": "zetta",
"profile": {
"auth": {
"auth_url": "https://identity.api.zetta.io/v3"
},
"regions": [
"no-osl1"
],
"identity_api_version": "3",
"dns_api_version": "2"
}
}

View File

@ -0,0 +1,22 @@
---
prelude: >
Swiftclient instantiation now provides authentication
information so that long lived swiftclient objects can
reauthenticate if necessary. This should be a temporary
situation until swiftclient supports keystoneauth
sessions at which point os-client-config will instantiate
swiftclient with a keystoneauth session.
features:
- Swiftclient instantiation now provides authentication
information so that long lived swiftclient objects can
reauthenticate if necessary.
- Add support for explicit v2password auth type.
- Add SSL support to VEXXHOST vendor profile.
- Add zetta.io cloud vendor profile.
fixes:
- Fix bug where project_domain_{name,id} was set even
if project_{name,id} was not set.
other:
- HPCloud vendor profile removed due to cloud shutdown.
- RunAbove vendor profile removed due to migration to
OVH.

View File

@ -0,0 +1,6 @@
---
features:
- Add a field to vendor cloud profiles to indicate
active, deprecated and shutdown status. A message to
the user is triggered when attempting to use cloud
with either deprecated or shutdown status.

View File

@ -0,0 +1,7 @@
---
issues:
- Fixed a regression when using latest os-client-config with
the keystoneauth from stable/newton. Although this isn't a
super common combination, the added feature that broke the
interaction is really not worthy of the incompatibility, so
a workaround was added.

View File

@ -0,0 +1,4 @@
---
features:
- Add support for passing Ironic microversion to the ironicclient
constructor in get_legacy_client.

View File

@ -0,0 +1,7 @@
---
features:
- Added a flag, 'load_yaml_config' that defaults to True.
If set to false, no clouds.yaml files will be loaded. This
is beneficial if os-client-config wants to be used inside of
a service where end-user clouds.yaml files would make things
more confusing.

View File

@ -0,0 +1,6 @@
---
fixes:
- Refactor ``OpenStackConfig._fix_backward_madness()`` into
``OpenStackConfig.magic_fixes()`` that allows subclasses
to inject more fixup magic into the flow during
``get_one_cloud()`` processing.

View File

@ -0,0 +1,4 @@
---
deprecations:
- Renamed session_client to make_rest_client. session_client
will continue to be supported for backwards compatability.

View File

@ -0,0 +1,15 @@
---
features:
- Add min_version and max_version to get_legacy_client
and to get_session_endpoint. At the moment this is only
really fully plumbed through for cinder, which has extra
special fun around volume, volumev2 and volumev3. Min and max
versions to both methods will look through the options available
in the service catalog and try to return the latest one available
from the span of requested versions. This means a user can say
volume_api_version=None, min_version=2, max_version=3 will get
an endpoint from get_session_endpoint or a Client from cinderclient
that will be either v2 or v3 but not v1. In the future, min and max
version for get_session_endpoint should be able to sort out
appropriate endpoints via version discovery, but that does not
currently exist.

View File

@ -0,0 +1,10 @@
---
features:
- Support added for configuring metadata about networks
for a cloud in a list of dicts, rather than in the
external_network and internal_network entries. The dicts
support a name, a routes_externally field, a nat_destination
field and a default_interface field.
deprecations:
- external_network and internal_network are deprecated and
should be replaced with the list of network dicts.

View File

@ -0,0 +1,7 @@
---
fixes:
- Reverse the order of option selction in
``OpenStackConfig._validate_auth()`` to prefer auth options
passed in (from argparse) over those found in clouds.yaml.
This allows the application to override config profile
auth settings.

View File

@ -0,0 +1,4 @@
---
features:
- Added helper method for constructing OpenStack SDK
Connection objects.

View File

@ -0,0 +1,6 @@
---
features:
- Added kwargs and argparse processing for session_client.
deprecations:
- Renamed simple_client to session_client. simple_client
will remain as an alias for backwards compat.

View File

@ -0,0 +1,4 @@
---
features:
- Added helper method for constructing shade
OpenStackCloud objects.

View File

@ -0,0 +1,3 @@
---
other:
- Started using reno for release notes.

View File

@ -0,0 +1,4 @@
---
other:
- Add citycloud regions for Buffalo, Frankfurt, Karlskrona and Los Angles
- Add new DreamCompute cloud and deprecate DreamHost cloud

View File

@ -0,0 +1,265 @@
# -*- coding: utf-8 -*-
#
# Os-Client-Config Release Notes documentation build configuration file, created by
# sphinx-quickstart on Thu Nov 5 11:50:32 2015.
#
# 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.
# 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.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
import openstackdocstheme
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'reno.sphinxext',
]
# 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-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'os-client-config Release Notes'
copyright = u'2015, os-client-config developers'
# 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.
#
import pbr.version
occ_version = pbr.version.VersionInfo('os-client-config')
# The short X.Y version.
version = occ_version.canonical_version_string()
# The full version, including alpha/beta/rc tags.
release = occ_version.version_string_with_vcs()
# 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 patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = []
# 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 = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = 'openstackdocs'
# 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 = []
html_theme_path = [openstackdocstheme.get_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']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# 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_domain_indices = 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, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = 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 = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'OCCReleaseNotesdoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'OCCReleaseNotes.tex', u'os-client-config Release Notes Documentation',
u'os-client-config developers', '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
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('index', 'occreleasenotes', u'os-client-config Release Notes Documentation',
[u'os-client-config developers'], 1)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'OCCReleaseNotes', u'os-client-config Release Notes Documentation',
u'os-client-config developers', 'OCCReleaseNotes',
'One line description of project.',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False
# -- Options for Internationalization output ------------------------------
locale_dirs = ['locale/']

View File

@ -0,0 +1,21 @@
================================
os-client-config Release Notes
================================
Contents
========
.. toctree::
:maxdepth: 2
unreleased
pike
ocata
newton
mitaka
Indices and tables
==================
* :ref:`genindex`
* :ref:`search`

View File

@ -0,0 +1,6 @@
===================================
Mitaka Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/mitaka

View File

@ -0,0 +1,6 @@
===================================
Newton Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/newton

View File

@ -0,0 +1,6 @@
===================================
Ocata Series Release Notes
===================================
.. release-notes::
:branch: origin/stable/ocata

View File

@ -0,0 +1,6 @@
===================================
Pike Series Release Notes
===================================
.. release-notes::
:branch: stable/pike

View File

@ -0,0 +1,5 @@
============================
Current Series Release Notes
============================
.. release-notes::

View File

@ -0,0 +1,7 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
PyYAML>=3.10 # MIT
appdirs>=1.3.0 # MIT License
keystoneauth1>=3.2.0 # Apache-2.0
requestsexceptions>=1.2.0 # Apache-2.0

View File

@ -0,0 +1,35 @@
[metadata]
name = os-client-config
summary = OpenStack Client Configuation Library
description-file =
README.rst
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = https://docs.openstack.org/os-client-config/latest
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 3
Programming Language :: Python :: 3.5
[files]
packages =
os_client_config
[build_sphinx]
source-dir = doc/source
build-dir = doc/build
all_files = 1
warning-is-error = 1
[upload_sphinx]
upload-dir = doc/build/html
[wheel]
universal = 1

29
os-client-config/setup.py Normal file
View File

@ -0,0 +1,29 @@
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr>=2.0.0'],
pbr=True)

View File

@ -0,0 +1,21 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
coverage!=4.4,>=4.0 # Apache-2.0
docutils>=0.11 # OSI-Approved Open Source, Public Domain
extras>=0.0.3 # MIT
fixtures>=3.0.0 # Apache-2.0/BSD
jsonschema<3.0.0,>=2.6.0 # MIT
mock>=2.0.0 # BSD
python-glanceclient>=2.8.0 # Apache-2.0
python-subunit>=0.0.18 # Apache-2.0/BSD
sphinx>=1.6.2 # BSD
openstackdocstheme>=1.17.0 # Apache-2.0
oslotest>=1.10.0 # Apache-2.0
reno>=2.5.0 # Apache-2.0
testrepository>=0.0.18 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=1.4.0 # MIT

View File

@ -0,0 +1,89 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os_client_config
import pprint
import sys
import urlparse
def print_versions(r):
if 'version' in r:
for version in r['version']:
print_version(version)
if 'values' in r:
for version in r['values']:
print_version(version)
if isinstance(r, list):
for version in r:
print_version(version)
def print_version(version):
if version['status'] in ('CURRENT', 'stable'):
print(
"\tVersion ID: {id} updated {updated}".format(
id=version.get('id'),
updated=version.get('updated')))
verbose = '-v' in sys.argv
ran = []
for cloud in os_client_config.OpenStackConfig().get_all_clouds():
if cloud.name in ran:
continue
ran.append(cloud.name)
# We don't actually need a compute client - but we'll be getting full urls
# anyway. Without this SSL cert info becomes wrong.
c = cloud.get_session_client('compute')
endpoint = cloud.config['auth']['auth_url']
try:
print(endpoint)
r = c.get(endpoint).json()
if verbose:
pprint.pprint(r)
except Exception as e:
print("Error with {cloud}: {e}".format(cloud=cloud.name, e=str(e)))
continue
if 'version' in r:
print_version(r['version'])
url = urlparse.urlparse(endpoint)
parts = url.path.split(':')
if len(parts) == 2:
path, port = parts
else:
path = url.path
port = None
stripped = path.rsplit('/', 2)[0]
if port:
stripped = '{stripped}:{port}'.format(stripped=stripped, port=port)
endpoint = urlparse.urlunsplit(
(url.scheme, url.netloc, stripped, url.params, url.query))
print(" also {endpoint}".format(endpoint=endpoint))
try:
r = c.get(endpoint).json()
if verbose:
pprint.pprint(r)
except Exception:
print("\tUnauthorized")
continue
if 'version' in r:
print_version(r)
elif 'versions' in r:
print_versions(r['versions'])
else:
print("\n\nUNKNOWN\n\n{r}".format(r=r))
else:
print_versions(r['versions'])

View File

@ -0,0 +1,56 @@
# Copyright (c) 2017 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os_client_config
ran = []
for cloud in os_client_config.OpenStackConfig().get_all_clouds():
if cloud.name in ran:
continue
ran.append(cloud.name)
c = cloud.get_session_client('compute')
try:
raw_endpoint = c.get_endpoint()
have_current = False
endpoint = raw_endpoint.rsplit('/', 2)[0]
print(endpoint)
r = c.get(endpoint).json()
except Exception:
print("Error with %s" % cloud.name)
continue
for version in r['versions']:
if version['status'] == 'CURRENT':
have_current = True
print(
"\tVersion ID: {id} updated {updated}".format(
id=version.get('id'),
updated=version.get('updated')))
print(
"\tVersion Max: {max}".format(max=version.get('version')))
print(
"\tVersion Min: {min}".format(min=version.get('min_version')))
if not have_current:
for version in r['versions']:
if version['status'] == 'SUPPORTED':
have_current = True
print(
"\tVersion ID: {id} updated {updated}".format(
id=version.get('id'),
updated=version.get('updated')))
print(
"\tVersion Max: {max}".format(max=version.get('version')))
print(
"\tVersion Min: {min}".format(
min=version.get('min_version')))

View File

@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Client constraint file contains this client version pin that is in conflict
# with installing the client from source. We should remove the version pin in
# the constraints file before applying it for from-source installation.
CONSTRAINTS_FILE=$1
shift 1
set -e
# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get
# published to logs.openstack.org for easy debugging.
localfile="$VIRTUAL_ENV/log/upper-constraints.txt"
if [[ $CONSTRAINTS_FILE != http* ]]; then
CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE
fi
# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep
curl $CONSTRAINTS_FILE --insecure --progress-bar --output $localfile
pip install -c$localfile openstack-requirements
# This is the main purpose of the script: Allow local installation of
# the current repo. It is listed in constraints file and thus any
# install will be constrained and we need to unconstrain it.
edit-constraints $localfile -- $CLIENT_NAME
pip install -c$localfile -U $*
exit $?

45
os-client-config/tox.ini Normal file
View File

@ -0,0 +1,45 @@
[tox]
minversion = 1.6
envlist = py35,py27,pypy,pep8
skipsdist = True
[testenv]
usedevelop = True
passenv = ZUUL_CACHE_DIR
REQUIREMENTS_PIP_LOCATION
install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
setenv =
VIRTUAL_ENV={envdir}
BRANCH_NAME=master
CLIENT_NAME=os-client-config
OS_STDOUT_CAPTURE=1
OS_STDERR_CAPTURE=1
OS_TEST_TIMEOUT=60
deps = -r{toxinidir}/test-requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}'
[testenv:pep8]
commands = flake8
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = python setup.py test --coverage --coverage-package-name=os_client_config --testr-args='{posargs}'
[testenv:docs]
deps =
{[testenv]deps}
readme
commands =
python setup.py build_sphinx
python setup.py check -r -s
[testenv:releasenotes]
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
[flake8]
show-source = True
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,releasenotes/source/conf.py