Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: I0b3d757594242dd8a2be4733f70720c1e20e0934
This commit is contained in:
29
.coveragerc
29
.coveragerc
@@ -1,29 +0,0 @@
|
||||
# .coveragerc to control coverage.py
|
||||
[run]
|
||||
branch = True
|
||||
|
||||
source=troveclient
|
||||
omit=troveclient/tests*,troveclient/compat/tests*
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __repr__
|
||||
if self\.debug
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
|
||||
ignore_errors = False
|
||||
|
||||
[html]
|
||||
directory=cover
|
||||
18
.gitignore
vendored
18
.gitignore
vendored
@@ -1,18 +0,0 @@
|
||||
*.pyc
|
||||
.testrepository
|
||||
.tox/*
|
||||
dist/*
|
||||
build/*
|
||||
html/*
|
||||
*.egg*
|
||||
cover/*
|
||||
.coverage
|
||||
rdserver.txt
|
||||
python-troveclient.iml
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
|
||||
# Files created by releasenotes build
|
||||
releasenotes/build
|
||||
/doc/build/
|
||||
/doc/source/reference/api/
|
||||
@@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/python-troveclient.git
|
||||
@@ -1,8 +0,0 @@
|
||||
[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
|
||||
@@ -1,16 +0,0 @@
|
||||
If you would like to contribute to the development of OpenStack,
|
||||
you must follow the steps documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
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/python-troveclient
|
||||
176
LICENSE
176
LICENSE
@@ -1,176 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
14
README
Normal file
14
README
Normal file
@@ -0,0 +1,14 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
For ongoing work on maintaining OpenStack packages in the Debian
|
||||
distribution, please see the Debian OpenStack packaging team at
|
||||
https://wiki.debian.org/OpenStack/.
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
||||
43
README.rst
43
README.rst
@@ -1,43 +0,0 @@
|
||||
Python bindings to the OpenStack Trove API
|
||||
==========================================
|
||||
|
||||
.. image:: http://governance.openstack.org/badges/python-troveclient.svg
|
||||
:target: http://governance.openstack.org/reference/tags/index.html
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/python-troveclient.svg
|
||||
:target: https://pypi.python.org/pypi/python-troveclient/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/python-troveclient.svg
|
||||
:target: https://pypi.python.org/pypi/python-troveclient/
|
||||
:alt: Downloads
|
||||
|
||||
This is a client for the OpenStack Trove API. There's a Python API (the
|
||||
``troveclient`` module), and a command-line script (``trove``). Each
|
||||
implements 100% of the OpenStack Trove API.
|
||||
|
||||
See the `Trove CLI Guide`_ for information on how to use the ``trove``
|
||||
command-line tool. You may also want to look at the
|
||||
`OpenStack API documentation`_.
|
||||
|
||||
.. _Trove CLI Guide: http://docs.openstack.org/trove/latest/cli
|
||||
.. _OpenStack API documentation: http://docs.openstack.org/api/quick-start/content/
|
||||
|
||||
python-troveclient is licensed under the Apache License like the rest of OpenStack.
|
||||
|
||||
* License: Apache License, Version 2.0
|
||||
* Documentation: http://docs.openstack.org/developer/python-troveclient/
|
||||
* Bugs: https://bugs.launchpad.net/python-troveclient
|
||||
* `PyPi`_- package installation
|
||||
* `Blueprints`_ - feature specifications
|
||||
* `Git Source`_
|
||||
* `Github`_
|
||||
* `Specs`_
|
||||
* `How to Contribute`_
|
||||
|
||||
.. _PyPi: https://pypi.python.org/pypi/python-troveclient
|
||||
.. _Blueprints: https://blueprints.launchpad.net/python-troveclient
|
||||
.. _Git Source: https://git.openstack.org/cgit/openstack/python-troveclient
|
||||
.. _Github: https://github.com/openstack/python-troveclient
|
||||
.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html
|
||||
.. _Specs: http://specs.openstack.org/openstack/trove-specs/
|
||||
@@ -1,3 +0,0 @@
|
||||
.. toctree::
|
||||
|
||||
trove.rst
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,61 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2013 Rackspace Hosting
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# Copyright 2013 Mirantis Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", ".."))
|
||||
|
||||
sys.path.insert(0, ROOT)
|
||||
sys.path.insert(0, BASE_DIR)
|
||||
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.doctest',
|
||||
'sphinx.ext.coverage',
|
||||
'openstackdocstheme',
|
||||
]
|
||||
|
||||
# openstackdocstheme options
|
||||
repository_name = 'openstack/python-troveclient'
|
||||
bug_project = 'python-troveclient'
|
||||
bug_tag = ''
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
templates_path = ['_templates']
|
||||
|
||||
source_suffix = '.rst'
|
||||
|
||||
master_doc = 'index'
|
||||
|
||||
project = u'python-troveclient'
|
||||
copyright = u'2014, OpenStack Foundation'
|
||||
|
||||
exclude_trees = []
|
||||
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
htmlhelp_basename = 'python-troveclientdoc'
|
||||
latex_documents = [
|
||||
('index', 'python-troveclient.tex', u'python-troveclient Documentation',
|
||||
u'OpenStack', 'manual'),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
..
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
not use this file except in compliance with the License. You may obtain
|
||||
a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
============================================
|
||||
Python bindings to the OpenStack Trove API
|
||||
============================================
|
||||
|
||||
This is a client for the OpenStack Trove API. There's a Python API (the
|
||||
``troveclient`` module), and a command-line script (``trove``). Each
|
||||
implements 100% of the OpenStack Trove API.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
user/index
|
||||
cli/index
|
||||
reference/index
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
@@ -1,8 +0,0 @@
|
||||
=============================
|
||||
troveclient Reference Guide
|
||||
=============================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
api/autoindex
|
||||
@@ -1,135 +0,0 @@
|
||||
Using the Client Programmatically
|
||||
=================================
|
||||
|
||||
Authentication
|
||||
--------------
|
||||
|
||||
Authenticating is necessary to use every feature of the client.
|
||||
|
||||
To create the client, create an instance of the Client class.
|
||||
The auth url, username, password, and project name must be specified in the
|
||||
call to the constructor.
|
||||
|
||||
.. testcode::
|
||||
|
||||
from troveclient.v1 import client
|
||||
tc = client.Client(username="testuser",
|
||||
password="PASSWORD",
|
||||
project_id="test_project",
|
||||
region_name="EAST",
|
||||
auth_url="http://api-server:5000/v2.0")
|
||||
|
||||
The default authentication strategy assumes a keystone compliant auth system.
|
||||
|
||||
Once you have an authenticated client object you can make calls with it,
|
||||
for example:
|
||||
|
||||
.. testcode::
|
||||
|
||||
flavors = tc.flavors.list()
|
||||
datastores = tc.datastores.list()
|
||||
|
||||
Instances
|
||||
---------
|
||||
|
||||
The following example creates a 512 MB instance with a 1 GB volume:
|
||||
|
||||
.. testcode::
|
||||
|
||||
from troveclient.v1 import client
|
||||
tc = client.Client(username="testuser",
|
||||
password="PASSWORD",
|
||||
project_id="test_project",
|
||||
region_name="EAST",
|
||||
auth_url="http://api-server:5000/v2.0")
|
||||
|
||||
flavor_id = '1'
|
||||
volume = {'size':1}
|
||||
databases = [{"name": "my_db",
|
||||
"character_set": "latin2", # These two fields
|
||||
"collate": "latin2_general_ci"}] # are optional.
|
||||
datastore = 'mysql'
|
||||
datastore_version = '5.6-104'
|
||||
users = [{"name": "jsmith", "password": "12345",
|
||||
"databases": [{"name": "my_db"}]
|
||||
}]
|
||||
instance = client.instances.create("My Instance", flavor_id, volume,
|
||||
databases, users, datastore=datastore,
|
||||
datastore_version=datastore_version)
|
||||
|
||||
To retrieve the instance, use the "get" method of "instances":
|
||||
|
||||
.. testcode::
|
||||
|
||||
updated_instance = client.instances.get(instance.id)
|
||||
print(updated_instance.name)
|
||||
print(" Status=%s Flavor=%s" %
|
||||
(updated_instance.status, updated_instance.flavor['id']))
|
||||
|
||||
.. testoutput::
|
||||
|
||||
My Instance
|
||||
Status=BUILD Flavor=1
|
||||
|
||||
You can delete an instance by calling "delete" on the instance object itself,
|
||||
or by using the delete method on "instances."
|
||||
|
||||
.. testcode::
|
||||
|
||||
# Wait for the instance to be ready before we delete it.
|
||||
import time
|
||||
from troveclient.exceptions import NotFound
|
||||
|
||||
while instance.status == "BUILD":
|
||||
instance.get()
|
||||
time.sleep(1)
|
||||
print("Ready in an %s state." % instance.status)
|
||||
instance.delete()
|
||||
# Delete and wait for the instance to go away.
|
||||
while True:
|
||||
try:
|
||||
instance = client.instances.get(instance.id)
|
||||
assert instance.status == "SHUTDOWN"
|
||||
except NotFound:
|
||||
break
|
||||
|
||||
.. testoutput::
|
||||
|
||||
Ready in an ACTIVE state.
|
||||
|
||||
|
||||
Listing Items and Pagination
|
||||
--------------------------------
|
||||
|
||||
Lists paginate after twenty items, meaning you'll only get twenty items back
|
||||
even if there are more. To see the next set of items, send a marker. The marker
|
||||
is a key value (in the case of instances, the ID) which is the non-inclusive
|
||||
starting point for all returned items.
|
||||
|
||||
The lists returned by the client always include a "next" property. This
|
||||
can be used as the "marker" argument to get the next section of the list
|
||||
back from the server. If no more items are available, then the next property
|
||||
is None.
|
||||
|
||||
Pagination applies to all listed objects, like instances, datastores, etc.
|
||||
The example below is for instances.
|
||||
|
||||
.. testcode::
|
||||
|
||||
# There are currently 30 instances.
|
||||
|
||||
instances = client.instances.list()
|
||||
print(len(instances))
|
||||
print(instances.next is None)
|
||||
|
||||
instances2 = client.instances.list(marker=instances.next)
|
||||
print(len(instances2))
|
||||
print(instances2.next is None)
|
||||
|
||||
.. testoutput::
|
||||
|
||||
20
|
||||
False
|
||||
10
|
||||
True
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
=========================
|
||||
Trove Client User Guide
|
||||
=========================
|
||||
|
||||
Command-line API
|
||||
----------------
|
||||
|
||||
Installing this package gets you a shell command, ``trove``, that you
|
||||
can use to interact with any OpenStack cloud.
|
||||
|
||||
You'll need to provide your OpenStack username and password. You can do this
|
||||
with the ``--os-username``, ``--os-password`` and ``--os-tenant-name``
|
||||
params, but it's easier to just set them as environment variables::
|
||||
|
||||
export OS_USERNAME=openstack
|
||||
export OS_PASSWORD=yadayada
|
||||
export OS_TENANT_NAME=myproject
|
||||
|
||||
You will also need to define the authentication url with ``--os-auth-url`` and
|
||||
the version of the API with ``--os-database-api-version`` (default is version
|
||||
1.0). Or set them as an environment variables as well::
|
||||
|
||||
export OS_AUTH_URL=http://example.com:5000/v2.0/
|
||||
export OS_AUTH_URL=1.0
|
||||
|
||||
If you are using Keystone, you need to set the OS_AUTH_URL to the keystone
|
||||
endpoint::
|
||||
|
||||
export OS_AUTH_URL=http://example.com:5000/v2.0/
|
||||
|
||||
Since Keystone can return multiple regions in the Service Catalog, you
|
||||
can specify the one you want with ``--os-region-name`` (or
|
||||
``export OS_REGION_NAME``). It defaults to the first in the list returned.
|
||||
|
||||
Argument ``--profile`` is available only when the osprofiler lib is installed.
|
||||
|
||||
You'll find complete documentation on the shell by running
|
||||
``trove help``.
|
||||
|
||||
For more details, refer to :doc:`../cli/index`.
|
||||
|
||||
Python API
|
||||
----------
|
||||
|
||||
There's also a complete Python API.
|
||||
|
||||
Quick-start using keystone::
|
||||
|
||||
# use v2.0 auth with http://example.com:5000/v2.0/
|
||||
>>> from troveclient.v1 import client
|
||||
>>> nt = client.Client(USERNAME, PASSWORD, TENANT_NAME, AUTH_URL)
|
||||
>>> nt.datastores.list()
|
||||
[...]
|
||||
>>> nt.flavors.list()
|
||||
[...]
|
||||
>>> nt.instances.list()
|
||||
[...]
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
api
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- The command ``trove backup-list`` is now available to use in
|
||||
the python-openstackclient CLI as ``openstack database backup
|
||||
list``
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- The command ``trove cluster-list`` is now available to use in
|
||||
the python-openstackclient CLI as ``openstack database cluster
|
||||
list``
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- The command ``trove configuration-list`` is now available to
|
||||
use in the python-openstackclient CLI as ``openstack database
|
||||
configuration list``
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- The command ``trove database-list`` is now available to use in
|
||||
the python-openstackclient CLI as ``openstack database list``
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- The command ``trove datastore-list`` is now available to use in
|
||||
the python-openstackclient CLI as ``openstack datastore list``
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- The command ``trove flavor-list`` is now available to use in
|
||||
the python-openstackclient CLI as ``openstack database flavor
|
||||
list``
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- The command ``trove list`` is now available to use in
|
||||
the python-openstackclient CLI as ``openstack database instance list``
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Add module-instance-count support to list
|
||||
a count of all instances having a specific
|
||||
module applied. Bug 1554900
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Add module-reapply command to facilitate
|
||||
applying a module again to all instances
|
||||
where it was previously applied. Bug 1554903
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Allow use of backup name in trove create
|
||||
when restoring a backup.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- configuration-* cli commands now allow name of configuration group
|
||||
entered instead of just the configuration id. This will allow a user
|
||||
to specify the configuration group name or the id to use for all
|
||||
the cli commands related to configuration groups. Bug 1505529
|
||||
@@ -1,3 +0,0 @@
|
||||
features:
|
||||
- Added cluster-upgrade command to upgrade all instances
|
||||
in a cluster to a new datastore version.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Support was added to manage users and databases for clusters.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Added support for listing volume types for a given datastore version.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- The CLI output from configuration-parameter-list
|
||||
was fixed to properly display the 'Min Size' and
|
||||
'Max Size' values. Bug 1572272
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Fixed CLI output of cluster-create to only print
|
||||
pertinent information so it is consistent with
|
||||
cluster-show. Bug 1563504
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Having the CLI command display non-redundant information
|
||||
for module-list when invoked with admin privileges now
|
||||
works with both keystone V2 and V3. Bug 1622019
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Add disk column in flavor-list.
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Add ephemeral column in flavor-list Bug 1617980.
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Add vCPUs column in flavor-list Bug 1261876.
|
||||
@@ -1,6 +0,0 @@
|
||||
features:
|
||||
- The reset-status command will set the task and status
|
||||
of an instance to ERROR after which it can be deleted.
|
||||
- The force-delete command will allow the deletion of
|
||||
an instance even if the instance is stuck in BUILD
|
||||
state.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Add a new trove upgrade CLI command and a new
|
||||
Instances.upgrade python API method to implement
|
||||
the new Instance Upgrade feature.
|
||||
@@ -1,6 +0,0 @@
|
||||
features:
|
||||
- The --incremental flag for backup-create will
|
||||
add the abiility to create incremental backup based
|
||||
on last full or incremental backup. If no full or
|
||||
incremental backup exists a new full backup will
|
||||
be created.
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- A --locality flag was added to the trove cluster-create
|
||||
command to allow a user to specify whether instances in
|
||||
a cluster should be on the same hypervisor (affinity)
|
||||
or on different hypervisors (anti-affinity).
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- A --locality flag was added to the trove create command
|
||||
to allow a user to specify whether new replicas should
|
||||
be on the same hypervisor (affinity) or on different
|
||||
hypervisors (anti-affinity).
|
||||
@@ -1,7 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Modules can now be applied in a consistent order,
|
||||
based on the new 'priority_apply' and 'apply_order'
|
||||
attributes available to module-create and
|
||||
module-update.
|
||||
Blueprint module-management-ordering
|
||||
@@ -1,11 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Support was added for modules in cluster-grow and.
|
||||
the CLI consolidated to look more like cluster-create.
|
||||
This means that not including --instance on
|
||||
cluster-grow now raises a MissingArgs exception.
|
||||
Not including a required option in the --instance
|
||||
argument also raises MissingArgs now (instead of the
|
||||
previously raised CommandError).
|
||||
Bug 15778917
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Updating a module with all_datastores and
|
||||
all_datastore_versions now works correctly.
|
||||
Bug 1612430
|
||||
@@ -1,4 +0,0 @@
|
||||
features:
|
||||
- Adds --region option to create and cluster-create APIs. For now, these
|
||||
options are excluded from CLI help strings. This is the first step in
|
||||
multiregion support.
|
||||
@@ -1,3 +0,0 @@
|
||||
features:
|
||||
- Added pagination support (limit and marker) to the CLI for
|
||||
configuration-list and configuration-instances.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
features:
|
||||
- Support added for error messages when running the
|
||||
Trove show command.
|
||||
@@ -1,4 +0,0 @@
|
||||
features:
|
||||
- Adds quota-show and quota-update commands to show the limits for all
|
||||
resources and to change the limit for a single resource. These commands
|
||||
require admin privileges.
|
||||
@@ -1,4 +0,0 @@
|
||||
---
|
||||
fixes:
|
||||
- Remove all the rax references in the client. Use the rackspace plugin for
|
||||
auth if you want to use rax auth with troveclient. Bug 1401804
|
||||
@@ -1,3 +0,0 @@
|
||||
features:
|
||||
- Implements trove schedule-* and execution-* commands to support
|
||||
scheduled backups.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Use i18n for shell.py Partial-Bug 1379001.
|
||||
@@ -1,3 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Use i18n for v1/shell.py
|
||||
@@ -1,274 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Trove Client Release Notes documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue Apr 5 19:49:56 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 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'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = []
|
||||
extensions = [
|
||||
'openstackdocstheme',
|
||||
'reno.sphinxext',
|
||||
]
|
||||
|
||||
|
||||
# openstackdocstheme options
|
||||
repository_name = 'openstack/python-troveclient'
|
||||
bug_project = 'python-troveclient'
|
||||
bug_tag = ''
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
# 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'Trove Client Release Notes'
|
||||
copyright = u'2016, Trove 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
|
||||
trove_version = pbr.version.VersionInfo('python-troveclient')
|
||||
# The short X.Y version.
|
||||
version = trove_version.canonical_version_string()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = trove_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 = []
|
||||
|
||||
# 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 = 'TroveClientReleaseNotesdoc'
|
||||
|
||||
|
||||
# -- 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', 'TroveClientReleaseNotes.tex', u'Trove Client Release Notes Documentation',
|
||||
u'Trove 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', 'troveclientreleasenotes', u'Trove Client Release Notes Documentation',
|
||||
[u'Trove 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', 'TroveClientReleaseNotes', u'Trove Client Release Notes Documentation',
|
||||
u'Trove developers', 'TroveClientReleaseNotes', 'OpenStack Database as a Service.',
|
||||
'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/']
|
||||
@@ -1,19 +0,0 @@
|
||||
==========================
|
||||
Trove Client Release Notes
|
||||
==========================
|
||||
|
||||
Contents:
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
unreleased
|
||||
ocata
|
||||
newton
|
||||
mitaka
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
||||
@@ -1,6 +0,0 @@
|
||||
===========================
|
||||
Mitaka Series Release Notes
|
||||
===========================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/mitaka
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Newton Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/newton
|
||||
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Ocata Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/ocata
|
||||
@@ -1,5 +0,0 @@
|
||||
============================
|
||||
Current Series Release Notes
|
||||
============================
|
||||
|
||||
.. release-notes::
|
||||
@@ -1,15 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
PrettyTable<0.8,>=0.7.1 # BSD
|
||||
requests>=2.14.2 # Apache-2.0
|
||||
simplejson>=2.2.0 # MIT
|
||||
oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0
|
||||
oslo.utils>=3.20.0 # Apache-2.0
|
||||
Babel!=2.4.0,>=2.3.4 # BSD
|
||||
keystoneauth1>=2.21.0 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
||||
python-swiftclient>=3.2.0 # Apache-2.0
|
||||
python-mistralclient>=3.1.0 # Apache-2.0
|
||||
osc-lib>=1.5.1 # Apache-2.0
|
||||
73
run_local.sh
73
run_local.sh
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Specify the path to the RDL repo as argument one.
|
||||
# Argument 2 cna be a log file for the RDL output.
|
||||
# This script will create a .pid file and report in the current directory.
|
||||
|
||||
set -e
|
||||
|
||||
me=${0##*/}
|
||||
|
||||
function print_usage() {
|
||||
cat >&2 <<EOS
|
||||
Run tests against a local instance of trove
|
||||
|
||||
Usage: $me trove_path [logfile]
|
||||
EOS
|
||||
}
|
||||
|
||||
# parse options
|
||||
while getopts ":h" opt; do
|
||||
case "$opt" in
|
||||
h|\?) print_usage; exit 5 ;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND-1))
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
print_usage
|
||||
exit 5
|
||||
fi
|
||||
|
||||
trove_path=$1
|
||||
trove_pid_file="`pwd`.pid"
|
||||
|
||||
function start_server() {
|
||||
server_log=`pwd`/rdserver.txt
|
||||
set +e
|
||||
rm $server_log
|
||||
set -e
|
||||
pushd $trove_path
|
||||
bin/start_server.sh --pid-file=$trove_pid_file \
|
||||
--override-logfile=$server_log
|
||||
popd
|
||||
}
|
||||
|
||||
function stop_server() {
|
||||
if [ -f $trove_pid_file ];
|
||||
then
|
||||
pushd $trove_path
|
||||
bin/stop_server.sh $trove_pid_file
|
||||
popd
|
||||
else
|
||||
echo "The pid file did not exist, so not stopping server."
|
||||
fi
|
||||
}
|
||||
|
||||
function on_error() {
|
||||
echo "Something went wrong!"
|
||||
stop_server
|
||||
}
|
||||
|
||||
trap on_error EXIT # Proceed to trap - END in event of failure.
|
||||
|
||||
start_server
|
||||
tox -edocs
|
||||
mkdir -p .tox/docs/html
|
||||
.tox/docs/bin/sphinx-build -b doctest docs/source .tox/docs/html
|
||||
.tox/docs/bin/sphinx-build -b html docs/source .tox/docs/html
|
||||
stop_server
|
||||
|
||||
|
||||
trap - EXIT
|
||||
echo "Ran tests successfully. :)"
|
||||
exit 0
|
||||
60
setup.cfg
60
setup.cfg
@@ -1,60 +0,0 @@
|
||||
[metadata]
|
||||
name = python-troveclient
|
||||
summary = Client library for OpenStack DBaaS API
|
||||
description-file =
|
||||
README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
home-page = http://docs.openstack.org/developer/python-troveclient
|
||||
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 =
|
||||
troveclient
|
||||
|
||||
[entry_points]
|
||||
console_scripts =
|
||||
trove = troveclient.shell:main
|
||||
|
||||
openstack.cli.extension =
|
||||
database = troveclient.osc.plugin
|
||||
|
||||
openstack.database.v1 =
|
||||
database_backup_list = troveclient.osc.v1.database_backups:ListDatabaseBackups
|
||||
database_cluster_list = troveclient.osc.v1.database_clusters:ListDatabaseClusters
|
||||
database_configuration_list = troveclient.osc.v1.database_configurations:ListDatabaseConfigurations
|
||||
database_flavor_list = troveclient.osc.v1.database_flavors:ListDatabaseFlavors
|
||||
database_instance_list = troveclient.osc.v1.database_instances:ListDatabaseInstances
|
||||
database_limit_list = troveclient.osc.v1.database_limits:ListDatabaseLimits
|
||||
database_list = troveclient.osc.v1.databases:ListDatabases
|
||||
database_user_list = troveclient.osc.v1.database_users:ListDatabaseUsers
|
||||
datastore_list = troveclient.osc.v1.datastores:ListDatastores
|
||||
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
source-dir = doc/source
|
||||
build-dir = doc/build
|
||||
warning-is-error = 1
|
||||
|
||||
[upload_sphinx]
|
||||
upload-dir = doc/build/html
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
||||
[pbr]
|
||||
autodoc_index_modules = True
|
||||
api_doc_dir = reference/api
|
||||
autodoc_exclude_modules =
|
||||
troveclient.tests.*
|
||||
29
setup.py
29
setup.py
@@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=2.0.0'],
|
||||
pbr=True)
|
||||
@@ -1,18 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
|
||||
coverage!=4.4,>=4.0 # Apache-2.0
|
||||
fixtures>=3.0.0 # Apache-2.0/BSD
|
||||
oslotest>=1.10.0 # Apache-2.0
|
||||
python-openstackclient!=3.10.0,>=3.3.0 # Apache-2.0
|
||||
requests-mock>=1.1 # Apache-2.0
|
||||
sphinx>=1.6.2 # BSD
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
testscenarios>=0.4 # Apache-2.0/BSD
|
||||
testtools>=1.4.0 # MIT
|
||||
mock>=2.0 # BSD
|
||||
httplib2>=0.7.5 # MIT
|
||||
pycrypto>=2.6 # Public Domain
|
||||
reno!=2.3.1,>=1.8.0 # Apache-2.0
|
||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
||||
@@ -1,172 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 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.
|
||||
|
||||
"""Provides methods needed by installation script for OpenStack development
|
||||
virtual environments.
|
||||
|
||||
Since this script is used to bootstrap a virtualenv from the system's Python
|
||||
environment, it should be kept strictly compatible with Python 2.6.
|
||||
|
||||
Synced in from openstack-common
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import optparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
class InstallVenv(object):
|
||||
|
||||
def __init__(self, root, venv, requirements,
|
||||
test_requirements, py_version,
|
||||
project):
|
||||
self.root = root
|
||||
self.venv = venv
|
||||
self.requirements = requirements
|
||||
self.test_requirements = test_requirements
|
||||
self.py_version = py_version
|
||||
self.project = project
|
||||
|
||||
def die(self, message, *args):
|
||||
print(message % args, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def check_python_version(self):
|
||||
if sys.version_info < (2, 6):
|
||||
self.die("Need Python Version >= 2.6")
|
||||
|
||||
def run_command_with_code(self, cmd, redirect_output=True,
|
||||
check_exit_code=True):
|
||||
"""Runs a command in an out-of-process shell.
|
||||
|
||||
Returns the output of that command. Working directory is self.root.
|
||||
"""
|
||||
if redirect_output:
|
||||
stdout = subprocess.PIPE
|
||||
else:
|
||||
stdout = None
|
||||
|
||||
proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
|
||||
output = proc.communicate()[0]
|
||||
if check_exit_code and proc.returncode != 0:
|
||||
self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
|
||||
return (output, proc.returncode)
|
||||
|
||||
def run_command(self, cmd, redirect_output=True, check_exit_code=True):
|
||||
return self.run_command_with_code(cmd, redirect_output,
|
||||
check_exit_code)[0]
|
||||
|
||||
def get_distro(self):
|
||||
if (os.path.exists('/etc/fedora-release') or
|
||||
os.path.exists('/etc/redhat-release')):
|
||||
return Fedora(
|
||||
self.root, self.venv, self.requirements,
|
||||
self.test_requirements, self.py_version, self.project)
|
||||
else:
|
||||
return Distro(
|
||||
self.root, self.venv, self.requirements,
|
||||
self.test_requirements, self.py_version, self.project)
|
||||
|
||||
def check_dependencies(self):
|
||||
self.get_distro().install_virtualenv()
|
||||
|
||||
def create_virtualenv(self, no_site_packages=True):
|
||||
"""Creates the virtual environment and installs PIP.
|
||||
|
||||
Creates the virtual environment and installs PIP only into the
|
||||
virtual environment.
|
||||
"""
|
||||
if not os.path.isdir(self.venv):
|
||||
print('Creating venv...', end=' ')
|
||||
if no_site_packages:
|
||||
self.run_command(['virtualenv', '-q', '--no-site-packages',
|
||||
self.venv])
|
||||
else:
|
||||
self.run_command(['virtualenv', '-q', self.venv])
|
||||
print('done.')
|
||||
else:
|
||||
print("venv already exists...")
|
||||
pass
|
||||
|
||||
def pip_install(self, *args):
|
||||
self.run_command(['tools/with_venv.sh',
|
||||
'pip', 'install', '--upgrade'] + list(args),
|
||||
redirect_output=False)
|
||||
|
||||
def install_dependencies(self):
|
||||
print('Installing dependencies with pip (this can take a while)...')
|
||||
|
||||
# First things first, make sure our venv has the latest pip and
|
||||
# setuptools and pbr
|
||||
self.pip_install('pip>=1.4')
|
||||
self.pip_install('setuptools')
|
||||
self.pip_install('pbr')
|
||||
|
||||
self.pip_install('-r', self.requirements, '-r', self.test_requirements)
|
||||
|
||||
def parse_args(self, argv):
|
||||
"""Parses command-line arguments."""
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option('-n', '--no-site-packages',
|
||||
action='store_true',
|
||||
help="Do not inherit packages from global Python "
|
||||
"install.")
|
||||
return parser.parse_args(argv[1:])[0]
|
||||
|
||||
|
||||
class Distro(InstallVenv):
|
||||
|
||||
def check_cmd(self, cmd):
|
||||
return bool(self.run_command(['which', cmd],
|
||||
check_exit_code=False).strip())
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if self.check_cmd('easy_install'):
|
||||
print('Installing virtualenv via easy_install...', end=' ')
|
||||
if self.run_command(['easy_install', 'virtualenv']):
|
||||
print('Succeeded')
|
||||
return
|
||||
else:
|
||||
print('Failed')
|
||||
|
||||
self.die('ERROR: virtualenv not found.\n\n%s development'
|
||||
' requires virtualenv, please install it using your'
|
||||
' favorite package management tool' % self.project)
|
||||
|
||||
|
||||
class Fedora(Distro):
|
||||
"""This covers all Fedora-based distributions.
|
||||
|
||||
Includes: Fedora, RHEL, CentOS, Scientific Linux
|
||||
"""
|
||||
|
||||
def check_pkg(self, pkg):
|
||||
return self.run_command_with_code(['rpm', '-q', pkg],
|
||||
check_exit_code=False)[1] == 0
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if not self.check_pkg('python-virtualenv'):
|
||||
self.die("Please install 'python-virtualenv'.")
|
||||
|
||||
super(Fedora, self).install_virtualenv()
|
||||
@@ -1,55 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Client constraint file contains this client version pin that is in conflict
|
||||
# with installing the client from source. We should replace the version pin in
|
||||
# the constraints file before applying it for from-source installation.
|
||||
|
||||
ZUUL_CLONER=/usr/zuul-env/bin/zuul-cloner
|
||||
BRANCH_NAME=master
|
||||
CLIENT_NAME=python-troveclient
|
||||
requirements_installed=$(echo "import openstack_requirements" | python 2>/dev/null ; echo $?)
|
||||
|
||||
set -e
|
||||
|
||||
CONSTRAINTS_FILE=$1
|
||||
shift
|
||||
|
||||
install_cmd="pip install"
|
||||
mydir=$(mktemp -dt "$CLIENT_NAME-tox_install-XXXXXXX")
|
||||
trap "rm -rf $mydir" EXIT
|
||||
localfile=$mydir/upper-constraints.txt
|
||||
if [[ $CONSTRAINTS_FILE != http* ]]; then
|
||||
CONSTRAINTS_FILE=file://$CONSTRAINTS_FILE
|
||||
fi
|
||||
curl $CONSTRAINTS_FILE -k -o $localfile
|
||||
install_cmd="$install_cmd -c$localfile"
|
||||
|
||||
if [ $requirements_installed -eq 0 ]; then
|
||||
echo "ALREADY INSTALLED" > /tmp/tox_install.txt
|
||||
echo "Requirements already installed; using existing package"
|
||||
elif [ -x "$ZUUL_CLONER" ]; then
|
||||
echo "ZUUL CLONER" > /tmp/tox_install.txt
|
||||
pushd $mydir
|
||||
$ZUUL_CLONER --cache-dir \
|
||||
/opt/git \
|
||||
--branch $BRANCH_NAME \
|
||||
git://git.openstack.org \
|
||||
openstack/requirements
|
||||
cd openstack/requirements
|
||||
$install_cmd -e .
|
||||
popd
|
||||
else
|
||||
echo "PIP HARDCODE" > /tmp/tox_install.txt
|
||||
if [ -z "$REQUIREMENTS_PIP_LOCATION" ]; then
|
||||
REQUIREMENTS_PIP_LOCATION="git+https://git.openstack.org/openstack/requirements@$BRANCH_NAME#egg=requirements"
|
||||
fi
|
||||
$install_cmd -U -e ${REQUIREMENTS_PIP_LOCATION}
|
||||
fi
|
||||
|
||||
# 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 "-e file://$PWD#egg=$CLIENT_NAME"
|
||||
|
||||
$install_cmd -U $*
|
||||
exit $?
|
||||
64
tox.ini
64
tox.ini
@@ -1,64 +0,0 @@
|
||||
# Python Trove Client
|
||||
|
||||
[tox]
|
||||
envlist = py34,py27,pypy,pep8
|
||||
minversion = 1.6
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
NOSE_WITH_OPENSTACK=1
|
||||
NOSE_OPENSTACK_COLOR=1
|
||||
NOSE_OPENSTACK_RED=0.05
|
||||
NOSE_OPENSTACK_YELLOW=0.025
|
||||
NOSE_OPENSTACK_SHOW_ELAPSED=1
|
||||
usedevelop = True
|
||||
install_command = {toxinidir}/tools/tox_install.sh \
|
||||
{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} \
|
||||
{opts} {packages}
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
commands = find . -type f -name "*.pyc" -delete
|
||||
rm -f .testrepository/times.dbm
|
||||
python setup.py testr --testr-args='{posargs}'
|
||||
whitelist_externals = find
|
||||
rm
|
||||
|
||||
[testenv:debug]
|
||||
commands = oslo_debug_helper -t troveclient/tests {posargs}
|
||||
|
||||
[testenv:debug-py27]
|
||||
basepython = python2.7
|
||||
commands = oslo_debug_helper -t troveclient/tests {posargs}
|
||||
|
||||
[testenv:debug-py34]
|
||||
basepython = python3.4
|
||||
commands = oslo_debug_helper -t troveclient/tests {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
commands = flake8
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:cover]
|
||||
commands =
|
||||
coverage erase
|
||||
python setup.py testr --coverage --testr-args='{posargs}'
|
||||
coverage html
|
||||
coverage report
|
||||
|
||||
[testenv:docs]
|
||||
commands =
|
||||
rm -rf doc/html doc/build
|
||||
python setup.py build_sphinx
|
||||
|
||||
[testenv:releasenotes]
|
||||
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|
||||
|
||||
[flake8]
|
||||
enable-extensions = H106,H203,H904
|
||||
ignore = H202,H405,H501
|
||||
show-source = True
|
||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build,releasenotes
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2013 Rackspace Hosting
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
__all__ = ['__version__']
|
||||
|
||||
import pbr.version
|
||||
|
||||
version_info = pbr.version.VersionInfo('python-troveclient')
|
||||
# We have a circular import problem when we first run python setup.py sdist
|
||||
# It's harmless, so deflect it.
|
||||
try:
|
||||
__version__ = version_info.version_string()
|
||||
except AttributeError:
|
||||
__version__ = None
|
||||
@@ -1,50 +0,0 @@
|
||||
# Copyright 2016 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""oslo.i18n integration module.
|
||||
|
||||
See http://docs.openstack.org/developer/oslo.i18n/usage.html .
|
||||
|
||||
"""
|
||||
|
||||
import oslo_i18n
|
||||
|
||||
DOMAIN = "troveclient"
|
||||
|
||||
_translators = oslo_i18n.TranslatorFactory(domain=DOMAIN)
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# The contextual translation function using the name "_C"
|
||||
# requires oslo.i18n >=2.1.0
|
||||
_C = _translators.contextual_form
|
||||
|
||||
# The plural translation function using the name "_P"
|
||||
# requires oslo.i18n >=2.1.0
|
||||
_P = _translators.plural_form
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||
# the level.
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
||||
|
||||
|
||||
def get_available_languages():
|
||||
return oslo_i18n.get_available_languages(DOMAIN)
|
||||
@@ -1,221 +0,0 @@
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 Spanish National Research Council.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
import abc
|
||||
import argparse
|
||||
import os
|
||||
|
||||
import six
|
||||
from stevedore import extension
|
||||
|
||||
from troveclient.apiclient import exceptions
|
||||
|
||||
|
||||
_discovered_plugins = {}
|
||||
|
||||
|
||||
def discover_auth_systems():
|
||||
"""Discover the available auth-systems.
|
||||
|
||||
This won't take into account the old style auth-systems.
|
||||
"""
|
||||
global _discovered_plugins
|
||||
_discovered_plugins = {}
|
||||
|
||||
def add_plugin(ext):
|
||||
_discovered_plugins[ext.name] = ext.plugin
|
||||
|
||||
ep_namespace = "troveclient.apiclient.auth"
|
||||
mgr = extension.ExtensionManager(ep_namespace)
|
||||
mgr.map(add_plugin)
|
||||
|
||||
|
||||
def load_auth_system_opts(parser):
|
||||
"""Load options needed by the available auth-systems into a parser.
|
||||
|
||||
This function will try to populate the parser with options from the
|
||||
available plugins.
|
||||
"""
|
||||
group = parser.add_argument_group("Common auth options")
|
||||
BaseAuthPlugin.add_common_opts(group)
|
||||
for name, auth_plugin in six.iteritems(_discovered_plugins):
|
||||
group = parser.add_argument_group(
|
||||
"Auth-system '%s' options" % name,
|
||||
conflict_handler="resolve")
|
||||
auth_plugin.add_opts(group)
|
||||
|
||||
|
||||
def load_plugin(auth_system):
|
||||
try:
|
||||
plugin_class = _discovered_plugins[auth_system]
|
||||
except KeyError:
|
||||
raise exceptions.AuthSystemNotFound(auth_system)
|
||||
return plugin_class(auth_system=auth_system)
|
||||
|
||||
|
||||
def load_plugin_from_args(args):
|
||||
"""Load required plugin and populate it with options.
|
||||
|
||||
Try to guess auth system if it is not specified. Systems are tried in
|
||||
alphabetical order.
|
||||
|
||||
:type args: argparse.Namespace
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
auth_system = args.os_auth_system
|
||||
if auth_system:
|
||||
plugin = load_plugin(auth_system)
|
||||
plugin.parse_opts(args)
|
||||
plugin.sufficient_options()
|
||||
return plugin
|
||||
|
||||
for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)):
|
||||
plugin_class = _discovered_plugins[plugin_auth_system]
|
||||
plugin = plugin_class()
|
||||
plugin.parse_opts(args)
|
||||
try:
|
||||
plugin.sufficient_options()
|
||||
except exceptions.AuthPluginOptionsMissing:
|
||||
continue
|
||||
return plugin
|
||||
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class BaseAuthPlugin(object):
|
||||
"""Base class for authentication plugins.
|
||||
|
||||
An authentication plugin needs to override at least the authenticate
|
||||
method to be a valid plugin.
|
||||
"""
|
||||
|
||||
auth_system = None
|
||||
opt_names = []
|
||||
common_opt_names = [
|
||||
"auth_system",
|
||||
"username",
|
||||
"password",
|
||||
"tenant_name",
|
||||
"token",
|
||||
"auth_url",
|
||||
]
|
||||
|
||||
def __init__(self, auth_system=None, **kwargs):
|
||||
self.auth_system = auth_system or self.auth_system
|
||||
self.opts = dict((name, kwargs.get(name))
|
||||
for name in self.opt_names)
|
||||
|
||||
@staticmethod
|
||||
def _parser_add_opt(parser, opt):
|
||||
"""Add an option to parser in two variants.
|
||||
|
||||
:param opt: option name (with underscores)
|
||||
"""
|
||||
dashed_opt = opt.replace("_", "-")
|
||||
env_var = "OS_%s" % opt.upper()
|
||||
arg_default = os.environ.get(env_var, "")
|
||||
arg_help = "Defaults to env[%s]." % env_var
|
||||
parser.add_argument(
|
||||
"--os-%s" % dashed_opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
default=arg_default,
|
||||
help=arg_help)
|
||||
parser.add_argument(
|
||||
"--os_%s" % opt,
|
||||
metavar="<%s>" % dashed_opt,
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
@classmethod
|
||||
def add_opts(cls, parser):
|
||||
"""Populate the parser with the options for this plugin.
|
||||
"""
|
||||
for opt in cls.opt_names:
|
||||
# use `BaseAuthPlugin.common_opt_names` since it is never
|
||||
# changed in child classes
|
||||
if opt not in BaseAuthPlugin.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@classmethod
|
||||
def add_common_opts(cls, parser):
|
||||
"""Add options that are common for several plugins.
|
||||
"""
|
||||
for opt in cls.common_opt_names:
|
||||
cls._parser_add_opt(parser, opt)
|
||||
|
||||
@staticmethod
|
||||
def get_opt(opt_name, args):
|
||||
"""Return option name and value.
|
||||
|
||||
:param opt_name: name of the option, e.g., "username"
|
||||
:param args: parsed arguments
|
||||
"""
|
||||
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
||||
|
||||
def parse_opts(self, args):
|
||||
"""Parse the actual auth-system options if any.
|
||||
|
||||
This method is expected to populate the attribute `self.opts` with a
|
||||
dict containing the options and values needed to make authentication.
|
||||
"""
|
||||
self.opts.update(dict(self.get_opt(opt_name, args)
|
||||
for opt_name in self.opt_names))
|
||||
|
||||
def authenticate(self, http_client):
|
||||
"""Authenticate using plugin defined method.
|
||||
|
||||
The method usually analyses `self.opts` and performs
|
||||
a request to authentication server.
|
||||
|
||||
:param http_client: client object that needs authentication
|
||||
:type http_client: troveclient.client.HTTPClient
|
||||
:raises: AuthorizationFailure
|
||||
"""
|
||||
self.sufficient_options()
|
||||
self._do_authenticate(http_client)
|
||||
|
||||
@abc.abstractmethod
|
||||
def _do_authenticate(self, http_client):
|
||||
"""Protected method for authentication.
|
||||
"""
|
||||
|
||||
def sufficient_options(self):
|
||||
"""Check if all required options are present.
|
||||
|
||||
:raises: AuthPluginOptionsMissing
|
||||
"""
|
||||
missing = [opt
|
||||
for opt in self.opt_names
|
||||
if not self.opts.get(opt)]
|
||||
if missing:
|
||||
raise exceptions.AuthPluginOptionsMissing(missing)
|
||||
|
||||
@abc.abstractmethod
|
||||
def token_and_endpoint(self, endpoint_type, service_type):
|
||||
"""Return token and endpoint.
|
||||
|
||||
:param service_type: Service type of the endpoint
|
||||
:type service_type: string
|
||||
:param endpoint_type: Type of endpoint.
|
||||
Possible values: public or publicURL,
|
||||
internal or internalURL,
|
||||
admin or adminURL
|
||||
:type endpoint_type: string
|
||||
:returns: tuple of token and endpoint strings
|
||||
:raises: EndpointException
|
||||
"""
|
||||
@@ -1,496 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2012 Grid Dynamics
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
# E1102: %s is not callable
|
||||
# pylint: disable=E1102
|
||||
|
||||
import abc
|
||||
import copy
|
||||
|
||||
from oslo_utils import reflection
|
||||
from oslo_utils import strutils
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from troveclient.apiclient import exceptions
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Return id if argument is a Resource.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
if obj.uuid:
|
||||
return obj.uuid
|
||||
except AttributeError:
|
||||
pass
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
||||
class HookableMixin(object):
|
||||
"""Mixin so classes can register and run hooks."""
|
||||
_hooks_map = {}
|
||||
|
||||
@classmethod
|
||||
def add_hook(cls, hook_type, hook_func):
|
||||
"""Add a new hook of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param hook_func: hook function
|
||||
"""
|
||||
if hook_type not in cls._hooks_map:
|
||||
cls._hooks_map[hook_type] = []
|
||||
|
||||
cls._hooks_map[hook_type].append(hook_func)
|
||||
|
||||
@classmethod
|
||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
||||
"""Run all hooks of specified type.
|
||||
|
||||
:param cls: class that registers hooks
|
||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
||||
:param args: args to be passed to every hook function
|
||||
:param kwargs: kwargs to be passed to every hook function
|
||||
"""
|
||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||
for hook_func in hook_funcs:
|
||||
hook_func(*args, **kwargs)
|
||||
|
||||
|
||||
class BaseManager(HookableMixin):
|
||||
"""Basic manager type providing common operations.
|
||||
|
||||
Managers interact with a particular type of API (servers, flavors, images,
|
||||
etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, client):
|
||||
"""Initializes BaseManager with `client`.
|
||||
|
||||
:param client: instance of BaseClient descendant for HTTP requests
|
||||
"""
|
||||
super(BaseManager, self).__init__()
|
||||
self.client = client
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, json=None):
|
||||
"""List the collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
:param obj_class: class for constructing the returned objects
|
||||
(self.resource_class will be used by default)
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
"""
|
||||
if json:
|
||||
body = self.client.post(url, json=json).json()
|
||||
else:
|
||||
body = self.client.get(url).json()
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key]
|
||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||
# unlike other services which just return the list...
|
||||
try:
|
||||
data = data['values']
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||
|
||||
def _get(self, url, response_key):
|
||||
"""Get an object from collection.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'server'
|
||||
"""
|
||||
body = self.client.get(url).json()
|
||||
return self.resource_class(self, body[response_key], loaded=True)
|
||||
|
||||
def _head(self, url):
|
||||
"""Retrieve request headers for an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
"""
|
||||
resp = self.client.head(url)
|
||||
return resp.status_code == 204
|
||||
|
||||
def _post(self, url, json, response_key, return_raw=False):
|
||||
"""Create an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
:param return_raw: flag to force returning raw JSON instead of
|
||||
Python object of self.resource_class
|
||||
"""
|
||||
body = self.client.post(url, json=json).json()
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _put(self, url, json=None, response_key=None):
|
||||
"""Update an object with PUT method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
"""
|
||||
resp = self.client.put(url, json=json)
|
||||
# PUT requests may not return a body
|
||||
if resp.content:
|
||||
body = resp.json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _patch(self, url, json=None, response_key=None):
|
||||
"""Update an object with PATCH method.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers'
|
||||
:param json: data that will be encoded as JSON and passed in POST
|
||||
request (GET will be sent by default)
|
||||
:param response_key: the key to be looked up in response dictionary,
|
||||
e.g., 'servers'
|
||||
"""
|
||||
body = self.client.patch(url, json=json).json()
|
||||
if response_key is not None:
|
||||
return self.resource_class(self, body[response_key])
|
||||
else:
|
||||
return self.resource_class(self, body)
|
||||
|
||||
def _delete(self, url):
|
||||
"""Delete an object.
|
||||
|
||||
:param url: a partial URL, e.g., '/servers/my-server'
|
||||
"""
|
||||
return self.client.delete(url)
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class ManagerWithFind(BaseManager):
|
||||
"""Manager with additional `find()`/`findall()` methods."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def find(self, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
matches = self.findall(**kwargs)
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||
raise exceptions.NotFound(msg)
|
||||
elif num_matches > 1:
|
||||
raise exceptions.NoUniqueMatch()
|
||||
else:
|
||||
return matches[0]
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class CrudManager(BaseManager):
|
||||
"""Base manager class for manipulating entities.
|
||||
|
||||
Children of this class are expected to define a `collection_key` and `key`.
|
||||
|
||||
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
||||
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
||||
objects containing a list of member resources (e.g. `{'entities': [{},
|
||||
{}, {}]}`).
|
||||
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
||||
refer to an individual member of the collection.
|
||||
|
||||
"""
|
||||
collection_key = None
|
||||
key = None
|
||||
|
||||
def build_url(self, base_url=None, **kwargs):
|
||||
"""Builds a resource URL for the given kwargs.
|
||||
|
||||
Given an example collection where `collection_key = 'entities'` and
|
||||
`key = 'entity'`, the following URL's could be generated.
|
||||
|
||||
By default, the URL will represent a collection of entities, e.g.::
|
||||
|
||||
/entities
|
||||
|
||||
If kwargs contains an `entity_id`, then the URL will represent a
|
||||
specific member, e.g.::
|
||||
|
||||
/entities/{entity_id}
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
url = base_url if base_url is not None else ''
|
||||
|
||||
url += '/%s' % self.collection_key
|
||||
|
||||
# do we have a specific entity?
|
||||
entity_id = kwargs.get('%s_id' % self.key)
|
||||
if entity_id is not None:
|
||||
url += '/%s' % entity_id
|
||||
|
||||
return url
|
||||
|
||||
def _filter_kwargs(self, kwargs):
|
||||
"""Drop null values and handle ids."""
|
||||
for key, ref in six.iteritems(kwargs.copy()):
|
||||
if ref is None:
|
||||
kwargs.pop(key)
|
||||
else:
|
||||
if isinstance(ref, Resource):
|
||||
kwargs.pop(key)
|
||||
kwargs['%s_id' % key] = getid(ref)
|
||||
return kwargs
|
||||
|
||||
def create(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._post(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: kwargs},
|
||||
self.key)
|
||||
|
||||
def get(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._get(
|
||||
self.build_url(**kwargs),
|
||||
self.key)
|
||||
|
||||
def head(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
return self._head(self.build_url(**kwargs))
|
||||
|
||||
def list(self, base_url=None, **kwargs):
|
||||
"""List the collection.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
|
||||
def put(self, base_url=None, **kwargs):
|
||||
"""Update an element.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._put(self.build_url(base_url=base_url, **kwargs))
|
||||
|
||||
def update(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
params = kwargs.copy()
|
||||
params.pop('%s_id' % self.key)
|
||||
|
||||
return self._patch(
|
||||
self.build_url(**kwargs),
|
||||
{self.key: params},
|
||||
self.key)
|
||||
|
||||
def delete(self, **kwargs):
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
return self._delete(
|
||||
self.build_url(**kwargs))
|
||||
|
||||
def find(self, base_url=None, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
:param base_url: if provided, the generated URL will be appended to it
|
||||
"""
|
||||
kwargs = self._filter_kwargs(kwargs)
|
||||
|
||||
rl = self._list(
|
||||
'%(base_url)s%(query)s' % {
|
||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
||||
},
|
||||
self.collection_key)
|
||||
num = len(rl)
|
||||
|
||||
if num == 0:
|
||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||
raise exceptions.NotFound(404, msg)
|
||||
elif num > 1:
|
||||
raise exceptions.NoUniqueMatch
|
||||
else:
|
||||
return rl[0]
|
||||
|
||||
|
||||
class Extension(HookableMixin):
|
||||
"""Extension descriptor."""
|
||||
|
||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
||||
manager_class = None
|
||||
|
||||
def __init__(self, name, module):
|
||||
super(Extension, self).__init__()
|
||||
self.name = name
|
||||
self.module = module
|
||||
self._parse_extension_module()
|
||||
|
||||
def _parse_extension_module(self):
|
||||
self.manager_class = None
|
||||
for attr_name, attr_value in self.module.__dict__.items():
|
||||
if attr_name in self.SUPPORTED_HOOKS:
|
||||
self.add_hook(attr_name, attr_value)
|
||||
else:
|
||||
try:
|
||||
if issubclass(attr_value, BaseManager):
|
||||
self.manager_class = attr_value
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension '%s'>" % self.name
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
"""
|
||||
|
||||
HUMAN_ID = False
|
||||
NAME_ATTR = 'name'
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
"""Populate and bind to a manager.
|
||||
|
||||
:param manager: BaseManager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k
|
||||
for k in self.__dict__.keys()
|
||||
if k[0] != '_' and k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
self_cls_name = reflection.get_class_name(self,
|
||||
fully_qualified=False)
|
||||
return "<%s %s>" % (self_cls_name, info)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Human-readable ID which can be used for bash completion.
|
||||
"""
|
||||
if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
|
||||
return strutils.to_slug(getattr(self, self.NAME_ATTR))
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in six.iteritems(info):
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
self._info[k] = v
|
||||
except AttributeError:
|
||||
# In this case we already defined the attribute on the class
|
||||
pass
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded:
|
||||
self._get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def _get(self):
|
||||
# set _loaded first ... so if we have to bail, we know we tried.
|
||||
self._loaded = True
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Resource):
|
||||
return NotImplemented
|
||||
# two resources of different types are not equal
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
||||
@property
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def to_dict(self):
|
||||
return copy.deepcopy(self._info)
|
||||
@@ -1,361 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 Grid Dynamics
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
# E0202: An attribute inherited from %s hide this method
|
||||
# pylint: disable=E0202
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
try:
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json
|
||||
|
||||
import requests
|
||||
|
||||
from oslo_utils import importutils
|
||||
from troveclient.apiclient import exceptions
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPClient(object):
|
||||
"""This client handles sending HTTP requests to OpenStack servers.
|
||||
|
||||
Features:
|
||||
|
||||
- share authentication information between several clients to different
|
||||
services (e.g., for compute and image clients);
|
||||
- reissue authentication request for expired tokens;
|
||||
- encode/decode JSON bodies;
|
||||
- raise exceptions on HTTP errors;
|
||||
- pluggable authentication;
|
||||
- store authentication information in a keyring;
|
||||
- store time spent for requests;
|
||||
- register clients for particular services, so one can use
|
||||
`http_client.identity` or `http_client.compute`;
|
||||
- log requests and responses in a format that is easy to copy-and-paste
|
||||
into terminal and send the same request with curl.
|
||||
|
||||
"""
|
||||
|
||||
user_agent = "troveclient.apiclient"
|
||||
|
||||
def __init__(self,
|
||||
auth_plugin,
|
||||
region_name=None,
|
||||
endpoint_type="publicURL",
|
||||
original_ip=None,
|
||||
verify=True,
|
||||
cert=None,
|
||||
timeout=None,
|
||||
timings=False,
|
||||
keyring_saver=None,
|
||||
debug=False,
|
||||
user_agent=None,
|
||||
http=None):
|
||||
self.auth_plugin = auth_plugin
|
||||
|
||||
self.endpoint_type = endpoint_type
|
||||
self.region_name = region_name
|
||||
|
||||
self.original_ip = original_ip
|
||||
self.timeout = timeout
|
||||
self.verify = verify
|
||||
self.cert = cert
|
||||
|
||||
self.keyring_saver = keyring_saver
|
||||
self.debug = debug
|
||||
self.user_agent = user_agent or self.user_agent
|
||||
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
self.timings = timings
|
||||
|
||||
# requests within the same session can reuse TCP connections from pool
|
||||
self.http = http or requests.Session()
|
||||
|
||||
self.cached_token = None
|
||||
|
||||
def _http_log_req(self, method, url, kwargs):
|
||||
if not self.debug:
|
||||
return
|
||||
|
||||
string_parts = [
|
||||
"curl -i",
|
||||
"-X '%s'" % method,
|
||||
"'%s'" % url,
|
||||
]
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
LOG.debug("REQ: %s", " ".join(string_parts))
|
||||
if 'data' in kwargs:
|
||||
LOG.debug("REQ BODY: %s\n", kwargs['data'])
|
||||
|
||||
def _http_log_resp(self, resp):
|
||||
if not self.debug:
|
||||
return
|
||||
LOG.debug(
|
||||
"RESP: [%s] %s\n",
|
||||
resp.status_code,
|
||||
resp.headers)
|
||||
if resp._content_consumed:
|
||||
LOG.debug(
|
||||
"RESP BODY: %s\n",
|
||||
resp.text)
|
||||
|
||||
def serialize(self, kwargs):
|
||||
if kwargs.get('json') is not None:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = json.dumps(kwargs['json'])
|
||||
try:
|
||||
del kwargs['json']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def reset_timings(self):
|
||||
self.times = []
|
||||
|
||||
def request(self, method, url, **kwargs):
|
||||
"""Send an http request with the specified characteristics.
|
||||
|
||||
Wrapper around `requests.Session.request` to handle tasks such as
|
||||
setting headers, JSON encoding/decoding, and error handling.
|
||||
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
requests.Session.request (such as `headers`) or `json`
|
||||
that will be encoded as JSON and used as `data` argument
|
||||
"""
|
||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
||||
kwargs["headers"]["User-Agent"] = self.user_agent
|
||||
if self.original_ip:
|
||||
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
||||
self.original_ip, self.user_agent)
|
||||
if self.timeout is not None:
|
||||
kwargs.setdefault("timeout", self.timeout)
|
||||
kwargs.setdefault("verify", self.verify)
|
||||
if self.cert is not None:
|
||||
kwargs.setdefault("cert", self.cert)
|
||||
self.serialize(kwargs)
|
||||
|
||||
self._http_log_req(method, url, kwargs)
|
||||
if self.timings:
|
||||
start_time = time.time()
|
||||
resp = self.http.request(method, url, **kwargs)
|
||||
if self.timings:
|
||||
self.times.append(("%s %s" % (method, url),
|
||||
start_time, time.time()))
|
||||
self._http_log_resp(resp)
|
||||
|
||||
if resp.status_code >= 400:
|
||||
LOG.debug(
|
||||
"Request returned failure status: %s",
|
||||
resp.status_code)
|
||||
raise exceptions.from_response(resp, method, url)
|
||||
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def concat_url(endpoint, url):
|
||||
"""Concatenate endpoint and final URL.
|
||||
|
||||
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
||||
"http://keystone/v2.0/tokens".
|
||||
|
||||
:param endpoint: the base URL
|
||||
:param url: the final URL
|
||||
"""
|
||||
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
||||
|
||||
def client_request(self, client, method, url, **kwargs):
|
||||
"""Send an http request using `client`'s endpoint and specified `url`.
|
||||
|
||||
If request was rejected as unauthorized (possibly because the token is
|
||||
expired), issue one authorization attempt and send the request once
|
||||
again.
|
||||
|
||||
:param client: instance of BaseClient descendant
|
||||
:param method: method of HTTP request
|
||||
:param url: URL of HTTP request
|
||||
:param kwargs: any other parameter that can be passed to
|
||||
`HTTPClient.request`
|
||||
|
||||
"""
|
||||
|
||||
filter_args = {
|
||||
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
||||
"service_type": client.service_type,
|
||||
}
|
||||
token, endpoint = (self.cached_token, client.cached_endpoint)
|
||||
just_authenticated = False
|
||||
if not (token and endpoint):
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
pass
|
||||
if not (token and endpoint):
|
||||
self.authenticate()
|
||||
just_authenticated = True
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
if not (token and endpoint):
|
||||
raise exceptions.AuthorizationFailure(
|
||||
"Cannot find endpoint or token for request")
|
||||
|
||||
old_token_endpoint = (token, endpoint)
|
||||
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
# Perform the request once. If we get Unauthorized, then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
except exceptions.Unauthorized as unauth_ex:
|
||||
if just_authenticated:
|
||||
raise
|
||||
self.cached_token = None
|
||||
client.cached_endpoint = None
|
||||
self.authenticate()
|
||||
try:
|
||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
||||
**filter_args)
|
||||
except exceptions.EndpointException:
|
||||
raise unauth_ex
|
||||
if (not (token and endpoint) or
|
||||
old_token_endpoint == (token, endpoint)):
|
||||
raise unauth_ex
|
||||
self.cached_token = token
|
||||
client.cached_endpoint = endpoint
|
||||
kwargs["headers"]["X-Auth-Token"] = token
|
||||
return self.request(
|
||||
method, self.concat_url(endpoint, url), **kwargs)
|
||||
|
||||
def add_client(self, base_client_instance):
|
||||
"""Add a new instance of :class:`BaseClient` descendant.
|
||||
|
||||
`self` will store a reference to `base_client_instance`.
|
||||
|
||||
Example:
|
||||
|
||||
>>> def test_clients():
|
||||
... from keystoneclient.auth import keystone
|
||||
... from openstack.common.apiclient import client
|
||||
... auth = keystone.KeystoneAuthPlugin(
|
||||
... username="user", password="pass", tenant_name="tenant",
|
||||
... auth_url="http://auth:5000/v2.0")
|
||||
... openstack_client = client.HTTPClient(auth)
|
||||
... # create nova client
|
||||
... from novaclient.v1_1 import client
|
||||
... client.Client(openstack_client)
|
||||
... # create keystone client
|
||||
... from keystoneclient.v2_0 import client
|
||||
... client.Client(openstack_client)
|
||||
... # use them
|
||||
... openstack_client.identity.tenants.list()
|
||||
... openstack_client.compute.servers.list()
|
||||
"""
|
||||
service_type = base_client_instance.service_type
|
||||
if service_type and not hasattr(self, service_type):
|
||||
setattr(self, service_type, base_client_instance)
|
||||
|
||||
def authenticate(self):
|
||||
self.auth_plugin.authenticate(self)
|
||||
# Store the authentication results in the keyring for later requests
|
||||
if self.keyring_saver:
|
||||
self.keyring_saver.save(self)
|
||||
|
||||
|
||||
class BaseClient(object):
|
||||
"""Top-level object to access the OpenStack API.
|
||||
|
||||
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
||||
will handle a bunch of issues such as authentication.
|
||||
"""
|
||||
|
||||
service_type = None
|
||||
endpoint_type = None # "publicURL" will be used
|
||||
cached_endpoint = None
|
||||
|
||||
def __init__(self, http_client, extensions=None):
|
||||
self.http_client = http_client
|
||||
http_client.add_client(self)
|
||||
|
||||
# Add in any extensions...
|
||||
if extensions:
|
||||
for extension in extensions:
|
||||
if extension.manager_class:
|
||||
setattr(self, extension.name,
|
||||
extension.manager_class(self))
|
||||
|
||||
def client_request(self, method, url, **kwargs):
|
||||
return self.http_client.client_request(
|
||||
self, method, url, **kwargs)
|
||||
|
||||
def head(self, url, **kwargs):
|
||||
return self.client_request("HEAD", url, **kwargs)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self.client_request("GET", url, **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self.client_request("POST", url, **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self.client_request("PUT", url, **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self.client_request("DELETE", url, **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self.client_request("PATCH", url, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def get_class(api_name, version, version_map):
|
||||
"""Returns the client class for the requested API version
|
||||
|
||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
||||
:param version: the requested API version
|
||||
:param version_map: a dict of client classes keyed by version
|
||||
:rtype: a client class for the requested API version
|
||||
"""
|
||||
try:
|
||||
client_path = version_map[str(version)]
|
||||
except (KeyError, ValueError):
|
||||
msg = "Invalid %s client version '%s'. must be one of: %s" % (
|
||||
(api_name, version, ', '.join(version_map.keys())))
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
|
||||
return importutils.import_class(client_path)
|
||||
@@ -1,445 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 Nebula, Inc.
|
||||
# Copyright 2013 Alessio Ababilov
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class MissingArgs(ClientException):
|
||||
"""Supplied arguments are not sufficient for calling a function."""
|
||||
def __init__(self, missing, message=None):
|
||||
self.missing = missing
|
||||
self.message = message or "Missing argument(s): %s"
|
||||
self.message %= ", ".join(missing)
|
||||
super(MissingArgs, self).__init__(self.message)
|
||||
|
||||
|
||||
class ValidationError(ClientException):
|
||||
"""Error in validation on API client side."""
|
||||
pass
|
||||
|
||||
|
||||
class UnsupportedVersion(ClientException):
|
||||
"""User is trying to use an unsupported version of the API."""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(ClientException):
|
||||
"""Error in CLI tool."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(ClientException):
|
||||
"""Cannot authorize API client."""
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionRefused(ClientException):
|
||||
"""Cannot connect to API service."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
||||
"""Auth plugin misses some options."""
|
||||
def __init__(self, opt_names):
|
||||
super(AuthPluginOptionsMissing, self).__init__(
|
||||
"Authentication failed. Missing options: %s" %
|
||||
", ".join(opt_names))
|
||||
self.opt_names = opt_names
|
||||
|
||||
|
||||
class AuthSystemNotFound(AuthorizationFailure):
|
||||
"""User has specified a AuthSystem that is not installed."""
|
||||
def __init__(self, auth_system):
|
||||
super(AuthSystemNotFound, self).__init__(
|
||||
"AuthSystemNotFound: %s" % repr(auth_system))
|
||||
self.auth_system = auth_system
|
||||
|
||||
|
||||
class NoUniqueMatch(ClientException):
|
||||
"""Multiple entities found instead of one."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointException(ClientException):
|
||||
"""Something is rotten in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(EndpointException):
|
||||
"""Could not find requested endpoint in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(EndpointException):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
super(AmbiguousEndpoints, self).__init__(
|
||||
"AmbiguousEndpoints: %s" % repr(endpoints))
|
||||
self.endpoints = endpoints
|
||||
|
||||
|
||||
class HttpError(ClientException):
|
||||
"""The base exception class for all HTTP exceptions.
|
||||
"""
|
||||
http_status = 0
|
||||
message = "HTTP Error"
|
||||
|
||||
def __init__(self, message=None, details=None,
|
||||
response=None, request_id=None,
|
||||
url=None, method=None, http_status=None):
|
||||
self.http_status = http_status or self.http_status
|
||||
self.message = message or self.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
self.response = response
|
||||
self.url = url
|
||||
self.method = method
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
||||
if request_id:
|
||||
formatted_string += " (Request-ID: %s)" % request_id
|
||||
super(HttpError, self).__init__(formatted_string)
|
||||
|
||||
|
||||
class HTTPClientError(HttpError):
|
||||
"""Client-side HTTP error.
|
||||
|
||||
Exception for cases in which the client seems to have erred.
|
||||
"""
|
||||
message = "HTTP Client Error"
|
||||
|
||||
|
||||
class HttpServerError(HttpError):
|
||||
"""Server-side HTTP error.
|
||||
|
||||
Exception for cases in which the server is aware that it has
|
||||
erred or is incapable of performing the request.
|
||||
"""
|
||||
message = "HTTP Server Error"
|
||||
|
||||
|
||||
class BadRequest(HTTPClientError):
|
||||
"""HTTP 400 - Bad Request.
|
||||
|
||||
The request cannot be fulfilled due to bad syntax.
|
||||
"""
|
||||
http_status = 400
|
||||
message = "Bad Request"
|
||||
|
||||
|
||||
class Unauthorized(HTTPClientError):
|
||||
"""HTTP 401 - Unauthorized.
|
||||
|
||||
Similar to 403 Forbidden, but specifically for use when authentication
|
||||
is required and has failed or has not yet been provided.
|
||||
"""
|
||||
http_status = 401
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class PaymentRequired(HTTPClientError):
|
||||
"""HTTP 402 - Payment Required.
|
||||
|
||||
Reserved for future use.
|
||||
"""
|
||||
http_status = 402
|
||||
message = "Payment Required"
|
||||
|
||||
|
||||
class Forbidden(HTTPClientError):
|
||||
"""HTTP 403 - Forbidden.
|
||||
|
||||
The request was a valid request, but the server is refusing to respond
|
||||
to it.
|
||||
"""
|
||||
http_status = 403
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(HTTPClientError):
|
||||
"""HTTP 404 - Not Found.
|
||||
|
||||
The requested resource could not be found but may be available again
|
||||
in the future.
|
||||
"""
|
||||
http_status = 404
|
||||
message = "Not Found"
|
||||
|
||||
|
||||
class MethodNotAllowed(HTTPClientError):
|
||||
"""HTTP 405 - Method Not Allowed.
|
||||
|
||||
A request was made of a resource using a request method not supported
|
||||
by that resource.
|
||||
"""
|
||||
http_status = 405
|
||||
message = "Method Not Allowed"
|
||||
|
||||
|
||||
class NotAcceptable(HTTPClientError):
|
||||
"""HTTP 406 - Not Acceptable.
|
||||
|
||||
The requested resource is only capable of generating content not
|
||||
acceptable according to the Accept headers sent in the request.
|
||||
"""
|
||||
http_status = 406
|
||||
message = "Not Acceptable"
|
||||
|
||||
|
||||
class ProxyAuthenticationRequired(HTTPClientError):
|
||||
"""HTTP 407 - Proxy Authentication Required.
|
||||
|
||||
The client must first authenticate itself with the proxy.
|
||||
"""
|
||||
http_status = 407
|
||||
message = "Proxy Authentication Required"
|
||||
|
||||
|
||||
class RequestTimeout(HTTPClientError):
|
||||
"""HTTP 408 - Request Timeout.
|
||||
|
||||
The server timed out waiting for the request.
|
||||
"""
|
||||
http_status = 408
|
||||
message = "Request Timeout"
|
||||
|
||||
|
||||
class Conflict(HTTPClientError):
|
||||
"""HTTP 409 - Conflict.
|
||||
|
||||
Indicates that the request could not be processed because of conflict
|
||||
in the request, such as an edit conflict.
|
||||
"""
|
||||
http_status = 409
|
||||
message = "Conflict"
|
||||
|
||||
|
||||
class Gone(HTTPClientError):
|
||||
"""HTTP 410 - Gone.
|
||||
|
||||
Indicates that the resource requested is no longer available and will
|
||||
not be available again.
|
||||
"""
|
||||
http_status = 410
|
||||
message = "Gone"
|
||||
|
||||
|
||||
class LengthRequired(HTTPClientError):
|
||||
"""HTTP 411 - Length Required.
|
||||
|
||||
The request did not specify the length of its content, which is
|
||||
required by the requested resource.
|
||||
"""
|
||||
http_status = 411
|
||||
message = "Length Required"
|
||||
|
||||
|
||||
class PreconditionFailed(HTTPClientError):
|
||||
"""HTTP 412 - Precondition Failed.
|
||||
|
||||
The server does not meet one of the preconditions that the requester
|
||||
put on the request.
|
||||
"""
|
||||
http_status = 412
|
||||
message = "Precondition Failed"
|
||||
|
||||
|
||||
class RequestEntityTooLarge(HTTPClientError):
|
||||
"""HTTP 413 - Request Entity Too Large.
|
||||
|
||||
The request is larger than the server is willing or able to process.
|
||||
"""
|
||||
http_status = 413
|
||||
message = "Request Entity Too Large"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
try:
|
||||
self.retry_after = int(kwargs.pop('retry_after'))
|
||||
except (KeyError, ValueError):
|
||||
self.retry_after = 0
|
||||
|
||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class RequestUriTooLong(HTTPClientError):
|
||||
"""HTTP 414 - Request-URI Too Long.
|
||||
|
||||
The URI provided was too long for the server to process.
|
||||
"""
|
||||
http_status = 414
|
||||
message = "Request-URI Too Long"
|
||||
|
||||
|
||||
class UnsupportedMediaType(HTTPClientError):
|
||||
"""HTTP 415 - Unsupported Media Type.
|
||||
|
||||
The request entity has a media type which the server or resource does
|
||||
not support.
|
||||
"""
|
||||
http_status = 415
|
||||
message = "Unsupported Media Type"
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
||||
|
||||
The client has asked for a portion of the file, but the server cannot
|
||||
supply that portion.
|
||||
"""
|
||||
http_status = 416
|
||||
message = "Requested Range Not Satisfiable"
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPClientError):
|
||||
"""HTTP 417 - Expectation Failed.
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header field.
|
||||
"""
|
||||
http_status = 417
|
||||
message = "Expectation Failed"
|
||||
|
||||
|
||||
class UnprocessableEntity(HTTPClientError):
|
||||
"""HTTP 422 - Unprocessable Entity.
|
||||
|
||||
The request was well-formed but was unable to be followed due to semantic
|
||||
errors.
|
||||
"""
|
||||
http_status = 422
|
||||
message = "Unprocessable Entity"
|
||||
|
||||
|
||||
class InternalServerError(HttpServerError):
|
||||
"""HTTP 500 - Internal Server Error.
|
||||
|
||||
A generic error message, given when no more specific message is suitable.
|
||||
"""
|
||||
http_status = 500
|
||||
message = "Internal Server Error"
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HttpNotImplemented(HttpServerError):
|
||||
"""HTTP 501 - Not Implemented.
|
||||
|
||||
The server either does not recognize the request method, or it lacks
|
||||
the ability to fulfill the request.
|
||||
"""
|
||||
http_status = 501
|
||||
message = "Not Implemented"
|
||||
|
||||
|
||||
class BadGateway(HttpServerError):
|
||||
"""HTTP 502 - Bad Gateway.
|
||||
|
||||
The server was acting as a gateway or proxy and received an invalid
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 502
|
||||
message = "Bad Gateway"
|
||||
|
||||
|
||||
class ServiceUnavailable(HttpServerError):
|
||||
"""HTTP 503 - Service Unavailable.
|
||||
|
||||
The server is currently unavailable.
|
||||
"""
|
||||
http_status = 503
|
||||
message = "Service Unavailable"
|
||||
|
||||
|
||||
class GatewayTimeout(HttpServerError):
|
||||
"""HTTP 504 - Gateway Timeout.
|
||||
|
||||
The server was acting as a gateway or proxy and did not receive a timely
|
||||
response from the upstream server.
|
||||
"""
|
||||
http_status = 504
|
||||
message = "Gateway Timeout"
|
||||
|
||||
|
||||
class HttpVersionNotSupported(HttpServerError):
|
||||
"""HTTP 505 - HttpVersion Not Supported.
|
||||
|
||||
The server does not support the HTTP protocol version used in the request.
|
||||
"""
|
||||
http_status = 505
|
||||
message = "HTTP Version Not Supported"
|
||||
|
||||
|
||||
# _code_map contains all the classes that have http_status attribute.
|
||||
_code_map = dict(
|
||||
(getattr(obj, 'http_status', None), obj)
|
||||
for name, obj in six.iteritems(vars(sys.modules[__name__]))
|
||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
||||
)
|
||||
|
||||
|
||||
def from_response(response, method, url):
|
||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
||||
|
||||
:param response: instance of `requests.Response` class
|
||||
:param method: HTTP method used for request
|
||||
:param url: URL used for request
|
||||
"""
|
||||
kwargs = {
|
||||
"http_status": response.status_code,
|
||||
"response": response,
|
||||
"method": method,
|
||||
"url": url,
|
||||
"request_id": response.headers.get("x-compute-request-id"),
|
||||
}
|
||||
if "retry-after" in response.headers:
|
||||
kwargs["retry_after"] = response.headers["retry-after"]
|
||||
|
||||
content_type = response.headers.get("Content-Type", "")
|
||||
if content_type.startswith("application/json"):
|
||||
try:
|
||||
body = response.json()
|
||||
except ValueError:
|
||||
pass
|
||||
else:
|
||||
if isinstance(body, dict):
|
||||
error = list(body.values())[0]
|
||||
kwargs["message"] = error.get("message", None)
|
||||
kwargs["details"] = error.get("details", None)
|
||||
elif content_type.startswith("text/"):
|
||||
kwargs["details"] = response.text
|
||||
|
||||
try:
|
||||
cls = _code_map[response.status_code]
|
||||
except KeyError:
|
||||
if 500 <= response.status_code < 600:
|
||||
cls = HttpServerError
|
||||
elif 400 <= response.status_code < 500:
|
||||
cls = HTTPClientError
|
||||
else:
|
||||
cls = HttpError
|
||||
return cls(**kwargs)
|
||||
@@ -1,107 +0,0 @@
|
||||
# Copyright 2014 Rackspace
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 Spanish National Research Council.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
import pkg_resources
|
||||
import six
|
||||
|
||||
from troveclient._i18n import _
|
||||
from troveclient import exceptions
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
_discovered_plugins = {}
|
||||
|
||||
|
||||
def discover_auth_systems():
|
||||
"""Discover the available auth-systems.
|
||||
|
||||
This won't take into account the old style auth-systems.
|
||||
"""
|
||||
ep_name = 'openstack.client.auth_plugin'
|
||||
for ep in pkg_resources.iter_entry_points(ep_name):
|
||||
try:
|
||||
auth_plugin = ep.load()
|
||||
except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e:
|
||||
LOG.debug(_("ERROR: Cannot load auth plugin %s"), ep.name)
|
||||
LOG.debug(e, exc_info=1)
|
||||
else:
|
||||
_discovered_plugins[ep.name] = auth_plugin
|
||||
|
||||
|
||||
def load_auth_system_opts(parser):
|
||||
"""Load options needed by the available auth-systems into a parser.
|
||||
|
||||
This function will try to populate the parser with options from the
|
||||
available plugins.
|
||||
"""
|
||||
for name, auth_plugin in six.iteritems(_discovered_plugins):
|
||||
add_opts_fn = getattr(auth_plugin, "add_opts", None)
|
||||
if add_opts_fn:
|
||||
group = parser.add_argument_group("Auth-system '%s' options" %
|
||||
name)
|
||||
add_opts_fn(group)
|
||||
|
||||
|
||||
def load_plugin(auth_system):
|
||||
if auth_system in _discovered_plugins:
|
||||
return _discovered_plugins[auth_system]()
|
||||
|
||||
raise exceptions.AuthSystemNotFound(auth_system)
|
||||
|
||||
|
||||
class BaseAuthPlugin(object):
|
||||
"""Base class for authentication plugins.
|
||||
|
||||
An authentication plugin needs to override at least the authenticate
|
||||
method to be a valid plugin.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.opts = {}
|
||||
|
||||
def get_auth_url(self):
|
||||
"""Return the auth url for the plugin (if any)."""
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def add_opts(parser):
|
||||
"""Populate and return the parser with the options for this plugin.
|
||||
|
||||
If the plugin does not need any options, it should return the same
|
||||
parser untouched.
|
||||
"""
|
||||
return parser
|
||||
|
||||
def parse_opts(self, args):
|
||||
"""Parse the actual auth-system options if any.
|
||||
|
||||
This method is expected to populate the attribute self.opts with a
|
||||
dict containing the options and values needed to make authentication.
|
||||
If the dict is empty, the client should assume that it needs the same
|
||||
options as the 'keystone' auth system (i.e. os_username and
|
||||
os_password).
|
||||
|
||||
Returns the self.opts dict.
|
||||
"""
|
||||
return self.opts
|
||||
|
||||
def authenticate(self, cls, auth_url):
|
||||
"""Authenticate using plugin defined method."""
|
||||
raise exceptions.AuthSystemNotFound(self.auth_system)
|
||||
@@ -1,265 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2013 Rackspace Hosting
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
import abc
|
||||
import contextlib
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from troveclient.apiclient import base
|
||||
from troveclient.apiclient import exceptions
|
||||
from troveclient import common
|
||||
from troveclient import utils
|
||||
|
||||
# Python 2.4 compat
|
||||
try:
|
||||
all
|
||||
except NameError:
|
||||
def all(iterable):
|
||||
return True not in (not x for x in iterable)
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Retrieves an id from object or integer.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an object's
|
||||
ID as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(utils.HookableMixin):
|
||||
"""Manager defining CRUD operations for API.
|
||||
|
||||
Managers interact with a particular type of API (servers, flavors,
|
||||
images, etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _paginated(self, url, response_key, limit=None, marker=None,
|
||||
query_strings=None):
|
||||
query_strings = query_strings or {}
|
||||
url = common.append_query_strings(url, limit=limit, marker=marker,
|
||||
**query_strings)
|
||||
resp, body = self.api.client.get(url)
|
||||
if not body:
|
||||
raise Exception("Call to " + url + " did not return a body.")
|
||||
links = body.get('links', [])
|
||||
next_links = [link['href'] for link in links if link['rel'] == 'next']
|
||||
next_marker = None
|
||||
for link in next_links:
|
||||
# Extract the marker from the url.
|
||||
parsed_url = parse.urlparse(link)
|
||||
query_dict = dict(parse.parse_qsl(parsed_url.query))
|
||||
next_marker = query_dict.get('marker')
|
||||
data = [self.resource_class(self, res) for res in body[response_key]]
|
||||
return common.Paginated(data, next_marker=next_marker, links=links)
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp = None
|
||||
if body:
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
else:
|
||||
resp, body = self.api.client.get(url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key]
|
||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||
# unlike other services which just return the list...
|
||||
if isinstance(data, dict):
|
||||
try:
|
||||
data = data['values']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
with self.completion_cache('human_id', obj_class, mode="w"):
|
||||
with self.completion_cache('uuid', obj_class, mode="w"):
|
||||
return [obj_class(self, res, loaded=True)
|
||||
for res in data if res]
|
||||
|
||||
@contextlib.contextmanager
|
||||
def completion_cache(self, cache_type, obj_class, mode):
|
||||
"""Bash-completion cache.
|
||||
|
||||
The completion cache store items that can be used for bash
|
||||
autocompletion, like UUIDs or human-friendly IDs.
|
||||
|
||||
A resource listing will clear and repopulate the cache.
|
||||
|
||||
A resource create will append to the cache.
|
||||
|
||||
Delete is not handled because listings are assumed to be performed
|
||||
often enough to keep the cache reasonably up-to-date.
|
||||
"""
|
||||
base_dir = utils.env('TROVECLIENT_UUID_CACHE_DIR',
|
||||
default="~/.troveclient")
|
||||
|
||||
# NOTE(sirp): Keep separate UUID caches for each username + endpoint
|
||||
# pair
|
||||
username = utils.env('OS_USERNAME', 'TROVE_USERNAME')
|
||||
url = utils.env('OS_URL', 'NOVA_URL')
|
||||
uniqifier = hashlib.md5(username.encode('utf-8') +
|
||||
url.encode('utf-8')).hexdigest()
|
||||
|
||||
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
|
||||
|
||||
try:
|
||||
os.makedirs(cache_dir, 0o755)
|
||||
except OSError:
|
||||
# NOTE(kiall): This is typically either permission denied while
|
||||
# attempting to create the directory, or the directory
|
||||
# already exists. Either way, don't fail.
|
||||
pass
|
||||
|
||||
resource = obj_class.__name__.lower()
|
||||
filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
|
||||
path = os.path.join(cache_dir, filename)
|
||||
|
||||
cache_attr = "_%s_cache" % cache_type
|
||||
|
||||
try:
|
||||
setattr(self, cache_attr, open(path, mode))
|
||||
except IOError:
|
||||
# NOTE(kiall): This is typically a permission denied while
|
||||
# attempting to write the cache file.
|
||||
pass
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cache = getattr(self, cache_attr, None)
|
||||
if cache:
|
||||
cache.close()
|
||||
delattr(self, cache_attr)
|
||||
|
||||
def write_to_completion_cache(self, cache_type, val):
|
||||
cache = getattr(self, "_%s_cache" % cache_type, None)
|
||||
if cache:
|
||||
cache.write("%s\n" % val)
|
||||
|
||||
def _get(self, url, response_key=None):
|
||||
resp, body = self.api.client.get(url)
|
||||
if response_key:
|
||||
return self.resource_class(self, body[response_key], loaded=True)
|
||||
else:
|
||||
return self.resource_class(self, body, loaded=True)
|
||||
|
||||
def _create(self, url, body, response_key, return_raw=False, **kwargs):
|
||||
self.run_hooks('modify_body_for_create', body, **kwargs)
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
if body:
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
|
||||
with self.completion_cache('human_id', self.resource_class,
|
||||
mode="a"):
|
||||
with self.completion_cache('uuid', self.resource_class,
|
||||
mode="a"):
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _delete(self, url):
|
||||
resp, body = self.api.client.delete(url)
|
||||
|
||||
def _update(self, url, body, **kwargs):
|
||||
self.run_hooks('modify_body_for_update', body, **kwargs)
|
||||
resp, body = self.api.client.put(url, body=body)
|
||||
return body
|
||||
|
||||
def _edit(self, url, body):
|
||||
resp, body = self.api.client.patch(url, body=body)
|
||||
return body
|
||||
|
||||
|
||||
class ManagerWithFind(six.with_metaclass(abc.ABCMeta, Manager)):
|
||||
"""Like a `Manager`, but with additional `find()`/`findall()` methods."""
|
||||
|
||||
@abc.abstractmethod
|
||||
def list(self):
|
||||
pass
|
||||
|
||||
def find(self, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
matches = self.findall(**kwargs)
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||
raise exceptions.NotFound(404, msg)
|
||||
elif num_matches > 1:
|
||||
raise exceptions.NoUniqueMatch
|
||||
else:
|
||||
return matches[0]
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = list(kwargs.items())
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(self.get(obj.id))
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
|
||||
class Resource(base.Resource):
|
||||
"""A resource represents a particular instance of an object like server.
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
:param manager: Manager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
HUMAN_ID = False
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
super(Resource, self).__init__(manager, info, loaded)
|
||||
|
||||
# NOTE(sirp): ensure `id` is already present because if it isn't we'll
|
||||
# enter an infinite loop of __getattr__ -> get -> __init__ ->
|
||||
# __getattr__ -> ...
|
||||
if 'id' in self.__dict__ and len(str(self.id)) == 36:
|
||||
self.manager.write_to_completion_cache('uuid', self.id)
|
||||
|
||||
human_id = self.human_id
|
||||
if human_id:
|
||||
self.manager.write_to_completion_cache('human_id', human_id)
|
||||
@@ -1,507 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2013 Rackspace Hosting
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
|
||||
from keystoneauth1 import adapter
|
||||
from oslo_utils import importutils
|
||||
import requests
|
||||
import six.moves.urllib.parse as urlparse
|
||||
|
||||
from troveclient.apiclient import client
|
||||
from troveclient import exceptions
|
||||
from troveclient import service_catalog
|
||||
|
||||
try:
|
||||
import eventlet as sleep_lib
|
||||
except ImportError:
|
||||
import time as sleep_lib
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
# Python 2.5 compat fix
|
||||
if not hasattr(urlparse, 'parse_qsl'):
|
||||
import cgi
|
||||
urlparse.parse_qsl = cgi.parse_qsl
|
||||
|
||||
osprofiler_web = importutils.try_import("osprofiler.web")
|
||||
|
||||
|
||||
class TroveClientMixin(object):
|
||||
|
||||
def get_database_api_version_from_endpoint(self):
|
||||
magic_tuple = urlparse.urlsplit(self.management_url)
|
||||
scheme, netloc, path, query, frag = magic_tuple
|
||||
v = path.split("/")[1]
|
||||
valid_versions = ['v1.0']
|
||||
if v not in valid_versions:
|
||||
msg = "Invalid client version '%s'. must be one of: %s" % (
|
||||
(v, ', '.join(valid_versions)))
|
||||
raise exceptions.UnsupportedVersion(msg)
|
||||
return v[1:]
|
||||
|
||||
|
||||
class HTTPClient(TroveClientMixin):
|
||||
|
||||
USER_AGENT = 'python-troveclient'
|
||||
|
||||
def __init__(self, user, password, projectid, auth_url, insecure=False,
|
||||
timeout=None, tenant_id=None, proxy_tenant_id=None,
|
||||
proxy_token=None, region_name=None,
|
||||
endpoint_type='publicURL', service_type=None,
|
||||
service_name=None, database_service_name=None, retries=None,
|
||||
http_log_debug=False, cacert=None, bypass_url=None,
|
||||
auth_system='keystone', auth_plugin=None):
|
||||
|
||||
if auth_system and auth_system != 'keystone' and not auth_plugin:
|
||||
raise exceptions.AuthSystemNotFound(auth_system)
|
||||
|
||||
if not auth_url and auth_system and auth_system != 'keystone':
|
||||
auth_url = auth_plugin.get_auth_url()
|
||||
if not auth_url:
|
||||
raise exceptions.EndpointNotFound()
|
||||
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.projectid = projectid
|
||||
self.tenant_id = tenant_id
|
||||
|
||||
self.auth_url = auth_url.rstrip('/') if auth_url else auth_url
|
||||
self.version = 'v1'
|
||||
self.region_name = region_name
|
||||
self.endpoint_type = endpoint_type
|
||||
self.service_type = service_type
|
||||
self.service_name = service_name
|
||||
self.database_service_name = database_service_name
|
||||
self.retries = int(retries or 0)
|
||||
self.http_log_debug = http_log_debug
|
||||
|
||||
self.management_url = None
|
||||
self.auth_token = None
|
||||
self.proxy_token = proxy_token
|
||||
self.proxy_tenant_id = proxy_tenant_id
|
||||
self.timeout = timeout
|
||||
self.bypass_url = bypass_url
|
||||
self.auth_system = auth_system
|
||||
self.auth_plugin = auth_plugin
|
||||
|
||||
if insecure:
|
||||
self.verify_cert = False
|
||||
else:
|
||||
if cacert:
|
||||
self.verify_cert = cacert
|
||||
else:
|
||||
self.verify_cert = True
|
||||
|
||||
self.auth_system = auth_system
|
||||
self.auth_plugin = auth_plugin
|
||||
|
||||
self.LOG = logging.getLogger(__name__)
|
||||
if self.http_log_debug and not self.LOG.handlers:
|
||||
ch = logging.StreamHandler()
|
||||
self.LOG.setLevel(logging.DEBUG)
|
||||
self.LOG.addHandler(ch)
|
||||
if hasattr(requests, 'logging'):
|
||||
requests.logging.getLogger(requests.__name__).addHandler(ch)
|
||||
|
||||
def http_log_req(self, args, kwargs):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST', 'DELETE', 'PUT'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
if 'data' in kwargs:
|
||||
string_parts.append(" -d '%s'" % (kwargs['data']))
|
||||
self.LOG.debug("\nREQ: %s\n", "".join(string_parts))
|
||||
|
||||
def http_log_resp(self, resp):
|
||||
if not self.http_log_debug:
|
||||
return
|
||||
self.LOG.debug(
|
||||
"RESP: [%s] %s\nRESP BODY: %s\n",
|
||||
resp.status_code,
|
||||
resp.headers,
|
||||
resp.text)
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
if osprofiler_web:
|
||||
kwargs['headers'].update(osprofiler_web.get_trace_id_headers())
|
||||
if 'body' in kwargs:
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
kwargs['data'] = json.dumps(kwargs['body'])
|
||||
del kwargs['body']
|
||||
|
||||
if self.timeout:
|
||||
kwargs.setdefault('timeout', self.timeout)
|
||||
self.http_log_req((url, method,), kwargs)
|
||||
resp = requests.request(
|
||||
method,
|
||||
url,
|
||||
verify=self.verify_cert,
|
||||
**kwargs)
|
||||
self.http_log_resp(resp)
|
||||
|
||||
if resp.text:
|
||||
try:
|
||||
body = json.loads(resp.text)
|
||||
except ValueError:
|
||||
pass
|
||||
body = None
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status_code >= 400:
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
auth_attempts = 0
|
||||
attempts = 0
|
||||
backoff = 1
|
||||
while True:
|
||||
attempts += 1
|
||||
if not self.management_url or not self.auth_token:
|
||||
self.authenticate()
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.projectid:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.projectid
|
||||
try:
|
||||
resp, body = self.request(self.management_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
except exceptions.BadRequest:
|
||||
if attempts > self.retries:
|
||||
raise
|
||||
except exceptions.Unauthorized:
|
||||
if auth_attempts > 0:
|
||||
raise
|
||||
self.LOG.debug("Unauthorized, reauthenticating.")
|
||||
self.management_url = self.auth_token = None
|
||||
# First reauth. Discount this attempt.
|
||||
attempts -= 1
|
||||
auth_attempts += 1
|
||||
continue
|
||||
except exceptions.ClientException as e:
|
||||
if attempts > self.retries:
|
||||
raise
|
||||
if 500 <= e.code <= 599:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
# Catch a connection refused from requests.request
|
||||
self.LOG.debug("Connection refused: %s", e)
|
||||
msg = 'Unable to establish connection: %s' % e
|
||||
raise exceptions.ConnectionRefused(msg)
|
||||
self.LOG.debug(
|
||||
"Failed attempt(%s of %s), retrying in %s seconds",
|
||||
attempts, self.retries, backoff)
|
||||
sleep_lib.sleep(backoff)
|
||||
backoff *= 2
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PATCH', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def _extract_service_catalog(self, url, resp, body, extract_token=True):
|
||||
"""See what the auth service told us and process the response.
|
||||
We may get redirected to another site, fail or actually get
|
||||
back a service catalog with a token and our endpoints.
|
||||
"""
|
||||
|
||||
if resp.status_code == 200: # content must always present
|
||||
try:
|
||||
self.auth_url = url
|
||||
self.service_catalog = \
|
||||
service_catalog.ServiceCatalog(body)
|
||||
|
||||
if extract_token:
|
||||
self.auth_token = self.service_catalog.get_token()
|
||||
|
||||
management_url = self.service_catalog.url_for(
|
||||
attr='region',
|
||||
filter_value=self.region_name,
|
||||
endpoint_type=self.endpoint_type,
|
||||
service_type=self.service_type,
|
||||
service_name=self.service_name,
|
||||
database_service_name=self.database_service_name)
|
||||
self.management_url = management_url.rstrip('/')
|
||||
return None
|
||||
except exceptions.AmbiguousEndpoints:
|
||||
print("Found more than one valid endpoint. Use a more "
|
||||
"restrictive filter")
|
||||
raise
|
||||
except KeyError:
|
||||
raise exceptions.AuthorizationFailure()
|
||||
except exceptions.EndpointNotFound:
|
||||
print("Could not find any suitable endpoint. Correct region?")
|
||||
raise
|
||||
elif resp.status_code == 305:
|
||||
return resp['location']
|
||||
else:
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
def _fetch_endpoints_from_auth(self, url):
|
||||
"""We have a token, but don't know the final endpoint for
|
||||
the region. We have to go back to the auth service and
|
||||
ask again. This request requires an admin-level token
|
||||
to work. The proxy token supplied could be from a low-level enduser.
|
||||
|
||||
We can't get this from the keystone service endpoint, we have to use
|
||||
the admin endpoint.
|
||||
|
||||
This will overwrite our admin token with the user token.
|
||||
"""
|
||||
|
||||
# GET ...:5001/v2.0/tokens/#####/endpoints
|
||||
url = '/'.join([url, 'tokens', '%s?belongsTo=%s'
|
||||
% (self.proxy_token, self.proxy_tenant_id)])
|
||||
self.LOG.debug("Using Endpoint URL: %s", url)
|
||||
resp, body = self.request(url, "GET",
|
||||
headers={'X-Auth-Token': self.auth_token})
|
||||
return self._extract_service_catalog(url, resp, body,
|
||||
extract_token=False)
|
||||
|
||||
def authenticate(self):
|
||||
magic_tuple = urlparse.urlsplit(self.auth_url)
|
||||
scheme, netloc, path, query, frag = magic_tuple
|
||||
port = magic_tuple.port
|
||||
if port is None:
|
||||
port = 80
|
||||
path_parts = path.split('/')
|
||||
for part in path_parts:
|
||||
if len(part) > 0 and part[0] == 'v':
|
||||
self.version = part
|
||||
break
|
||||
|
||||
# TODO(sandy): Assume admin endpoint is 35357 for now.
|
||||
# Ideally this is going to have to be provided by the service catalog.
|
||||
new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,))
|
||||
admin_url = urlparse.urlunsplit((scheme, new_netloc,
|
||||
path, query, frag))
|
||||
|
||||
auth_url = self.auth_url
|
||||
if self.version == "v2.0":
|
||||
while auth_url:
|
||||
if not self.auth_system or self.auth_system == 'keystone':
|
||||
auth_url = self._v2_auth(auth_url)
|
||||
else:
|
||||
auth_url = self._plugin_auth(auth_url)
|
||||
|
||||
# Are we acting on behalf of another user via an
|
||||
# existing token? If so, our actual endpoints may
|
||||
# be different than that of the admin token.
|
||||
if self.proxy_token:
|
||||
self._fetch_endpoints_from_auth(admin_url)
|
||||
# Since keystone no longer returns the user token
|
||||
# with the endpoints any more, we need to replace
|
||||
# our service account token with the user token.
|
||||
self.auth_token = self.proxy_token
|
||||
|
||||
else:
|
||||
try:
|
||||
while auth_url:
|
||||
auth_url = self._v1_auth(auth_url)
|
||||
# In some configurations trove makes redirection to
|
||||
# v2.0 keystone endpoint. Also, new location does not contain
|
||||
# real endpoint, only hostname and port.
|
||||
except exceptions.AuthorizationFailure:
|
||||
if auth_url.find('v2.0') < 0:
|
||||
auth_url = auth_url + '/v2.0'
|
||||
self._v2_auth(auth_url)
|
||||
|
||||
# Allows for setting an endpoint not defined in the catalog
|
||||
if self.bypass_url is not None and self.bypass_url != '':
|
||||
self.management_url = self.bypass_url
|
||||
|
||||
def _plugin_auth(self, auth_url):
|
||||
return self.auth_plugin.authenticate(self, auth_url)
|
||||
|
||||
def _v1_auth(self, url):
|
||||
if self.proxy_token:
|
||||
raise exceptions.NoTokenLookupException()
|
||||
|
||||
headers = {'X-Auth-User': self.user,
|
||||
'X-Auth-Key': self.password}
|
||||
if self.projectid:
|
||||
headers['X-Auth-Project-Id'] = self.projectid
|
||||
|
||||
resp, body = self.request(url, 'GET', headers=headers)
|
||||
if resp.status_code in (200, 204): # in some cases we get No Content
|
||||
try:
|
||||
mgmt_header = 'x-server-management-url'
|
||||
self.management_url = resp.headers[mgmt_header].rstrip('/')
|
||||
self.auth_token = resp.headers['x-auth-token']
|
||||
self.auth_url = url
|
||||
except (KeyError, TypeError):
|
||||
raise exceptions.AuthorizationFailure()
|
||||
elif resp.status_code == 305:
|
||||
return resp.headers['location']
|
||||
else:
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
def _v2_auth(self, url):
|
||||
"""Authenticate against a v2.0 auth service."""
|
||||
body = {"auth": {
|
||||
"passwordCredentials": {"username": self.user,
|
||||
"password": self.password}}}
|
||||
|
||||
if self.projectid:
|
||||
body['auth']['tenantName'] = self.projectid
|
||||
elif self.tenant_id:
|
||||
body['auth']['tenantId'] = self.tenant_id
|
||||
|
||||
self._authenticate(url, body)
|
||||
|
||||
def _authenticate(self, url, body):
|
||||
"""Authenticate and extract the service catalog."""
|
||||
token_url = url + "/tokens"
|
||||
|
||||
# Make sure we follow redirects when trying to reach Keystone
|
||||
resp, body = self.request(
|
||||
token_url,
|
||||
"POST",
|
||||
body=body,
|
||||
allow_redirects=True)
|
||||
|
||||
return self._extract_service_catalog(url, resp, body)
|
||||
|
||||
|
||||
class SessionClient(adapter.LegacyJsonAdapter, TroveClientMixin):
|
||||
|
||||
def __init__(self, session, auth, **kwargs):
|
||||
self.database_service_name = kwargs.pop('database_service_name', None)
|
||||
|
||||
super(SessionClient, self).__init__(session=session,
|
||||
auth=auth,
|
||||
**kwargs)
|
||||
|
||||
# FIXME(jamielennox): this is going to cause an authentication request
|
||||
# on client init. This is different to how the other clients work.
|
||||
endpoint = self.get_endpoint()
|
||||
|
||||
if not endpoint:
|
||||
raise exceptions.EndpointNotFound()
|
||||
|
||||
self.management_url = endpoint.rstrip('/')
|
||||
|
||||
def request(self, url, method, **kwargs):
|
||||
raise_exc = kwargs.pop('raise_exc', True)
|
||||
resp, body = super(SessionClient, self).request(url,
|
||||
method,
|
||||
raise_exc=False,
|
||||
**kwargs)
|
||||
|
||||
if raise_exc and resp.status_code >= 400:
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
return resp, body
|
||||
|
||||
|
||||
def _construct_http_client(username=None, password=None, project_id=None,
|
||||
auth_url=None, insecure=False, timeout=None,
|
||||
proxy_tenant_id=None, proxy_token=None,
|
||||
region_name=None, endpoint_type='publicURL',
|
||||
service_type='database',
|
||||
service_name=None, database_service_name=None,
|
||||
retries=None,
|
||||
http_log_debug=False,
|
||||
auth_system='keystone', auth_plugin=None,
|
||||
cacert=None, bypass_url=None, tenant_id=None,
|
||||
session=None,
|
||||
**kwargs):
|
||||
if session:
|
||||
try:
|
||||
kwargs.setdefault('interface', endpoint_type)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
return SessionClient(session=session,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
region_name=region_name,
|
||||
database_service_name=database_service_name,
|
||||
connect_retries=retries,
|
||||
**kwargs)
|
||||
else:
|
||||
return HTTPClient(username,
|
||||
password,
|
||||
projectid=project_id,
|
||||
auth_url=auth_url,
|
||||
insecure=insecure,
|
||||
timeout=timeout,
|
||||
tenant_id=tenant_id,
|
||||
proxy_token=proxy_token,
|
||||
proxy_tenant_id=proxy_tenant_id,
|
||||
region_name=region_name,
|
||||
endpoint_type=endpoint_type,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
database_service_name=database_service_name,
|
||||
retries=retries,
|
||||
http_log_debug=http_log_debug,
|
||||
cacert=cacert,
|
||||
bypass_url=bypass_url,
|
||||
auth_system=auth_system,
|
||||
auth_plugin=auth_plugin,
|
||||
)
|
||||
|
||||
|
||||
def get_version_map():
|
||||
return {
|
||||
'1.0': 'troveclient.v1.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def Client(version, *args, **kwargs):
|
||||
version_map = get_version_map()
|
||||
client_class = client.BaseClient.get_class('database',
|
||||
version, version_map)
|
||||
return client_class(*args, **kwargs)
|
||||
@@ -1,51 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2013 Rackspace Hosting
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from six.moves.urllib import parse
|
||||
|
||||
from troveclient.apiclient import exceptions
|
||||
|
||||
|
||||
def check_for_exceptions(resp, body, url):
|
||||
if resp.status_code in (400, 422, 500):
|
||||
raise exceptions.from_response(resp, body, url)
|
||||
|
||||
|
||||
def append_query_strings(url, **query_strings):
|
||||
if not query_strings:
|
||||
return url
|
||||
query = '&'.join('{0}={1}'.format(key, val)
|
||||
for key, val in query_strings.items() if val)
|
||||
return url + ('?' + query if query else "")
|
||||
|
||||
|
||||
def quote_user_host(user, host):
|
||||
quoted = ''
|
||||
if host:
|
||||
quoted = parse.quote("%s@%s" % (user, host))
|
||||
else:
|
||||
quoted = parse.quote("%s" % user)
|
||||
return quoted.replace('.', '%2e')
|
||||
|
||||
|
||||
class Paginated(list):
|
||||
|
||||
def __init__(self, items=None, next_marker=None, links=None):
|
||||
items = items or []
|
||||
links = links or []
|
||||
super(Paginated, self).__init__(items)
|
||||
self.next = next_marker
|
||||
self.links = links
|
||||
@@ -1,32 +0,0 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
from troveclient.compat.client import Dbaas # noqa
|
||||
from troveclient.compat.client import TroveHTTPClient # noqa
|
||||
from troveclient.compat.versions import Versions # noqa
|
||||
from troveclient.v1.accounts import Accounts # noqa
|
||||
from troveclient.v1.databases import Databases # noqa
|
||||
from troveclient.v1.diagnostics import DiagnosticsInterrogator # noqa
|
||||
from troveclient.v1.diagnostics import HwInfoInterrogator # noqa
|
||||
from troveclient.v1.flavors import Flavors # noqa
|
||||
from troveclient.v1.hosts import Hosts # noqa
|
||||
from troveclient.v1.instances import Instances # noqa
|
||||
from troveclient.v1.management import Management # noqa
|
||||
from troveclient.v1.management import MgmtFlavors # noqa
|
||||
from troveclient.v1.management import RootHistory # noqa
|
||||
from troveclient.v1.root import Root # noqa
|
||||
from troveclient.v1.storage import StorageInfo # noqa
|
||||
from troveclient.v1.users import Users # noqa
|
||||
@@ -1,233 +0,0 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import print_function
|
||||
import six
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
|
||||
def get_authenticator_cls(cls_or_name):
|
||||
"""Factory method to retrieve Authenticator class."""
|
||||
if isinstance(cls_or_name, type):
|
||||
return cls_or_name
|
||||
elif isinstance(cls_or_name, six.string_types):
|
||||
if cls_or_name == "keystone":
|
||||
return KeyStoneV2Authenticator
|
||||
elif cls_or_name == "auth1.1":
|
||||
return Auth1_1
|
||||
elif cls_or_name == "fake":
|
||||
return FakeAuth
|
||||
|
||||
raise ValueError("Could not determine authenticator class from the given "
|
||||
"value %r." % cls_or_name)
|
||||
|
||||
|
||||
class Authenticator(object):
|
||||
"""Helper class to perform Keystone or other miscellaneous authentication.
|
||||
|
||||
The "authenticate" method returns a ServiceCatalog, which can be used
|
||||
to obtain a token.
|
||||
|
||||
"""
|
||||
|
||||
URL_REQUIRED = True
|
||||
|
||||
def __init__(self, client, type, url, username, password, tenant,
|
||||
region=None, service_type=None, service_name=None,
|
||||
service_url=None):
|
||||
self.client = client
|
||||
self.type = type
|
||||
self.url = url
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.tenant = tenant
|
||||
self.region = region
|
||||
self.service_type = service_type
|
||||
self.service_name = service_name
|
||||
self.service_url = service_url
|
||||
|
||||
def _authenticate(self, url, body, root_key='access'):
|
||||
"""Authenticate and extract the service catalog."""
|
||||
# Make sure we follow redirects when trying to reach Keystone
|
||||
tmp_follow_all_redirects = self.client.follow_all_redirects
|
||||
self.client.follow_all_redirects = True
|
||||
|
||||
try:
|
||||
resp, body = self.client._time_request(url, "POST", body=body)
|
||||
finally:
|
||||
self.client.follow_all_redirects = tmp_follow_all_redirects
|
||||
|
||||
if resp.status == 200: # content must always present
|
||||
try:
|
||||
return ServiceCatalog(body, region=self.region,
|
||||
service_type=self.service_type,
|
||||
service_name=self.service_name,
|
||||
service_url=self.service_url,
|
||||
root_key=root_key)
|
||||
except exceptions.AmbiguousEndpoints:
|
||||
print("Found more than one valid endpoint. Use a more "
|
||||
"restrictive filter")
|
||||
raise
|
||||
except KeyError:
|
||||
raise exceptions.AuthorizationFailure()
|
||||
except exceptions.EndpointNotFound:
|
||||
print("Could not find any suitable endpoint. Correct region?")
|
||||
raise
|
||||
|
||||
elif resp.status == 305:
|
||||
return resp['location']
|
||||
else:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
def authenticate(self):
|
||||
raise NotImplementedError("Missing authenticate method.")
|
||||
|
||||
|
||||
class KeyStoneV2Authenticator(Authenticator):
|
||||
def authenticate(self):
|
||||
if self.url is None:
|
||||
raise exceptions.AuthUrlNotGiven()
|
||||
return self._v2_auth(self.url)
|
||||
|
||||
def _v2_auth(self, url):
|
||||
"""Authenticate against a v2.0 auth service."""
|
||||
body = {"auth": {
|
||||
"passwordCredentials": {
|
||||
"username": self.username,
|
||||
"password": self.password}
|
||||
}
|
||||
}
|
||||
|
||||
if self.tenant:
|
||||
body['auth']['tenantName'] = self.tenant
|
||||
|
||||
return self._authenticate(url, body)
|
||||
|
||||
|
||||
class Auth1_1(Authenticator):
|
||||
def authenticate(self):
|
||||
"""Authenticate against a v2.0 auth service."""
|
||||
if self.url is None:
|
||||
raise exceptions.AuthUrlNotGiven()
|
||||
auth_url = self.url
|
||||
body = {
|
||||
"credentials": {
|
||||
"username": self.username,
|
||||
"key": self.password
|
||||
}}
|
||||
return self._authenticate(auth_url, body, root_key='auth')
|
||||
|
||||
|
||||
class FakeAuth(Authenticator):
|
||||
"""Useful for faking auth."""
|
||||
|
||||
def authenticate(self):
|
||||
class FakeCatalog(object):
|
||||
def __init__(self, auth):
|
||||
self.auth = auth
|
||||
|
||||
def get_public_url(self):
|
||||
return "%s/%s" % ('http://localhost:8779/v1.0',
|
||||
self.auth.tenant)
|
||||
|
||||
def get_token(self):
|
||||
return self.auth.tenant
|
||||
|
||||
return FakeCatalog(self)
|
||||
|
||||
|
||||
class ServiceCatalog(object):
|
||||
"""Represents a Keystone Service Catalog which describes a service.
|
||||
|
||||
This class has methods to obtain a valid token as well as a public service
|
||||
url and a management url.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, resource_dict, region=None, service_type=None,
|
||||
service_name=None, service_url=None, root_key='access'):
|
||||
self.catalog = resource_dict
|
||||
self.region = region
|
||||
self.service_type = service_type
|
||||
self.service_name = service_name
|
||||
self.service_url = service_url
|
||||
self.management_url = None
|
||||
self.public_url = None
|
||||
self.root_key = root_key
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
if not self.service_url:
|
||||
self.public_url = self._url_for(attr='region',
|
||||
filter_value=self.region,
|
||||
endpoint_type="publicURL")
|
||||
self.management_url = self._url_for(attr='region',
|
||||
filter_value=self.region,
|
||||
endpoint_type="adminURL")
|
||||
else:
|
||||
self.public_url = self.service_url
|
||||
self.management_url = self.service_url
|
||||
|
||||
def get_token(self):
|
||||
return self.catalog[self.root_key]['token']['id']
|
||||
|
||||
def get_management_url(self):
|
||||
return self.management_url
|
||||
|
||||
def get_public_url(self):
|
||||
return self.public_url
|
||||
|
||||
def _url_for(self, attr=None, filter_value=None,
|
||||
endpoint_type='publicURL'):
|
||||
"""Fetch requested URL.
|
||||
|
||||
Fetch the public URL from the Trove service for a particular
|
||||
endpoint attribute. If none given, return the first.
|
||||
"""
|
||||
matching_endpoints = []
|
||||
if 'endpoints' in self.catalog:
|
||||
# We have a bastardized service catalog. Treat it special. :/
|
||||
for endpoint in self.catalog['endpoints']:
|
||||
if not filter_value or endpoint[attr] == filter_value:
|
||||
matching_endpoints.append(endpoint)
|
||||
if not matching_endpoints:
|
||||
raise exceptions.EndpointNotFound()
|
||||
|
||||
# We don't always get a service catalog back ...
|
||||
if 'serviceCatalog' not in self.catalog[self.root_key]:
|
||||
raise exceptions.EndpointNotFound()
|
||||
|
||||
# Full catalog ...
|
||||
catalog = self.catalog[self.root_key]['serviceCatalog']
|
||||
|
||||
for service in catalog:
|
||||
if service.get("type") != self.service_type:
|
||||
continue
|
||||
|
||||
if (self.service_name and self.service_type == 'database' and
|
||||
service.get('name') != self.service_name):
|
||||
continue
|
||||
|
||||
endpoints = service['endpoints']
|
||||
for endpoint in endpoints:
|
||||
if not filter_value or endpoint.get(attr) == filter_value:
|
||||
endpoint["serviceName"] = service.get("name")
|
||||
matching_endpoints.append(endpoint)
|
||||
|
||||
if not matching_endpoints:
|
||||
raise exceptions.EndpointNotFound()
|
||||
elif len(matching_endpoints) > 1:
|
||||
raise exceptions.AmbiguousEndpoints(endpoints=matching_endpoints)
|
||||
else:
|
||||
return matching_endpoints[0].get(endpoint_type, None)
|
||||
@@ -1,296 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from oslo_utils import reflection
|
||||
from oslo_utils import strutils
|
||||
|
||||
from troveclient.compat import exceptions
|
||||
from troveclient.compat import utils
|
||||
|
||||
|
||||
# Python 2.4 compat
|
||||
try:
|
||||
all
|
||||
except NameError:
|
||||
def all(iterable):
|
||||
return True not in (not x for x in iterable)
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""Retrives an id from object or integer.
|
||||
|
||||
Abstracts the common pattern of allowing both an object or an object's
|
||||
ID as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(utils.HookableMixin):
|
||||
"""Manager defining CRUD operations for API.
|
||||
|
||||
Managers interact with a particular type of API (servers, flavors,
|
||||
images, etc.) and provide CRUD operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp = None
|
||||
if body:
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
else:
|
||||
resp, body = self.api.client.get(url)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key]
|
||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
||||
# unlike other services which just return the list...
|
||||
if isinstance(data, dict):
|
||||
try:
|
||||
data = data['values']
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
with self.completion_cache('human_id', obj_class, mode="w"):
|
||||
with self.completion_cache('uuid', obj_class, mode="w"):
|
||||
return [obj_class(self, res, loaded=True)
|
||||
for res in data if res]
|
||||
|
||||
@contextlib.contextmanager
|
||||
def completion_cache(self, cache_type, obj_class, mode):
|
||||
"""Bash-completion cache.
|
||||
|
||||
The completion cache store items that can be used for bash
|
||||
autocompletion, like UUIDs or human-friendly IDs.
|
||||
|
||||
A resource listing will clear and repopulate the cache.
|
||||
|
||||
A resource create will append to the cache.
|
||||
|
||||
Delete is not handled because listings are assumed to be performed
|
||||
often enough to keep the cache reasonably up-to-date.
|
||||
"""
|
||||
base_dir = utils.env('REDDWARFCLIENT_ID_CACHE_DIR',
|
||||
default="~/.troveclient")
|
||||
|
||||
# NOTE(sirp): Keep separate UUID caches for each username + endpoint
|
||||
# pair
|
||||
username = utils.env('OS_USERNAME', 'USERNAME')
|
||||
url = utils.env('OS_URL', 'SERVICE_URL')
|
||||
uniqifier = hashlib.md5(username + url).hexdigest()
|
||||
|
||||
cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier))
|
||||
|
||||
try:
|
||||
os.makedirs(cache_dir, 0o755)
|
||||
except OSError:
|
||||
# NOTE(kiall): This is typically either permission denied while
|
||||
# attempting to create the directory, or the directory
|
||||
# already exists. Either way, don't fail.
|
||||
pass
|
||||
|
||||
resource = obj_class.__name__.lower()
|
||||
filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-'))
|
||||
path = os.path.join(cache_dir, filename)
|
||||
|
||||
cache_attr = "_%s_cache" % cache_type
|
||||
|
||||
try:
|
||||
setattr(self, cache_attr, open(path, mode))
|
||||
except IOError:
|
||||
# NOTE(kiall): This is typically a permission denied while
|
||||
# attempting to write the cache file.
|
||||
pass
|
||||
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
cache = getattr(self, cache_attr, None)
|
||||
if cache:
|
||||
cache.close()
|
||||
delattr(self, cache_attr)
|
||||
|
||||
def write_to_completion_cache(self, cache_type, val):
|
||||
cache = getattr(self, "_%s_cache" % cache_type, None)
|
||||
if cache:
|
||||
cache.write("%s\n" % val)
|
||||
|
||||
def _get(self, url, response_key=None):
|
||||
resp, body = self.api.client.get(url)
|
||||
if response_key:
|
||||
return self.resource_class(self, body[response_key], loaded=True)
|
||||
else:
|
||||
return self.resource_class(self, body, loaded=True)
|
||||
|
||||
def _create(self, url, body, response_key, return_raw=False, **kwargs):
|
||||
self.run_hooks('modify_body_for_create', body, **kwargs)
|
||||
resp, body = self.api.client.post(url, body=body)
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
|
||||
with self.completion_cache('human_id', self.resource_class, mode="a"):
|
||||
with self.completion_cache('uuid', self.resource_class, mode="a"):
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _delete(self, url):
|
||||
resp, body = self.api.client.delete(url)
|
||||
|
||||
def _update(self, url, body, **kwargs):
|
||||
self.run_hooks('modify_body_for_update', body, **kwargs)
|
||||
resp, body = self.api.client.put(url, body=body)
|
||||
return body
|
||||
|
||||
|
||||
class ManagerWithFind(Manager):
|
||||
"""Like a `Manager`, but with additional `find()`/`findall()` methods."""
|
||||
|
||||
def find(self, **kwargs):
|
||||
"""Find a single item with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
matches = self.findall(**kwargs)
|
||||
num_matches = len(matches)
|
||||
if num_matches == 0:
|
||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
||||
raise exceptions.NotFound(404, msg)
|
||||
elif num_matches > 1:
|
||||
raise exceptions.NoUniqueMatch
|
||||
else:
|
||||
return matches[0]
|
||||
|
||||
def findall(self, **kwargs):
|
||||
"""Find all items with attributes matching ``**kwargs``.
|
||||
|
||||
This isn't very efficient: it loads the entire list then filters on
|
||||
the Python side.
|
||||
"""
|
||||
found = []
|
||||
searches = kwargs.items()
|
||||
|
||||
for obj in self.list():
|
||||
try:
|
||||
if all(getattr(obj, attr) == value
|
||||
for (attr, value) in searches):
|
||||
found.append(obj)
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
return found
|
||||
|
||||
def list(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""A resource represents a particular instance of an object like server.
|
||||
|
||||
This is pretty much just a bag for attributes.
|
||||
:param manager: Manager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
HUMAN_ID = False
|
||||
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
# NOTE(sirp): ensure `id` is already present because if it isn't we'll
|
||||
# enter an infinite loop of __getattr__ -> get -> __init__ ->
|
||||
# __getattr__ -> ...
|
||||
if 'id' in self.__dict__ and len(str(self.id)) == 36:
|
||||
self.manager.write_to_completion_cache('uuid', self.id)
|
||||
|
||||
human_id = self.human_id
|
||||
if human_id:
|
||||
self.manager.write_to_completion_cache('human_id', human_id)
|
||||
|
||||
@property
|
||||
def human_id(self):
|
||||
"""Provides a pretty ID which can be used for bash completion."""
|
||||
if 'name' in self.__dict__ and self.HUMAN_ID:
|
||||
return strutils.to_slug(self.name)
|
||||
return None
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
try:
|
||||
setattr(self, k, v)
|
||||
except AttributeError:
|
||||
# In this case we already defined the attribute on the class
|
||||
pass
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys()
|
||||
if k[0] != '_' and k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
self_cls_name = reflection.get_class_name(self,
|
||||
fully_qualified=False)
|
||||
return "<%s %s>" % (self_cls_name, info)
|
||||
|
||||
def get(self):
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self.set_loaded(True)
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
||||
@@ -1,510 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Trove Command line tool
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# If ../trove/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'troveclient',
|
||||
'__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from troveclient.compat import common
|
||||
|
||||
|
||||
class InstanceCommands(common.AuthedCommandsBase):
|
||||
"""Commands to perform various instance operations and actions."""
|
||||
|
||||
params = [
|
||||
'flavor',
|
||||
'id',
|
||||
'limit',
|
||||
'marker',
|
||||
'name',
|
||||
'size',
|
||||
'backup',
|
||||
'availability_zone',
|
||||
'configuration_id',
|
||||
]
|
||||
|
||||
def _get_configuration_ref(self):
|
||||
configuration_ref = None
|
||||
if self.configuration_id is not None:
|
||||
if self.configuration_id == "":
|
||||
configuration_ref = self.configuration_id
|
||||
else:
|
||||
configuration_ref = "/".join(
|
||||
[
|
||||
self.dbaas.client.service_url,
|
||||
self.configuration_id,
|
||||
]
|
||||
)
|
||||
return configuration_ref
|
||||
|
||||
def create(self):
|
||||
"""Create a new instance."""
|
||||
self._require('name', 'flavor')
|
||||
volume = None
|
||||
if self.size:
|
||||
volume = {"size": self.size}
|
||||
restorePoint = None
|
||||
if self.backup:
|
||||
restorePoint = {"backupRef": self.backup}
|
||||
self._pretty_print(self.dbaas.instances.create, self.name,
|
||||
self.flavor, volume, restorePoint=restorePoint,
|
||||
availability_zone=self.availability_zone,
|
||||
configuration=self._get_configuration_ref())
|
||||
|
||||
# TODO(pdmars): is this actually what this should be named?
|
||||
def modify(self):
|
||||
"""Modify an instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.instances.modify, self.id,
|
||||
configuration=self._get_configuration_ref())
|
||||
|
||||
def delete(self):
|
||||
"""Delete the specified instance."""
|
||||
self._require('id')
|
||||
print(self.dbaas.instances.delete(self.id))
|
||||
|
||||
def get(self):
|
||||
"""Get details for the specified instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.instances.get, self.id)
|
||||
|
||||
def backups(self):
|
||||
"""Get a list of backups for the specified instance."""
|
||||
self._require('id')
|
||||
self._pretty_list(self.dbaas.instances.backups, self.id)
|
||||
|
||||
def list(self):
|
||||
"""List all instances for account."""
|
||||
# limit and marker are not required.
|
||||
limit = self.limit or None
|
||||
if limit:
|
||||
limit = int(limit, 10)
|
||||
self._pretty_paged(self.dbaas.instances.list)
|
||||
|
||||
def resize_volume(self):
|
||||
"""Resize an instance volume."""
|
||||
self._require('id', 'size')
|
||||
self._pretty_print(self.dbaas.instances.resize_volume, self.id,
|
||||
self.size)
|
||||
|
||||
def resize_instance(self):
|
||||
"""Resize an instance flavor"""
|
||||
self._require('id', 'flavor')
|
||||
self._pretty_print(self.dbaas.instances.resize_instance, self.id,
|
||||
self.flavor)
|
||||
|
||||
def restart(self):
|
||||
"""Restart the database."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.instances.restart, self.id)
|
||||
|
||||
def configuration(self):
|
||||
"""Get configuration for the specified instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.instances.configuration, self.id)
|
||||
|
||||
|
||||
class FlavorsCommands(common.AuthedCommandsBase):
|
||||
"""Command for listing Flavors."""
|
||||
|
||||
params = []
|
||||
|
||||
def list(self):
|
||||
"""List the available flavors."""
|
||||
self._pretty_list(self.dbaas.flavors.list)
|
||||
|
||||
|
||||
class DatabaseCommands(common.AuthedCommandsBase):
|
||||
"""Database CRUD operations on an instance."""
|
||||
|
||||
params = [
|
||||
'name',
|
||||
'id',
|
||||
'limit',
|
||||
'marker',
|
||||
]
|
||||
|
||||
def create(self):
|
||||
"""Create a database."""
|
||||
self._require('id', 'name')
|
||||
databases = [{'name': self.name}]
|
||||
print(self.dbaas.databases.create(self.id, databases))
|
||||
|
||||
def delete(self):
|
||||
"""Delete a database."""
|
||||
self._require('id', 'name')
|
||||
print(self.dbaas.databases.delete(self.id, self.name))
|
||||
|
||||
def list(self):
|
||||
"""List the databases."""
|
||||
self._require('id')
|
||||
self._pretty_paged(self.dbaas.databases.list, self.id)
|
||||
|
||||
|
||||
class UserCommands(common.AuthedCommandsBase):
|
||||
"""User CRUD operations on an instance."""
|
||||
params = [
|
||||
'id',
|
||||
'database',
|
||||
'databases',
|
||||
'hostname',
|
||||
'name',
|
||||
'password',
|
||||
'new_name',
|
||||
'new_host',
|
||||
'new_password',
|
||||
]
|
||||
|
||||
def create(self):
|
||||
"""Create a user in instance, with access to one or more databases."""
|
||||
self._require('id', 'name', 'password', 'databases')
|
||||
self._make_list('databases')
|
||||
databases = [{'name': dbname} for dbname in self.databases]
|
||||
users = [{'name': self.name, 'password': self.password,
|
||||
'databases': databases}]
|
||||
if self.hostname:
|
||||
users[0]['host'] = self.hostname
|
||||
self.dbaas.users.create(self.id, users)
|
||||
|
||||
def delete(self):
|
||||
"""Delete the specified user"""
|
||||
self._require('id', 'name')
|
||||
self.dbaas.users.delete(self.id, self.name, self.hostname)
|
||||
|
||||
def get(self):
|
||||
"""Get a single user."""
|
||||
self._require('id', 'name')
|
||||
self._pretty_print(self.dbaas.users.get, self.id,
|
||||
self.name, self.hostname)
|
||||
|
||||
def update_attributes(self):
|
||||
"""Update attributes of a single user."""
|
||||
self._require('id', 'name')
|
||||
self._require_at_least_one_of('new_name', 'new_host', 'new_password')
|
||||
user_new = {}
|
||||
if self.new_name:
|
||||
user_new['name'] = self.new_name
|
||||
if self.new_host:
|
||||
user_new['host'] = self.new_host
|
||||
if self.new_password:
|
||||
user_new['password'] = self.new_password
|
||||
self.dbaas.users.update_attributes(self.id, self.name, user_new,
|
||||
self.hostname)
|
||||
|
||||
def list(self):
|
||||
"""List all the users for an instance."""
|
||||
self._require('id')
|
||||
self._pretty_paged(self.dbaas.users.list, self.id)
|
||||
|
||||
def access(self):
|
||||
"""Show all databases the user has access to."""
|
||||
self._require('id', 'name')
|
||||
self._pretty_list(self.dbaas.users.list_access, self.id,
|
||||
self.name, self.hostname)
|
||||
|
||||
def grant(self):
|
||||
"""Allow an existing user permissions to access one or more
|
||||
databases.
|
||||
"""
|
||||
self._require('id', 'name', 'databases')
|
||||
self._make_list('databases')
|
||||
self.dbaas.users.grant(self.id, self.name, self.databases,
|
||||
self.hostname)
|
||||
|
||||
def revoke(self):
|
||||
"""Revoke from an existing user access permissions to a database."""
|
||||
self._require('id', 'name', 'database')
|
||||
self.dbaas.users.revoke(self.id, self.name, self.database,
|
||||
self.hostname)
|
||||
|
||||
def change_password(self):
|
||||
"""Change the password of a single user."""
|
||||
self._require('id', 'name', 'password')
|
||||
users = [{'name': self.name,
|
||||
'host': self.hostname,
|
||||
'password': self.password}]
|
||||
self.dbaas.users.change_passwords(self.id, users)
|
||||
|
||||
|
||||
class RootCommands(common.AuthedCommandsBase):
|
||||
"""Root user related operations on an instance."""
|
||||
|
||||
params = [
|
||||
'id',
|
||||
]
|
||||
|
||||
def create(self):
|
||||
"""Enable the instance's root user."""
|
||||
self._require('id')
|
||||
try:
|
||||
user, password = self.dbaas.root.create(self.id)
|
||||
print("User:\t\t%s\nPassword:\t%s" % (user, password))
|
||||
except Exception:
|
||||
print(sys.exc_info()[1])
|
||||
|
||||
def delete(self):
|
||||
"""Disable the instance's root user."""
|
||||
self._require('id')
|
||||
print(self.dbaas.root.delete(self.id))
|
||||
|
||||
def enabled(self):
|
||||
"""Check the instance for root access."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.root.is_root_enabled, self.id)
|
||||
|
||||
|
||||
class VersionCommands(common.AuthedCommandsBase):
|
||||
"""List available versions."""
|
||||
|
||||
params = [
|
||||
'url',
|
||||
]
|
||||
|
||||
def list(self):
|
||||
"""List all the supported versions."""
|
||||
self._require('url')
|
||||
self._pretty_list(self.dbaas.versions.index, self.url)
|
||||
|
||||
|
||||
class LimitsCommands(common.AuthedCommandsBase):
|
||||
"""Show the rate limits and absolute limits."""
|
||||
|
||||
def list(self):
|
||||
"""List the rate limits and absolute limits."""
|
||||
self._pretty_list(self.dbaas.limits.list)
|
||||
|
||||
|
||||
class BackupsCommands(common.AuthedCommandsBase):
|
||||
"""Command to manage and show backups."""
|
||||
params = ['name', 'instance', 'description']
|
||||
|
||||
def get(self):
|
||||
"""Get details for the specified backup."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.backups.get, self.id)
|
||||
|
||||
def list(self):
|
||||
"""List backups."""
|
||||
self._pretty_list(self.dbaas.backups.list)
|
||||
|
||||
def create(self):
|
||||
"""Create a new backup."""
|
||||
self._require('name', 'instance')
|
||||
self._pretty_print(self.dbaas.backups.create, self.name,
|
||||
self.instance, self.description)
|
||||
|
||||
def delete(self):
|
||||
"""Delete a backup."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.backups.delete, self.id)
|
||||
|
||||
|
||||
class DatastoreConfigurationParameters(common.AuthedCommandsBase):
|
||||
"""Command to show configuration parameters for a datastore."""
|
||||
params = ['datastore', 'parameter']
|
||||
|
||||
def parameters(self):
|
||||
"""List parameters that can be set."""
|
||||
self._require('datastore')
|
||||
self._pretty_print(self.dbaas.configuration_parameters.parameters,
|
||||
self.datastore)
|
||||
|
||||
def get_parameter(self):
|
||||
"""List parameters that can be set."""
|
||||
self._require('datastore', 'parameter')
|
||||
self._pretty_print(self.dbaas.configuration_parameters.get_parameter,
|
||||
self.datastore, self.parameter)
|
||||
|
||||
|
||||
class ConfigurationsCommands(common.AuthedCommandsBase):
|
||||
"""Command to manage and show configurations."""
|
||||
params = ['name', 'instances', 'values', 'description', 'parameter']
|
||||
|
||||
def get(self):
|
||||
"""Get details for the specified configuration."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.configurations.get, self.id)
|
||||
|
||||
def list_instances(self):
|
||||
"""Get details for the specified configuration."""
|
||||
self._require('id')
|
||||
self._pretty_list(self.dbaas.configurations.instances, self.id)
|
||||
|
||||
def list(self):
|
||||
"""List configurations."""
|
||||
self._pretty_list(self.dbaas.configurations.list)
|
||||
|
||||
def create(self):
|
||||
"""Create a new configuration."""
|
||||
self._require('name', 'values')
|
||||
self._pretty_print(self.dbaas.configurations.create, self.name,
|
||||
self.values, self.description)
|
||||
|
||||
def update(self):
|
||||
"""Update an existing configuration."""
|
||||
self._require('id', 'values')
|
||||
self._pretty_print(self.dbaas.configurations.update, self.id,
|
||||
self.values, self.name, self.description)
|
||||
|
||||
def edit(self):
|
||||
"""Edit an existing configuration values."""
|
||||
self._require('id', 'values')
|
||||
self._pretty_print(self.dbaas.configurations.edit, self.id,
|
||||
self.values)
|
||||
|
||||
def delete(self):
|
||||
"""Delete a configuration."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.configurations.delete, self.id)
|
||||
|
||||
|
||||
class SecurityGroupCommands(common.AuthedCommandsBase):
|
||||
"""Commands to list and show Security Groups For an Instance and
|
||||
create and delete security group rules for them.
|
||||
"""
|
||||
params = [
|
||||
'id',
|
||||
'secgroup_id',
|
||||
'protocol',
|
||||
'from_port',
|
||||
'to_port',
|
||||
'cidr'
|
||||
]
|
||||
|
||||
def get(self):
|
||||
"""Get a security group associated with an instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.security_groups.get, self.id)
|
||||
|
||||
def list(self):
|
||||
"""List all the Security Groups and the rules."""
|
||||
self._pretty_paged(self.dbaas.security_groups.list)
|
||||
|
||||
def add_rule(self):
|
||||
"""Add a security group rule."""
|
||||
self._require('secgroup_id', 'protocol',
|
||||
'from_port', 'to_port', 'cidr')
|
||||
self.dbaas.security_group_rules.create(self.secgroup_id, self.protocol,
|
||||
self.from_port, self.to_port,
|
||||
self.cidr)
|
||||
|
||||
def delete_rule(self):
|
||||
"""Delete a security group rule."""
|
||||
self._require('id')
|
||||
self.dbaas.security_group_rules.delete(self.id)
|
||||
|
||||
|
||||
class MetadataCommands(common.AuthedCommandsBase):
|
||||
"""Commands to create/update/replace/delete/show metadata for an instance
|
||||
"""
|
||||
params = [
|
||||
'instance_id',
|
||||
'metadata'
|
||||
]
|
||||
|
||||
def show(self):
|
||||
"""Show instance metadata."""
|
||||
self._require('instance_id')
|
||||
self._pretty_print(self.dbaas.metadata.show(self.instance_id))
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
'auth': common.Auth,
|
||||
'instance': InstanceCommands,
|
||||
'flavor': FlavorsCommands,
|
||||
'database': DatabaseCommands,
|
||||
'limit': LimitsCommands,
|
||||
'backup': BackupsCommands,
|
||||
'configuration': ConfigurationsCommands,
|
||||
'user': UserCommands,
|
||||
'root': RootCommands,
|
||||
'version': VersionCommands,
|
||||
'secgroup': SecurityGroupCommands,
|
||||
'metadata': MetadataCommands,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
# Parse arguments
|
||||
load_file = True
|
||||
for index, arg in enumerate(sys.argv):
|
||||
if (arg == "auth" and len(sys.argv) > (index + 1)
|
||||
and sys.argv[index + 1] == "login"):
|
||||
load_file = False
|
||||
|
||||
oparser = common.CliOptions.create_optparser(load_file)
|
||||
for k, v in COMMANDS.items():
|
||||
v._prepare_parser(oparser)
|
||||
(options, args) = oparser.parse_args()
|
||||
|
||||
if not args:
|
||||
common.print_commands(COMMANDS)
|
||||
|
||||
if options.verbose:
|
||||
os.environ['RDC_PP'] = "True"
|
||||
os.environ['REDDWARFCLIENT_DEBUG'] = "True"
|
||||
|
||||
# Pop the command and check if it's in the known commands
|
||||
cmd = args.pop(0)
|
||||
if cmd in COMMANDS:
|
||||
fn = COMMANDS.get(cmd)
|
||||
command_object = None
|
||||
try:
|
||||
command_object = fn(oparser)
|
||||
except Exception as ex:
|
||||
if options.debug:
|
||||
raise
|
||||
print(ex)
|
||||
|
||||
# Get a list of supported actions for the command
|
||||
actions = common.methods_of(command_object)
|
||||
|
||||
if len(args) < 1:
|
||||
common.print_actions(cmd, actions)
|
||||
|
||||
# Check for a valid action and perform that action
|
||||
action = args.pop(0)
|
||||
if action in actions:
|
||||
if not options.debug:
|
||||
try:
|
||||
getattr(command_object, action)()
|
||||
except Exception as ex:
|
||||
if options.debug:
|
||||
raise
|
||||
print(ex)
|
||||
else:
|
||||
getattr(command_object, action)()
|
||||
else:
|
||||
common.print_actions(cmd, actions)
|
||||
else:
|
||||
common.print_commands(COMMANDS)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,386 +0,0 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import httplib2
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
|
||||
from troveclient.compat import auth
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
RDC_PP = os.environ.get("RDC_PP", "False") == "True"
|
||||
|
||||
|
||||
expected_errors = (400, 401, 403, 404, 408, 409, 413, 422, 500, 501)
|
||||
|
||||
|
||||
def log_to_streamhandler(stream=None):
|
||||
stream = stream or sys.stderr
|
||||
ch = logging.StreamHandler(stream)
|
||||
LOG.setLevel(logging.DEBUG)
|
||||
LOG.addHandler(ch)
|
||||
|
||||
|
||||
if 'REDDWARFCLIENT_DEBUG' in os.environ and os.environ['REDDWARFCLIENT_DEBUG']:
|
||||
log_to_streamhandler()
|
||||
|
||||
|
||||
class TroveHTTPClient(httplib2.Http):
|
||||
|
||||
USER_AGENT = 'python-troveclient'
|
||||
|
||||
def __init__(self, user, password, tenant, auth_url, service_name,
|
||||
service_url=None,
|
||||
auth_strategy=None, insecure=False,
|
||||
timeout=None, proxy_tenant_id=None,
|
||||
proxy_token=None, region_name=None,
|
||||
endpoint_type='publicURL', service_type=None,
|
||||
timings=False):
|
||||
|
||||
super(TroveHTTPClient, self).__init__(timeout=timeout)
|
||||
|
||||
self.username = user
|
||||
self.password = password
|
||||
self.tenant = tenant
|
||||
if auth_url:
|
||||
self.auth_url = auth_url.rstrip('/')
|
||||
else:
|
||||
self.auth_url = None
|
||||
self.region_name = region_name
|
||||
self.endpoint_type = endpoint_type
|
||||
self.service_url = service_url
|
||||
self.service_type = service_type
|
||||
self.service_name = service_name
|
||||
self.timings = timings
|
||||
|
||||
self.times = [] # [("item", starttime, endtime), ...]
|
||||
|
||||
self.auth_token = None
|
||||
self.proxy_token = proxy_token
|
||||
self.proxy_tenant_id = proxy_tenant_id
|
||||
|
||||
# httplib2 overrides
|
||||
self.force_exception_to_status_code = True
|
||||
self.disable_ssl_certificate_validation = insecure
|
||||
|
||||
auth_cls = auth.get_authenticator_cls(auth_strategy)
|
||||
|
||||
self.authenticator = auth_cls(self, auth_strategy,
|
||||
self.auth_url, self.username,
|
||||
self.password, self.tenant,
|
||||
region=region_name,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
service_url=service_url)
|
||||
|
||||
def get_timings(self):
|
||||
return self.times
|
||||
|
||||
def http_log(self, args, kwargs, resp, body):
|
||||
if not RDC_PP:
|
||||
self.simple_log(args, kwargs, resp, body)
|
||||
else:
|
||||
self.pretty_log(args, kwargs, resp, body)
|
||||
|
||||
def simple_log(self, args, kwargs, resp, body):
|
||||
if not LOG.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
LOG.debug("REQ: %s\n", "".join(string_parts))
|
||||
if 'body' in kwargs:
|
||||
LOG.debug("REQ BODY: %s\n", kwargs['body'])
|
||||
LOG.debug("RESP:%s %s\n", resp, body)
|
||||
|
||||
def pretty_log(self, args, kwargs, resp, body):
|
||||
if not LOG.isEnabledFor(logging.DEBUG):
|
||||
return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
curl_cmd = "".join(string_parts)
|
||||
LOG.debug("REQUEST:")
|
||||
if 'body' in kwargs:
|
||||
LOG.debug("%s -d '%s'", curl_cmd, kwargs['body'])
|
||||
try:
|
||||
req_body = json.dumps(json.loads(kwargs['body']),
|
||||
sort_keys=True, indent=4)
|
||||
except Exception:
|
||||
req_body = kwargs['body']
|
||||
LOG.debug("BODY: %s\n", req_body)
|
||||
else:
|
||||
LOG.debug(curl_cmd)
|
||||
|
||||
try:
|
||||
resp_body = json.dumps(json.loads(body), sort_keys=True, indent=4)
|
||||
except Exception:
|
||||
resp_body = body
|
||||
LOG.debug("RESPONSE HEADERS: %s", resp)
|
||||
LOG.debug("RESPONSE BODY : %s", resp_body)
|
||||
|
||||
def request(self, *args, **kwargs):
|
||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
||||
kwargs['headers']['User-Agent'] = self.USER_AGENT
|
||||
self.morph_request(kwargs)
|
||||
|
||||
resp, body = super(TroveHTTPClient, self).request(*args, **kwargs)
|
||||
# compat between requests and httplib2
|
||||
resp.status_code = resp.status
|
||||
|
||||
# Save this in case anyone wants it.
|
||||
self.last_response = (resp, body)
|
||||
self.http_log(args, kwargs, resp, body)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = self.morph_response_body(body)
|
||||
except exceptions.ResponseFormatError:
|
||||
# Acceptable only if the response status is an error code.
|
||||
# Otherwise its the API or client misbehaving.
|
||||
self.raise_error_from_status(resp, None)
|
||||
raise # Not accepted!
|
||||
else:
|
||||
body = None
|
||||
|
||||
if resp.status in expected_errors:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def raise_error_from_status(self, resp, body):
|
||||
if resp.status in expected_errors:
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
def morph_request(self, kwargs):
|
||||
kwargs['headers']['Accept'] = 'application/json'
|
||||
kwargs['headers']['Content-Type'] = 'application/json'
|
||||
if 'body' in kwargs:
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
def morph_response_body(self, body_string):
|
||||
try:
|
||||
return json.loads(body_string)
|
||||
except ValueError:
|
||||
raise exceptions.ResponseFormatError()
|
||||
|
||||
def _time_request(self, url, method, **kwargs):
|
||||
start_time = time.time()
|
||||
resp, body = self.request(url, method, **kwargs)
|
||||
self.times.append(("%s %s" % (method, url),
|
||||
start_time, time.time()))
|
||||
return resp, body
|
||||
|
||||
def _cs_request(self, url, method, **kwargs):
|
||||
def request():
|
||||
kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token
|
||||
if self.tenant:
|
||||
kwargs['headers']['X-Auth-Project-Id'] = self.tenant
|
||||
|
||||
resp, body = self._time_request(self.service_url + url, method,
|
||||
**kwargs)
|
||||
return resp, body
|
||||
|
||||
if not self.auth_token or not self.service_url:
|
||||
self.authenticate()
|
||||
|
||||
# Perform the request once. If we get a 401 back then it
|
||||
# might be because the auth token expired, so try to
|
||||
# re-authenticate and try again. If it still fails, bail.
|
||||
try:
|
||||
return request()
|
||||
except exceptions.Unauthorized:
|
||||
self.authenticate()
|
||||
return request()
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._cs_request(url, 'GET', **kwargs)
|
||||
|
||||
def patch(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PATCH', **kwargs)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._cs_request(url, 'POST', **kwargs)
|
||||
|
||||
def put(self, url, **kwargs):
|
||||
return self._cs_request(url, 'PUT', **kwargs)
|
||||
|
||||
def delete(self, url, **kwargs):
|
||||
return self._cs_request(url, 'DELETE', **kwargs)
|
||||
|
||||
def authenticate(self):
|
||||
"""Auths the client and gets a token. May optionally set a service url.
|
||||
|
||||
The client will get auth errors until the authentication step
|
||||
occurs. Additionally, if a service_url was not explicitly given in
|
||||
the clients __init__ method, one will be obtained from the auth
|
||||
service.
|
||||
|
||||
"""
|
||||
catalog = self.authenticator.authenticate()
|
||||
if self.service_url:
|
||||
possible_service_url = None
|
||||
else:
|
||||
if self.endpoint_type == "publicURL":
|
||||
possible_service_url = catalog.get_public_url()
|
||||
elif self.endpoint_type == "adminURL":
|
||||
possible_service_url = catalog.get_management_url()
|
||||
self.authenticate_with_token(catalog.get_token(), possible_service_url)
|
||||
|
||||
def authenticate_with_token(self, token, service_url=None):
|
||||
self.auth_token = token
|
||||
if not self.service_url:
|
||||
if not service_url:
|
||||
raise exceptions.ServiceUrlNotGiven()
|
||||
else:
|
||||
self.service_url = service_url
|
||||
|
||||
|
||||
class Dbaas(object):
|
||||
"""Top-level object to access the Rackspace Database as a Service API.
|
||||
|
||||
Create an instance with your creds::
|
||||
|
||||
>> red = Dbaas(USERNAME, API_KEY, TENANT, AUTH_URL, SERVICE_NAME, \
|
||||
SERVICE_URL)
|
||||
|
||||
Then call methods on its managers::
|
||||
|
||||
>> red.instances.list()
|
||||
...
|
||||
>> red.flavors.list()
|
||||
...
|
||||
|
||||
&c.
|
||||
"""
|
||||
|
||||
def __init__(self, username, api_key, tenant=None, auth_url=None,
|
||||
service_type='database', service_name=None,
|
||||
service_url=None, insecure=False, auth_strategy='keystone',
|
||||
region_name=None, client_cls=TroveHTTPClient):
|
||||
|
||||
from troveclient.compat import versions
|
||||
from troveclient.v1 import accounts
|
||||
from troveclient.v1 import backups
|
||||
from troveclient.v1 import clusters
|
||||
from troveclient.v1 import configurations
|
||||
from troveclient.v1 import databases
|
||||
from troveclient.v1 import datastores
|
||||
from troveclient.v1 import diagnostics
|
||||
from troveclient.v1 import flavors
|
||||
from troveclient.v1 import hosts
|
||||
from troveclient.v1 import instances
|
||||
from troveclient.v1 import limits
|
||||
from troveclient.v1 import management
|
||||
from troveclient.v1 import metadata
|
||||
from troveclient.v1 import modules
|
||||
from troveclient.v1 import quota
|
||||
from troveclient.v1 import root
|
||||
from troveclient.v1 import security_groups
|
||||
from troveclient.v1 import storage
|
||||
from troveclient.v1 import users
|
||||
|
||||
self.client = client_cls(username, api_key, tenant, auth_url,
|
||||
service_type=service_type,
|
||||
service_name=service_name,
|
||||
service_url=service_url,
|
||||
insecure=insecure,
|
||||
auth_strategy=auth_strategy,
|
||||
region_name=region_name)
|
||||
self.versions = versions.Versions(self)
|
||||
self.databases = databases.Databases(self)
|
||||
self.flavors = flavors.Flavors(self)
|
||||
self.instances = instances.Instances(self)
|
||||
self.limits = limits.Limits(self)
|
||||
self.users = users.Users(self)
|
||||
self.root = root.Root(self)
|
||||
self.hosts = hosts.Hosts(self)
|
||||
self.quota = quota.Quotas(self)
|
||||
self.backups = backups.Backups(self)
|
||||
self.clusters = clusters.Clusters(self)
|
||||
self.security_groups = security_groups.SecurityGroups(self)
|
||||
self.security_group_rules = security_groups.SecurityGroupRules(self)
|
||||
self.datastores = datastores.Datastores(self)
|
||||
self.datastore_versions = datastores.DatastoreVersions(self)
|
||||
self.datastore_version_members = (datastores.
|
||||
DatastoreVersionMembers(self))
|
||||
self.storage = storage.StorageInfo(self)
|
||||
self.management = management.Management(self)
|
||||
self.mgmt_cluster = management.MgmtClusters(self)
|
||||
self.mgmt_flavor = management.MgmtFlavors(self)
|
||||
self.accounts = accounts.Accounts(self)
|
||||
self.diagnostics = diagnostics.DiagnosticsInterrogator(self)
|
||||
self.hwinfo = diagnostics.HwInfoInterrogator(self)
|
||||
self.configurations = configurations.Configurations(self)
|
||||
config_parameters = configurations.ConfigurationParameters(self)
|
||||
self.configuration_parameters = config_parameters
|
||||
self.metadata = metadata.Metadata(self)
|
||||
self.modules = modules.Modules(self)
|
||||
self.mgmt_configs = management.MgmtConfigurationParameters(self)
|
||||
self.mgmt_datastore_versions = management.MgmtDatastoreVersions(self)
|
||||
|
||||
class Mgmt(object):
|
||||
def __init__(self, dbaas):
|
||||
self.instances = dbaas.management
|
||||
self.hosts = dbaas.hosts
|
||||
self.accounts = dbaas.accounts
|
||||
self.storage = dbaas.storage
|
||||
self.datastore_version = dbaas.mgmt_datastore_versions
|
||||
|
||||
self.mgmt = Mgmt(self)
|
||||
|
||||
def set_management_url(self, url):
|
||||
self.client.management_url = url
|
||||
|
||||
def get_timings(self):
|
||||
return self.client.get_timings()
|
||||
|
||||
def authenticate(self):
|
||||
"""Authenticate against the server.
|
||||
|
||||
This is called to perform an authentication to retrieve a token.
|
||||
|
||||
Returns on success; raises :exc:`exceptions.Unauthorized` if the
|
||||
credentials are wrong.
|
||||
"""
|
||||
self.client.authenticate()
|
||||
@@ -1,426 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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 json
|
||||
import optparse
|
||||
import os
|
||||
import pickle
|
||||
import six
|
||||
from six.moves.urllib import parse
|
||||
import sys
|
||||
|
||||
from troveclient.compat import client
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
|
||||
def methods_of(obj):
|
||||
"""Get all callable methods of an object that don't start with underscore
|
||||
returns a list of tuples of the form (method_name, method).
|
||||
"""
|
||||
result = {}
|
||||
for i in dir(obj):
|
||||
if callable(getattr(obj, i)) and not i.startswith('_'):
|
||||
result[i] = getattr(obj, i)
|
||||
return result
|
||||
|
||||
|
||||
def check_for_exceptions(resp, body):
|
||||
if resp.status in (400, 422, 500):
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
|
||||
def print_actions(cmd, actions):
|
||||
"""Print help for the command with list of options and description."""
|
||||
print("Available actions for '%s' cmd:" % cmd)
|
||||
for k, v in six.iteritems(actions):
|
||||
print("\t%-20s%s" % (k, v.__doc__))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def print_commands(commands):
|
||||
"""Print the list of available commands and description."""
|
||||
|
||||
print("Available commands")
|
||||
for k, v in six.iteritems(commands):
|
||||
print("\t%-20s%s" % (k, v.__doc__))
|
||||
sys.exit(2)
|
||||
|
||||
|
||||
def limit_url(url, limit=None, marker=None):
|
||||
if not limit and not marker:
|
||||
return url
|
||||
query = []
|
||||
if marker:
|
||||
query.append("marker=%s" % marker)
|
||||
if limit:
|
||||
query.append("limit=%s" % limit)
|
||||
query = '?' + '&'.join(query)
|
||||
return url + query
|
||||
|
||||
|
||||
def quote_user_host(user, host):
|
||||
quoted = ''
|
||||
if host:
|
||||
quoted = parse.quote("%s@%s" % (user, host))
|
||||
else:
|
||||
quoted = parse.quote("%s" % user)
|
||||
return quoted.replace('.', '%2e')
|
||||
|
||||
|
||||
class CliOptions(object):
|
||||
"""A token object containing the user, apikey and token which
|
||||
is pickleable.
|
||||
"""
|
||||
|
||||
APITOKEN = os.path.expanduser("~/.apitoken")
|
||||
|
||||
DEFAULT_VALUES = {
|
||||
'username': None,
|
||||
'apikey': None,
|
||||
'tenant_id': None,
|
||||
'auth_url': None,
|
||||
'auth_type': 'keystone',
|
||||
'service_type': 'database',
|
||||
'service_name': '',
|
||||
'region': 'RegionOne',
|
||||
'service_url': None,
|
||||
'insecure': False,
|
||||
'verbose': False,
|
||||
'debug': False,
|
||||
'token': None,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for key, value in self.DEFAULT_VALUES.items():
|
||||
setattr(self, key, value)
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
kwargs = copy.deepcopy(cls.DEFAULT_VALUES)
|
||||
return cls(**kwargs)
|
||||
|
||||
@classmethod
|
||||
def load_from_file(cls):
|
||||
try:
|
||||
with open(cls.APITOKEN, 'rb') as token:
|
||||
return pickle.load(token)
|
||||
except IOError:
|
||||
pass # File probably not found.
|
||||
except Exception:
|
||||
print("ERROR: Token file found at %s was corrupt." % cls.APITOKEN)
|
||||
return cls.default()
|
||||
|
||||
@classmethod
|
||||
def save_from_instance_fields(cls, instance):
|
||||
apitoken = cls.default()
|
||||
for key, default_value in cls.DEFAULT_VALUES.items():
|
||||
final_value = getattr(instance, key, default_value)
|
||||
setattr(apitoken, key, final_value)
|
||||
with open(cls.APITOKEN, 'wb') as token:
|
||||
pickle.dump(apitoken, token, protocol=2)
|
||||
|
||||
@classmethod
|
||||
def create_optparser(cls, load_file):
|
||||
oparser = optparse.OptionParser(
|
||||
usage="%prog [options] <cmd> <action> <args>",
|
||||
version='1.0', conflict_handler='resolve')
|
||||
if load_file:
|
||||
file = cls.load_from_file()
|
||||
else:
|
||||
file = cls.default()
|
||||
|
||||
def add_option(*args, **kwargs):
|
||||
if len(args) == 1:
|
||||
name = args[0]
|
||||
else:
|
||||
name = args[1]
|
||||
kwargs['default'] = getattr(file, name, cls.DEFAULT_VALUES[name])
|
||||
oparser.add_option("--%s" % name, **kwargs)
|
||||
|
||||
add_option("verbose", action="store_true",
|
||||
help="Show equivalent curl statement along "
|
||||
"with actual HTTP communication.")
|
||||
add_option("debug", action="store_true",
|
||||
help="Show the stack trace on errors.")
|
||||
add_option("auth_url", help="Auth API endpoint URL with port and "
|
||||
"version. Default: http://localhost:5000/v2.0")
|
||||
add_option("username", help="Login username.")
|
||||
add_option("apikey", help="API key.")
|
||||
add_option("tenant_id",
|
||||
help="Tenant Id associated with the account.")
|
||||
add_option("auth_type",
|
||||
help="Auth type to support different auth environments, "
|
||||
"Supported value are 'keystone'.")
|
||||
add_option("service_type",
|
||||
help="Service type is a name associated for the catalog.")
|
||||
add_option("service_name",
|
||||
help="Service name as provided in the service catalog.")
|
||||
add_option("service_url",
|
||||
help="Service endpoint to use "
|
||||
"if the catalog doesn't have one.")
|
||||
add_option("region", help="Region the service is located in.")
|
||||
add_option("insecure", action="store_true",
|
||||
help="Run in insecure mode for https endpoints.")
|
||||
add_option("token", help="Token from a prior login.")
|
||||
|
||||
oparser.add_option("--json", action="store_false", dest="xml",
|
||||
help="Changes format to JSON.")
|
||||
oparser.add_option("--secure", action="store_false", dest="insecure",
|
||||
help="Run in insecure mode for https endpoints.")
|
||||
oparser.add_option("--terse", action="store_false", dest="verbose",
|
||||
help="Toggles verbose mode off.")
|
||||
oparser.add_option("--hide-debug", action="store_false", dest="debug",
|
||||
help="Toggles debug mode off.")
|
||||
return oparser
|
||||
|
||||
|
||||
class ArgumentRequired(Exception):
|
||||
def __init__(self, param):
|
||||
self.param = param
|
||||
|
||||
def __str__(self):
|
||||
return 'Argument "--%s" required.' % self.param
|
||||
|
||||
|
||||
class ArgumentsRequired(ArgumentRequired):
|
||||
def __init__(self, *params):
|
||||
self.params = params
|
||||
|
||||
def __str__(self):
|
||||
returnstring = 'Specify at least one of these arguments: '
|
||||
for param in self.params:
|
||||
returnstring = returnstring + '"--%s" ' % param
|
||||
return returnstring
|
||||
|
||||
|
||||
class CommandsBase(object):
|
||||
params = []
|
||||
|
||||
def __init__(self, parser):
|
||||
self._parse_options(parser)
|
||||
|
||||
def _get_client(self):
|
||||
"""Creates the all important client object."""
|
||||
try:
|
||||
client_cls = client.TroveHTTPClient
|
||||
if self.verbose:
|
||||
client.log_to_streamhandler(sys.stdout)
|
||||
client.RDC_PP = True
|
||||
return client.Dbaas(self.username, self.apikey, self.tenant_id,
|
||||
auth_url=self.auth_url,
|
||||
auth_strategy=self.auth_type,
|
||||
service_type=self.service_type,
|
||||
service_name=self.service_name,
|
||||
region_name=self.region,
|
||||
service_url=self.service_url,
|
||||
insecure=self.insecure,
|
||||
client_cls=client_cls)
|
||||
except Exception:
|
||||
if self.debug:
|
||||
raise
|
||||
print(sys.exc_info()[1])
|
||||
|
||||
def _safe_exec(self, func, *args, **kwargs):
|
||||
if not self.debug:
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except Exception:
|
||||
print(sys.exc_info()[1])
|
||||
return None
|
||||
else:
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _prepare_parser(cls, parser):
|
||||
for param in cls.params:
|
||||
parser.add_option("--%s" % param)
|
||||
|
||||
def _parse_options(self, parser):
|
||||
opts, args = parser.parse_args()
|
||||
for param in opts.__dict__:
|
||||
value = getattr(opts, param)
|
||||
setattr(self, param, value)
|
||||
|
||||
def _require(self, *params):
|
||||
for param in params:
|
||||
if not hasattr(self, param):
|
||||
raise ArgumentRequired(param)
|
||||
if not getattr(self, param):
|
||||
raise ArgumentRequired(param)
|
||||
|
||||
def _require_at_least_one_of(self, *params):
|
||||
# One or more of params is required to be present.
|
||||
argument_present = False
|
||||
for param in params:
|
||||
if hasattr(self, param):
|
||||
if getattr(self, param):
|
||||
argument_present = True
|
||||
if argument_present is False:
|
||||
raise ArgumentsRequired(*params)
|
||||
|
||||
def _make_list(self, *params):
|
||||
# Convert the listed params to lists.
|
||||
for param in params:
|
||||
raw = getattr(self, param)
|
||||
if isinstance(raw, list):
|
||||
return
|
||||
raw = [item.strip() for item in raw.split(',')]
|
||||
setattr(self, param, raw)
|
||||
|
||||
def _pretty_print(self, func, *args, **kwargs):
|
||||
if self.verbose:
|
||||
self._safe_exec(func, *args, **kwargs)
|
||||
return # Skip this, since the verbose stuff will show up anyway.
|
||||
|
||||
def wrapped_func():
|
||||
result = func(*args, **kwargs)
|
||||
if result:
|
||||
print(json.dumps(result._info, sort_keys=True, indent=4))
|
||||
else:
|
||||
print("OK")
|
||||
|
||||
self._safe_exec(wrapped_func)
|
||||
|
||||
def _dumps(self, item):
|
||||
return json.dumps(item, sort_keys=True, indent=4)
|
||||
|
||||
def _pretty_list(self, func, *args, **kwargs):
|
||||
result = self._safe_exec(func, *args, **kwargs)
|
||||
if self.verbose:
|
||||
return
|
||||
if result and len(result) > 0:
|
||||
for item in result:
|
||||
print(self._dumps(item._info))
|
||||
else:
|
||||
print("OK")
|
||||
|
||||
def _pretty_paged(self, func, *args, **kwargs):
|
||||
try:
|
||||
limit = self.limit
|
||||
if limit:
|
||||
limit = int(limit, 10)
|
||||
result = func(*args, limit=limit, marker=self.marker, **kwargs)
|
||||
if self.verbose:
|
||||
return # Verbose already shows the output, so skip this.
|
||||
if result and len(result) > 0:
|
||||
for item in result:
|
||||
print(self._dumps(item._info))
|
||||
if result.links:
|
||||
print("Links:")
|
||||
for link in result.links:
|
||||
print(self._dumps((link)))
|
||||
else:
|
||||
print("OK")
|
||||
except Exception:
|
||||
if self.debug:
|
||||
raise
|
||||
print(sys.exc_info()[1])
|
||||
|
||||
|
||||
class Auth(CommandsBase):
|
||||
"""Authenticate with your username and api key."""
|
||||
params = [
|
||||
'apikey',
|
||||
'auth_strategy',
|
||||
'auth_type',
|
||||
'auth_url',
|
||||
'options',
|
||||
'region',
|
||||
'service_name',
|
||||
'service_type',
|
||||
'service_url',
|
||||
'tenant_id',
|
||||
'username',
|
||||
]
|
||||
|
||||
def __init__(self, parser):
|
||||
super(Auth, self).__init__(parser)
|
||||
self.dbaas = None
|
||||
|
||||
def login(self):
|
||||
"""Login to retrieve an auth token to use for other api calls."""
|
||||
self._require('username', 'apikey', 'tenant_id', 'auth_url')
|
||||
try:
|
||||
self.dbaas = self._get_client()
|
||||
self.dbaas.authenticate()
|
||||
self.token = self.dbaas.client.auth_token
|
||||
self.service_url = self.dbaas.client.service_url
|
||||
CliOptions.save_from_instance_fields(self)
|
||||
print("Token acquired! Saving to %s..." % CliOptions.APITOKEN)
|
||||
print(" service_url = %s" % self.service_url)
|
||||
print(" token = %s" % self.token)
|
||||
except Exception:
|
||||
if self.debug:
|
||||
raise
|
||||
print(sys.exc_info()[1])
|
||||
|
||||
|
||||
class AuthedCommandsBase(CommandsBase):
|
||||
"""Commands that work only with an authenticated client."""
|
||||
|
||||
def __init__(self, parser):
|
||||
"""Makes sure a token is available somehow and logs in."""
|
||||
super(AuthedCommandsBase, self).__init__(parser)
|
||||
try:
|
||||
self._require('token')
|
||||
except ArgumentRequired:
|
||||
if self.debug:
|
||||
raise
|
||||
print('No token argument supplied. Use the "auth login" command '
|
||||
'to log in and get a token.\n')
|
||||
sys.exit(1)
|
||||
try:
|
||||
self._require('service_url')
|
||||
except ArgumentRequired:
|
||||
if self.debug:
|
||||
raise
|
||||
print('No service_url given.\n')
|
||||
sys.exit(1)
|
||||
self.dbaas = self._get_client()
|
||||
# Actually set the token to avoid a re-auth.
|
||||
self.dbaas.client.auth_token = self.token
|
||||
self.dbaas.client.authenticate_with_token(self.token, self.service_url)
|
||||
|
||||
|
||||
class Paginated(object):
|
||||
"""Pretends to be a list if you iterate over it, but also keeps a
|
||||
next property you can use to get the next page of data.
|
||||
"""
|
||||
|
||||
def __init__(self, items=None, next_marker=None, links=None):
|
||||
self.items = items or []
|
||||
self.next = next_marker
|
||||
self.links = links or []
|
||||
|
||||
def __len__(self):
|
||||
return len(self.items)
|
||||
|
||||
def __iter__(self):
|
||||
return self.items.__iter__()
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.items[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.items[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.items[key]
|
||||
|
||||
def __reversed__(self):
|
||||
return reversed(self.items)
|
||||
|
||||
def __contains__(self, needle):
|
||||
return needle in self.items
|
||||
@@ -1,172 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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 UnsupportedVersion(Exception):
|
||||
"""Indicates that the user is trying to use an unsupported
|
||||
version of the API.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoUniqueMatch(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoTokenLookupException(Exception):
|
||||
"""This form of authentication does not support looking up
|
||||
endpoints from an existing token.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(Exception):
|
||||
"""Could not find Service or Region in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AuthUrlNotGiven(EndpointNotFound):
|
||||
"""The auth url was not given."""
|
||||
pass
|
||||
|
||||
|
||||
class ServiceUrlNotGiven(EndpointNotFound):
|
||||
"""The service url was not given."""
|
||||
pass
|
||||
|
||||
|
||||
class ResponseFormatError(Exception):
|
||||
"""Could not parse the response format."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(Exception):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
self.endpoints = endpoints
|
||||
|
||||
def __str__(self):
|
||||
return "AmbiguousEndpoints: %s" % repr(self.endpoints)
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""The base exception class for all exceptions this library raises."""
|
||||
def __init__(self, code, message=None, details=None, request_id=None):
|
||||
self.code = code
|
||||
self.message = message or self.__class__.message
|
||||
self.details = details
|
||||
self.request_id = request_id
|
||||
|
||||
def __str__(self):
|
||||
formatted_string = "%s (HTTP %s)" % (self.message, self.code)
|
||||
if self.request_id:
|
||||
formatted_string += " (Request-ID: %s)" % self.request_id
|
||||
|
||||
return formatted_string
|
||||
|
||||
|
||||
class BadRequest(ClientException):
|
||||
"""HTTP 400 - Bad request: you sent some malformed data."""
|
||||
http_status = 400
|
||||
message = "Bad request"
|
||||
|
||||
|
||||
class Unauthorized(ClientException):
|
||||
"""HTTP 401 - Unauthorized: bad credentials."""
|
||||
http_status = 401
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class Forbidden(ClientException):
|
||||
"""HTTP 403 - Forbidden: your don't have access to this resource."""
|
||||
http_status = 403
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(ClientException):
|
||||
"""HTTP 404 - Not found."""
|
||||
http_status = 404
|
||||
message = "Not found"
|
||||
|
||||
|
||||
class Conflict(ClientException):
|
||||
"""HTTP 409 - Conflict."""
|
||||
http_status = 409
|
||||
message = "Conflict"
|
||||
|
||||
|
||||
class OverLimit(ClientException):
|
||||
"""HTTP 413
|
||||
- Over limit: you're over the API limits for this time period.
|
||||
"""
|
||||
http_status = 413
|
||||
message = "Over limit"
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HTTPNotImplemented(ClientException):
|
||||
"""HTTP 501
|
||||
- Not Implemented: the server does not support this operation.
|
||||
"""
|
||||
http_status = 501
|
||||
message = "Not Implemented"
|
||||
|
||||
|
||||
class UnprocessableEntity(ClientException):
|
||||
"""HTTP 422 - Unprocessable Entity: The request cannot be processed."""
|
||||
http_status = 422
|
||||
message = "Unprocessable Entity"
|
||||
|
||||
|
||||
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
||||
# so we can do this:
|
||||
# _code_map = dict((c.http_status, c)
|
||||
# for c in ClientException.__subclasses__())
|
||||
#
|
||||
# Instead, we have to hardcode it:
|
||||
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
Forbidden, NotFound, Conflict,
|
||||
OverLimit, HTTPNotImplemented,
|
||||
UnprocessableEntity])
|
||||
|
||||
|
||||
def from_response(response, body):
|
||||
"""Return an instance of an ClientException based on a request's response.
|
||||
|
||||
Usage::
|
||||
|
||||
resp, body = http.request(...)
|
||||
if resp.status != 200:
|
||||
raise exception_from_response(resp, body)
|
||||
"""
|
||||
cls = _code_map.get(response.status, ClientException)
|
||||
if body:
|
||||
message = "n/a"
|
||||
details = "n/a"
|
||||
if hasattr(body, 'keys'):
|
||||
error = body[list(body.keys())[0]]
|
||||
message = error.get('message', None)
|
||||
details = error.get('details', None)
|
||||
return cls(code=response.status, message=message, details=details)
|
||||
else:
|
||||
request_id = response.get('x-compute-request-id')
|
||||
return cls(code=response.status, request_id=request_id)
|
||||
@@ -1,271 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Trove Management Command line tool
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
# If ../trove/__init__.py exists, add ../ to Python search path, so that
|
||||
# it will override what happens to be installed in /usr/(local/)lib/python...
|
||||
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
|
||||
os.pardir,
|
||||
os.pardir))
|
||||
if os.path.exists(os.path.join(possible_topdir, 'troveclient.compat',
|
||||
'__init__.py')):
|
||||
sys.path.insert(0, possible_topdir)
|
||||
|
||||
from troveclient.compat import common
|
||||
|
||||
|
||||
oparser = None
|
||||
|
||||
|
||||
def _pretty_print(info):
|
||||
print(json.dumps(info, sort_keys=True, indent=4))
|
||||
|
||||
|
||||
class HostCommands(common.AuthedCommandsBase):
|
||||
"""Commands to list info on hosts."""
|
||||
|
||||
params = [
|
||||
'name',
|
||||
]
|
||||
|
||||
def update_all(self):
|
||||
"""Update all instances on a host."""
|
||||
self._require('name')
|
||||
self.dbaas.hosts.update_all(self.name)
|
||||
|
||||
def get(self):
|
||||
"""List details for the specified host."""
|
||||
self._require('name')
|
||||
self._pretty_print(self.dbaas.hosts.get, self.name)
|
||||
|
||||
def list(self):
|
||||
"""List all compute hosts."""
|
||||
self._pretty_list(self.dbaas.hosts.index)
|
||||
|
||||
|
||||
class QuotaCommands(common.AuthedCommandsBase):
|
||||
"""List and update quota limits for a tenant."""
|
||||
|
||||
params = ['id',
|
||||
'instances',
|
||||
'volumes',
|
||||
'backups']
|
||||
|
||||
def list(self):
|
||||
"""List all quotas for a tenant."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.quota.show, self.id)
|
||||
|
||||
def update(self):
|
||||
"""Update quota limits for a tenant."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.quota.update, self.id,
|
||||
dict((param, getattr(self, param))
|
||||
for param in self.params if param != 'id'))
|
||||
|
||||
|
||||
class RootCommands(common.AuthedCommandsBase):
|
||||
"""List details about the root info for an instance."""
|
||||
|
||||
params = [
|
||||
'id',
|
||||
]
|
||||
|
||||
def history(self):
|
||||
"""List root history for the instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.management.root_enabled_history, self.id)
|
||||
|
||||
|
||||
class AccountCommands(common.AuthedCommandsBase):
|
||||
"""Commands to list account info."""
|
||||
|
||||
params = [
|
||||
'id',
|
||||
]
|
||||
|
||||
def list(self):
|
||||
"""List all accounts with non-deleted instances."""
|
||||
self._pretty_print(self.dbaas.accounts.index)
|
||||
|
||||
def get(self):
|
||||
"""List details for the account provided."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.accounts.show, self.id)
|
||||
|
||||
|
||||
class InstanceCommands(common.AuthedCommandsBase):
|
||||
"""List details about an instance."""
|
||||
|
||||
params = [
|
||||
'deleted',
|
||||
'id',
|
||||
'limit',
|
||||
'marker',
|
||||
'host',
|
||||
]
|
||||
|
||||
def get(self):
|
||||
"""List details for the instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.management.show, self.id)
|
||||
|
||||
def list(self):
|
||||
"""List all instances for account."""
|
||||
deleted = None
|
||||
if self.deleted is not None:
|
||||
if self.deleted.lower() in ['true']:
|
||||
deleted = True
|
||||
elif self.deleted.lower() in ['false']:
|
||||
deleted = False
|
||||
self._pretty_paged(self.dbaas.management.index, deleted=deleted)
|
||||
|
||||
def hwinfo(self):
|
||||
"""Show hardware information details about an instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.hwinfo.get, self.id)
|
||||
|
||||
def diagnostic(self):
|
||||
"""List diagnostic details about an instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.diagnostics.get, self.id)
|
||||
|
||||
def stop(self):
|
||||
"""Stop MySQL on the given instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.management.stop, self.id)
|
||||
|
||||
def reboot(self):
|
||||
"""Reboot the instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.management.reboot, self.id)
|
||||
|
||||
def migrate(self):
|
||||
"""Migrate the instance."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.management.migrate, self.id, self.host)
|
||||
|
||||
def reset_task_status(self):
|
||||
"""Set the instance's task status to NONE."""
|
||||
self._require('id')
|
||||
self._pretty_print(self.dbaas.management.reset_task_status, self.id)
|
||||
|
||||
|
||||
class StorageCommands(common.AuthedCommandsBase):
|
||||
"""Commands to list devices info."""
|
||||
|
||||
params = []
|
||||
|
||||
def list(self):
|
||||
"""List details for the storage device."""
|
||||
self._pretty_list(self.dbaas.storage.index)
|
||||
|
||||
|
||||
class FlavorsCommands(common.AuthedCommandsBase):
|
||||
"""Commands for managing Flavors."""
|
||||
|
||||
params = [
|
||||
'name',
|
||||
'ram',
|
||||
'disk',
|
||||
'vcpus',
|
||||
'flavor_id',
|
||||
'ephemeral',
|
||||
'swap',
|
||||
'rxtx_factor',
|
||||
'service_type'
|
||||
]
|
||||
|
||||
def create(self):
|
||||
"""Create a new flavor."""
|
||||
self._require('name', 'ram', 'disk', 'vcpus',
|
||||
'flavor_id', 'service_type')
|
||||
self._pretty_print(self.dbaas.mgmt_flavor.create, self.name,
|
||||
self.ram, self.disk, self.vcpus, self.flavor_id,
|
||||
self.ephemeral, self.swap, self.rxtx_factor,
|
||||
self.service_type)
|
||||
|
||||
|
||||
def config_options(oparser):
|
||||
oparser.add_option("-u", "--url", default="http://localhost:5000/v1.1",
|
||||
help="Auth API endpoint URL with port and version. \
|
||||
Default: http://localhost:5000/v1.1")
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
'account': AccountCommands,
|
||||
'host': HostCommands,
|
||||
'instance': InstanceCommands,
|
||||
'root': RootCommands,
|
||||
'storage': StorageCommands,
|
||||
'quota': QuotaCommands,
|
||||
'flavor': FlavorsCommands,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
# Parse arguments
|
||||
oparser = common.CliOptions.create_optparser(True)
|
||||
for k, v in COMMANDS.items():
|
||||
v._prepare_parser(oparser)
|
||||
(options, args) = oparser.parse_args()
|
||||
|
||||
if not args:
|
||||
common.print_commands(COMMANDS)
|
||||
|
||||
# Pop the command and check if it's in the known commands
|
||||
cmd = args.pop(0)
|
||||
if cmd in COMMANDS:
|
||||
fn = COMMANDS.get(cmd)
|
||||
command_object = None
|
||||
try:
|
||||
command_object = fn(oparser)
|
||||
except Exception as ex:
|
||||
if options.debug:
|
||||
raise
|
||||
print(ex)
|
||||
|
||||
# Get a list of supported actions for the command
|
||||
actions = common.methods_of(command_object)
|
||||
|
||||
if len(args) < 1:
|
||||
common.print_actions(cmd, actions)
|
||||
|
||||
# Check for a valid action and perform that action
|
||||
action = args.pop(0)
|
||||
if action in actions:
|
||||
try:
|
||||
getattr(command_object, action)()
|
||||
except Exception as ex:
|
||||
if options.debug:
|
||||
raise
|
||||
print(ex)
|
||||
else:
|
||||
common.print_actions(cmd, actions)
|
||||
else:
|
||||
common.print_commands(COMMANDS)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,382 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2013 Rackspace Hosting
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from troveclient.compat import auth
|
||||
from troveclient.compat import exceptions
|
||||
|
||||
"""
|
||||
Unit tests for the classes and functions in auth.py.
|
||||
"""
|
||||
|
||||
|
||||
def check_url_none(test_case, auth_class):
|
||||
# url is None, it must throw exception
|
||||
authObj = auth_class(url=None, type=auth_class, client=None,
|
||||
username=None, password=None, tenant=None)
|
||||
try:
|
||||
authObj.authenticate()
|
||||
test_case.fail("AuthUrlNotGiven exception expected")
|
||||
except exceptions.AuthUrlNotGiven:
|
||||
pass
|
||||
|
||||
|
||||
class AuthenticatorTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthenticatorTest, self).setUp()
|
||||
self.orig_load = auth.ServiceCatalog._load
|
||||
self.orig__init = auth.ServiceCatalog.__init__
|
||||
|
||||
def tearDown(self):
|
||||
super(AuthenticatorTest, self).tearDown()
|
||||
auth.ServiceCatalog._load = self.orig_load
|
||||
auth.ServiceCatalog.__init__ = self.orig__init
|
||||
|
||||
def test_get_authenticator_cls(self):
|
||||
class_list = (auth.KeyStoneV2Authenticator,
|
||||
auth.Auth1_1,
|
||||
auth.FakeAuth)
|
||||
|
||||
for c in class_list:
|
||||
self.assertEqual(c, auth.get_authenticator_cls(c))
|
||||
|
||||
class_names = {"keystone": auth.KeyStoneV2Authenticator,
|
||||
"auth1.1": auth.Auth1_1,
|
||||
"fake": auth.FakeAuth}
|
||||
|
||||
for cn in class_names.keys():
|
||||
self.assertEqual(class_names[cn], auth.get_authenticator_cls(cn))
|
||||
|
||||
cls_or_name = "_unknown_"
|
||||
self.assertRaises(ValueError, auth.get_authenticator_cls, cls_or_name)
|
||||
|
||||
def test__authenticate(self):
|
||||
authObj = auth.Authenticator(mock.Mock(), auth.KeyStoneV2Authenticator,
|
||||
mock.Mock(), mock.Mock(),
|
||||
mock.Mock(), mock.Mock())
|
||||
# test response code 200
|
||||
resp = mock.Mock()
|
||||
resp.status = 200
|
||||
body = "test_body"
|
||||
|
||||
auth.ServiceCatalog._load = mock.Mock(return_value=1)
|
||||
authObj.client._time_request = mock.Mock(return_value=(resp, body))
|
||||
|
||||
sc = authObj._authenticate(mock.Mock(), mock.Mock())
|
||||
self.assertEqual(body, sc.catalog)
|
||||
|
||||
# test AmbiguousEndpoints exception
|
||||
auth.ServiceCatalog.__init__ = mock.Mock(
|
||||
side_effect=exceptions.AmbiguousEndpoints
|
||||
)
|
||||
self.assertRaises(exceptions.AmbiguousEndpoints,
|
||||
authObj._authenticate, mock.Mock(), mock.Mock())
|
||||
|
||||
# test handling KeyError and raising AuthorizationFailure exception
|
||||
auth.ServiceCatalog.__init__ = mock.Mock(side_effect=KeyError)
|
||||
self.assertRaises(exceptions.AuthorizationFailure,
|
||||
authObj._authenticate, mock.Mock(), mock.Mock())
|
||||
|
||||
# test EndpointNotFound exception
|
||||
mock_obj = mock.Mock(side_effect=exceptions.EndpointNotFound)
|
||||
auth.ServiceCatalog.__init__ = mock_obj
|
||||
self.assertRaises(exceptions.EndpointNotFound,
|
||||
authObj._authenticate, mock.Mock(), mock.Mock())
|
||||
mock_obj.side_effect = None
|
||||
|
||||
# test response code 305
|
||||
resp.__getitem__ = mock.Mock(return_value='loc')
|
||||
resp.status = 305
|
||||
body = "test_body"
|
||||
authObj.client._time_request = mock.Mock(return_value=(resp, body))
|
||||
|
||||
l = authObj._authenticate(mock.Mock(), mock.Mock())
|
||||
self.assertEqual('loc', l)
|
||||
|
||||
# test any response code other than 200 and 305
|
||||
resp.status = 404
|
||||
exceptions.from_response = mock.Mock(side_effect=ValueError)
|
||||
self.assertRaises(ValueError, authObj._authenticate,
|
||||
mock.Mock(), mock.Mock())
|
||||
|
||||
def test_authenticate(self):
|
||||
authObj = auth.Authenticator(mock.Mock(), auth.KeyStoneV2Authenticator,
|
||||
mock.Mock(), mock.Mock(),
|
||||
mock.Mock(), mock.Mock())
|
||||
self.assertRaises(NotImplementedError, authObj.authenticate)
|
||||
|
||||
|
||||
class KeyStoneV2AuthenticatorTest(testtools.TestCase):
|
||||
|
||||
def test_authenticate(self):
|
||||
# url is None
|
||||
check_url_none(self, auth.KeyStoneV2Authenticator)
|
||||
|
||||
# url is not None, so it must not throw exception
|
||||
url = "test_url"
|
||||
cls_type = auth.KeyStoneV2Authenticator
|
||||
authObj = auth.KeyStoneV2Authenticator(url=url, type=cls_type,
|
||||
client=None, username=None,
|
||||
password=None, tenant=None)
|
||||
|
||||
def side_effect_func(url):
|
||||
return url
|
||||
|
||||
mock_obj = mock.Mock()
|
||||
mock_obj.side_effect = side_effect_func
|
||||
authObj._v2_auth = mock_obj
|
||||
r = authObj.authenticate()
|
||||
self.assertEqual(url, r)
|
||||
|
||||
def test__v2_auth(self):
|
||||
username = "trove_user"
|
||||
password = "trove_password"
|
||||
tenant = "tenant"
|
||||
cls_type = auth.KeyStoneV2Authenticator
|
||||
authObj = auth.KeyStoneV2Authenticator(url=None, type=cls_type,
|
||||
client=None,
|
||||
username=username,
|
||||
password=password,
|
||||
tenant=tenant)
|
||||
|
||||
def side_effect_func(url, body):
|
||||
return body
|
||||
mock_obj = mock.Mock()
|
||||
mock_obj.side_effect = side_effect_func
|
||||
authObj._authenticate = mock_obj
|
||||
body = authObj._v2_auth(mock.Mock())
|
||||
self.assertEqual(username,
|
||||
body['auth']['passwordCredentials']['username'])
|
||||
self.assertEqual(password,
|
||||
body['auth']['passwordCredentials']['password'])
|
||||
self.assertEqual(tenant, body['auth']['tenantName'])
|
||||
|
||||
|
||||
class Auth1_1Test(testtools.TestCase):
|
||||
|
||||
def test_authenticate(self):
|
||||
# handle when url is None
|
||||
check_url_none(self, auth.Auth1_1)
|
||||
|
||||
# url is not none
|
||||
username = "trove_user"
|
||||
password = "trove_password"
|
||||
url = "test_url"
|
||||
authObj = auth.Auth1_1(url=url,
|
||||
type=auth.Auth1_1,
|
||||
client=None, username=username,
|
||||
password=password, tenant=None)
|
||||
|
||||
def side_effect_func(auth_url, body, root_key):
|
||||
return auth_url, body, root_key
|
||||
|
||||
mock_obj = mock.Mock()
|
||||
mock_obj.side_effect = side_effect_func
|
||||
authObj._authenticate = mock_obj
|
||||
auth_url, body, root_key = authObj.authenticate()
|
||||
|
||||
self.assertEqual(username, body['credentials']['username'])
|
||||
self.assertEqual(password, body['credentials']['key'])
|
||||
self.assertEqual(auth_url, url)
|
||||
self.assertEqual('auth', root_key)
|
||||
|
||||
|
||||
class FakeAuthTest(testtools.TestCase):
|
||||
|
||||
def test_authenticate(self):
|
||||
tenant = "tenant"
|
||||
authObj = auth.FakeAuth(url=None,
|
||||
type=auth.FakeAuth,
|
||||
client=None, username=None,
|
||||
password=None, tenant=tenant)
|
||||
|
||||
fc = authObj.authenticate()
|
||||
public_url = "%s/%s" % ('http://localhost:8779/v1.0', tenant)
|
||||
self.assertEqual(public_url, fc.get_public_url())
|
||||
self.assertEqual(tenant, fc.get_token())
|
||||
|
||||
|
||||
class ServiceCatalogTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ServiceCatalogTest, self).setUp()
|
||||
self.orig_url_for = auth.ServiceCatalog._url_for
|
||||
self.orig__init__ = auth.ServiceCatalog.__init__
|
||||
auth.ServiceCatalog.__init__ = mock.Mock(return_value=None)
|
||||
self.test_url = "http://localhost:1234/test"
|
||||
|
||||
def tearDown(self):
|
||||
super(ServiceCatalogTest, self).tearDown()
|
||||
auth.ServiceCatalog._url_for = self.orig_url_for
|
||||
auth.ServiceCatalog.__init__ = self.orig__init__
|
||||
|
||||
def test__load(self):
|
||||
url = "random_url"
|
||||
auth.ServiceCatalog._url_for = mock.Mock(return_value=url)
|
||||
|
||||
# when service_url is None
|
||||
scObj = auth.ServiceCatalog()
|
||||
scObj.region = None
|
||||
scObj.service_url = None
|
||||
scObj._load()
|
||||
self.assertEqual(url, scObj.public_url)
|
||||
self.assertEqual(url, scObj.management_url)
|
||||
|
||||
# service url is not None
|
||||
service_url = "service_url"
|
||||
scObj = auth.ServiceCatalog()
|
||||
scObj.region = None
|
||||
scObj.service_url = service_url
|
||||
scObj._load()
|
||||
self.assertEqual(service_url, scObj.public_url)
|
||||
self.assertEqual(service_url, scObj.management_url)
|
||||
|
||||
def test_get_token(self):
|
||||
test_id = "test_id"
|
||||
scObj = auth.ServiceCatalog()
|
||||
scObj.root_key = "root_key"
|
||||
scObj.catalog = dict()
|
||||
scObj.catalog[scObj.root_key] = dict()
|
||||
scObj.catalog[scObj.root_key]['token'] = dict()
|
||||
scObj.catalog[scObj.root_key]['token']['id'] = test_id
|
||||
self.assertEqual(test_id, scObj.get_token())
|
||||
|
||||
def test_get_management_url(self):
|
||||
test_mng_url = "test_management_url"
|
||||
scObj = auth.ServiceCatalog()
|
||||
scObj.management_url = test_mng_url
|
||||
self.assertEqual(test_mng_url, scObj.get_management_url())
|
||||
|
||||
def test_get_public_url(self):
|
||||
test_public_url = "test_public_url"
|
||||
scObj = auth.ServiceCatalog()
|
||||
scObj.public_url = test_public_url
|
||||
self.assertEqual(test_public_url, scObj.get_public_url())
|
||||
|
||||
def test__url_for(self):
|
||||
scObj = auth.ServiceCatalog()
|
||||
|
||||
# case for no endpoint found
|
||||
self.case_no_endpoint_match(scObj)
|
||||
|
||||
# case for empty service catalog
|
||||
self.case_endpoint_with_empty_catalog(scObj)
|
||||
|
||||
# more than one matching endpoints
|
||||
self.case_ambiguous_endpoint(scObj)
|
||||
|
||||
# happy case
|
||||
self.case_unique_endpoint(scObj)
|
||||
|
||||
# testing if-statements in for-loop to iterate services in catalog
|
||||
self.case_iterating_services_in_catalog(scObj)
|
||||
|
||||
def case_no_endpoint_match(self, scObj):
|
||||
# empty endpoint list
|
||||
scObj.catalog = dict()
|
||||
scObj.catalog['endpoints'] = list()
|
||||
self.assertRaises(exceptions.EndpointNotFound, scObj._url_for)
|
||||
|
||||
def side_effect_func_ep(attr):
|
||||
return "test_attr_value"
|
||||
|
||||
# simulating dict
|
||||
endpoint = mock.Mock()
|
||||
mock_obj = mock.Mock()
|
||||
mock_obj.side_effect = side_effect_func_ep
|
||||
endpoint.__getitem__ = mock_obj
|
||||
scObj.catalog['endpoints'].append(endpoint)
|
||||
|
||||
# not-empty list but not matching endpoint
|
||||
filter_value = "not_matching_value"
|
||||
self.assertRaises(exceptions.EndpointNotFound, scObj._url_for,
|
||||
attr="test_attr", filter_value=filter_value)
|
||||
|
||||
filter_value = "test_attr_value" # so that we have an endpoint match
|
||||
scObj.root_key = "access"
|
||||
scObj.catalog[scObj.root_key] = dict()
|
||||
self.assertRaises(exceptions.EndpointNotFound, scObj._url_for,
|
||||
attr="test_attr", filter_value=filter_value)
|
||||
|
||||
def case_endpoint_with_empty_catalog(self, scObj):
|
||||
# First, test with an empty catalog. This should pass since
|
||||
# there is already an endpoint added.
|
||||
scObj.catalog[scObj.root_key]['serviceCatalog'] = list()
|
||||
|
||||
endpoint = scObj.catalog['endpoints'][0]
|
||||
endpoint.get = mock.Mock(return_value=self.test_url)
|
||||
r_url = scObj._url_for(attr="test_attr",
|
||||
filter_value="test_attr_value")
|
||||
self.assertEqual(self.test_url, r_url)
|
||||
|
||||
def case_ambiguous_endpoint(self, scObj):
|
||||
scObj.service_type = "trove"
|
||||
scObj.service_name = "test_service_name"
|
||||
|
||||
def side_effect_func_service(key):
|
||||
if key == "type":
|
||||
return "trove"
|
||||
elif key == "name":
|
||||
return "test_service_name"
|
||||
return None
|
||||
|
||||
mock1 = mock.Mock()
|
||||
mock1.side_effect = side_effect_func_service
|
||||
service1 = mock.Mock()
|
||||
service1.get = mock1
|
||||
|
||||
endpoint2 = {"test_attr": "test_attr_value"}
|
||||
service1.__getitem__ = mock.Mock(return_value=[endpoint2])
|
||||
scObj.catalog[scObj.root_key]['serviceCatalog'] = [service1]
|
||||
self.assertRaises(exceptions.AmbiguousEndpoints, scObj._url_for,
|
||||
attr="test_attr", filter_value="test_attr_value")
|
||||
|
||||
def case_unique_endpoint(self, scObj):
|
||||
# changing the endpoint2 attribute to pass the filter
|
||||
service1 = scObj.catalog[scObj.root_key]['serviceCatalog'][0]
|
||||
endpoint2 = service1[0][0]
|
||||
endpoint2["test_attr"] = "new value not matching filter"
|
||||
r_url = scObj._url_for(attr="test_attr",
|
||||
filter_value="test_attr_value")
|
||||
self.assertEqual(self.test_url, r_url)
|
||||
|
||||
def case_iterating_services_in_catalog(self, scObj):
|
||||
service1 = scObj.catalog[scObj.root_key]['serviceCatalog'][0]
|
||||
|
||||
scObj.catalog = dict()
|
||||
scObj.root_key = "access"
|
||||
scObj.catalog[scObj.root_key] = dict()
|
||||
scObj.service_type = "no_match"
|
||||
|
||||
scObj.catalog[scObj.root_key]['serviceCatalog'] = [service1]
|
||||
self.assertRaises(exceptions.EndpointNotFound, scObj._url_for)
|
||||
|
||||
scObj.service_type = "database"
|
||||
scObj.service_name = "no_match"
|
||||
self.assertRaises(exceptions.EndpointNotFound, scObj._url_for)
|
||||
|
||||
# no endpoints and no 'serviceCatalog' in catalog => raise exception
|
||||
scObj = auth.ServiceCatalog()
|
||||
scObj.catalog = dict()
|
||||
scObj.root_key = "access"
|
||||
scObj.catalog[scObj.root_key] = dict()
|
||||
scObj.catalog[scObj.root_key]['serviceCatalog'] = []
|
||||
self.assertRaises(exceptions.EndpointNotFound, scObj._url_for,
|
||||
attr="test_attr", filter_value="test_attr_value")
|
||||
@@ -1,398 +0,0 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import collections
|
||||
import json
|
||||
import optparse
|
||||
import sys
|
||||
|
||||
import mock
|
||||
import testtools
|
||||
|
||||
from troveclient.compat import common
|
||||
|
||||
"""
|
||||
unit tests for common.py
|
||||
"""
|
||||
|
||||
|
||||
class CommonTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CommonTest, self).setUp()
|
||||
self.orig_sys_exit = sys.exit
|
||||
sys.exit = mock.Mock(return_value=None)
|
||||
|
||||
def tearDown(self):
|
||||
super(CommonTest, self).tearDown()
|
||||
sys.exit = self.orig_sys_exit
|
||||
|
||||
def test_methods_of(self):
|
||||
class DummyClass(object):
|
||||
def dummyMethod(self):
|
||||
print("just for test")
|
||||
|
||||
obj = DummyClass()
|
||||
result = common.methods_of(obj)
|
||||
self.assertEqual(1, len(result))
|
||||
method = result['dummyMethod']
|
||||
self.assertIsNotNone(method)
|
||||
|
||||
def test_check_for_exceptions(self):
|
||||
status = [400, 422, 500]
|
||||
for s in status:
|
||||
resp = mock.Mock()
|
||||
# compat still uses status
|
||||
resp.status = s
|
||||
self.assertRaises(Exception,
|
||||
common.check_for_exceptions, resp, "body")
|
||||
|
||||
# a no-exception case
|
||||
resp = mock.Mock()
|
||||
resp.status_code = 200
|
||||
common.check_for_exceptions(resp, "body")
|
||||
|
||||
def test_print_actions(self):
|
||||
cmd = "test-cmd"
|
||||
actions = {"test": "test action", "help": "help action"}
|
||||
common.print_actions(cmd, actions)
|
||||
pass
|
||||
|
||||
def test_print_commands(self):
|
||||
commands = {"cmd-1": "cmd 1", "cmd-2": "cmd 2"}
|
||||
common.print_commands(commands)
|
||||
pass
|
||||
|
||||
def test_limit_url(self):
|
||||
url = "test-url"
|
||||
limit = None
|
||||
marker = None
|
||||
self.assertEqual(url, common.limit_url(url))
|
||||
|
||||
limit = "test-limit"
|
||||
marker = "test-marker"
|
||||
expected = "test-url?marker=test-marker&limit=test-limit"
|
||||
self.assertEqual(expected,
|
||||
common.limit_url(url, limit=limit, marker=marker))
|
||||
|
||||
|
||||
class CliOptionsTest(testtools.TestCase):
|
||||
|
||||
def check_default_options(self, co):
|
||||
self.assertIsNone(co.username)
|
||||
self.assertIsNone(co.apikey)
|
||||
self.assertIsNone(co.tenant_id)
|
||||
self.assertIsNone(co.auth_url)
|
||||
self.assertEqual('keystone', co.auth_type)
|
||||
self.assertEqual('database', co.service_type)
|
||||
self.assertEqual('RegionOne', co.region)
|
||||
self.assertIsNone(co.service_url)
|
||||
self.assertFalse(co.insecure)
|
||||
self.assertFalse(co.verbose)
|
||||
self.assertFalse(co.debug)
|
||||
self.assertIsNone(co.token)
|
||||
|
||||
def check_option(self, oparser, option_name):
|
||||
option = oparser.get_option("--%s" % option_name)
|
||||
self.assertIsNotNone(option)
|
||||
if option_name in common.CliOptions.DEFAULT_VALUES:
|
||||
self.assertEqual(common.CliOptions.DEFAULT_VALUES[option_name],
|
||||
option.default)
|
||||
|
||||
def test___init__(self):
|
||||
co = common.CliOptions()
|
||||
self.check_default_options(co)
|
||||
|
||||
def test_default(self):
|
||||
co = common.CliOptions.default()
|
||||
self.check_default_options(co)
|
||||
|
||||
def test_load_from_file(self):
|
||||
co = common.CliOptions.load_from_file()
|
||||
self.check_default_options(co)
|
||||
|
||||
def test_create_optparser(self):
|
||||
option_names = ["verbose", "debug", "auth_url", "username", "apikey",
|
||||
"tenant_id", "auth_type", "service_type",
|
||||
"service_name", "service_type", "service_name",
|
||||
"service_url", "region", "insecure", "token",
|
||||
"secure", "json", "terse", "hide-debug"]
|
||||
|
||||
oparser = common.CliOptions.create_optparser(True)
|
||||
for option_name in option_names:
|
||||
self.check_option(oparser, option_name)
|
||||
|
||||
oparser = common.CliOptions.create_optparser(False)
|
||||
for option_name in option_names:
|
||||
self.check_option(oparser, option_name)
|
||||
|
||||
|
||||
class ArgumentRequiredTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(ArgumentRequiredTest, self).setUp()
|
||||
self.param = "test-param"
|
||||
self.arg_req = common.ArgumentRequired(self.param)
|
||||
|
||||
def test___init__(self):
|
||||
self.assertEqual(self.param, self.arg_req.param)
|
||||
|
||||
def test___str__(self):
|
||||
expected = 'Argument "--%s" required.' % self.param
|
||||
self.assertEqual(expected, self.arg_req.__str__())
|
||||
|
||||
|
||||
class CommandsBaseTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(CommandsBaseTest, self).setUp()
|
||||
self.orig_sys_exit = sys.exit
|
||||
sys.exit = mock.Mock(return_value=None)
|
||||
self.orig_sys_argv = sys.argv
|
||||
sys.argv = ['fakecmd']
|
||||
parser = common.CliOptions().create_optparser(False)
|
||||
self.cmd_base = common.CommandsBase(parser)
|
||||
|
||||
def tearDown(self):
|
||||
super(CommandsBaseTest, self).tearDown()
|
||||
sys.exit = self.orig_sys_exit
|
||||
sys.argv = self.orig_sys_argv
|
||||
|
||||
def test___init__(self):
|
||||
self.assertIsNotNone(self.cmd_base)
|
||||
|
||||
def test__safe_exec(self):
|
||||
func = mock.Mock(return_value="test")
|
||||
self.cmd_base.debug = True
|
||||
r = self.cmd_base._safe_exec(func)
|
||||
self.assertEqual("test", r)
|
||||
|
||||
self.cmd_base.debug = False
|
||||
r = self.cmd_base._safe_exec(func)
|
||||
self.assertEqual("test", r)
|
||||
|
||||
func = mock.Mock(side_effect=ValueError) # an arbitrary exception
|
||||
r = self.cmd_base._safe_exec(func)
|
||||
self.assertIsNone(r)
|
||||
|
||||
def test__prepare_parser(self):
|
||||
parser = optparse.OptionParser()
|
||||
common.CommandsBase.params = ["test_1", "test_2"]
|
||||
self.cmd_base._prepare_parser(parser)
|
||||
option = parser.get_option("--%s" % "test_1")
|
||||
self.assertIsNotNone(option)
|
||||
option = parser.get_option("--%s" % "test_2")
|
||||
self.assertIsNotNone(option)
|
||||
|
||||
def test__parse_options(self):
|
||||
parser = optparse.OptionParser()
|
||||
parser.add_option("--%s" % "test_1", default="test_1v")
|
||||
parser.add_option("--%s" % "test_2", default="test_2v")
|
||||
self.cmd_base._parse_options(parser)
|
||||
self.assertEqual("test_1v", self.cmd_base.test_1)
|
||||
self.assertEqual("test_2v", self.cmd_base.test_2)
|
||||
|
||||
def test__require(self):
|
||||
self.assertRaises(common.ArgumentRequired,
|
||||
self.cmd_base._require, "attr_1")
|
||||
self.cmd_base.attr_1 = None
|
||||
self.assertRaises(common.ArgumentRequired,
|
||||
self.cmd_base._require, "attr_1")
|
||||
self.cmd_base.attr_1 = "attr_v1"
|
||||
self.cmd_base._require("attr_1")
|
||||
|
||||
def test__make_list(self):
|
||||
self.assertRaises(AttributeError, self.cmd_base._make_list, "attr1")
|
||||
self.cmd_base.attr1 = "v1,v2"
|
||||
self.cmd_base._make_list("attr1")
|
||||
self.assertEqual(["v1", "v2"], self.cmd_base.attr1)
|
||||
self.cmd_base.attr1 = ["v3"]
|
||||
self.cmd_base._make_list("attr1")
|
||||
self.assertEqual(["v3"], self.cmd_base.attr1)
|
||||
|
||||
def test__pretty_print(self):
|
||||
func = mock.Mock(return_value=None)
|
||||
self.cmd_base.verbose = True
|
||||
self.assertIsNone(self.cmd_base._pretty_print(func))
|
||||
self.cmd_base.verbose = False
|
||||
self.assertIsNone(self.cmd_base._pretty_print(func))
|
||||
|
||||
def test__dumps(self):
|
||||
orig_dumps = json.dumps
|
||||
json.dumps = mock.Mock(return_value="test-dump")
|
||||
self.assertEqual("test-dump", self.cmd_base._dumps("item"))
|
||||
json.dumps = orig_dumps
|
||||
|
||||
def test__pretty_list(self):
|
||||
func = mock.Mock(return_value=None)
|
||||
self.cmd_base.verbose = True
|
||||
self.assertIsNone(self.cmd_base._pretty_list(func))
|
||||
self.cmd_base.verbose = False
|
||||
self.assertIsNone(self.cmd_base._pretty_list(func))
|
||||
item = mock.Mock(return_value="test")
|
||||
item._info = "info"
|
||||
func = mock.Mock(return_value=[item])
|
||||
self.assertIsNone(self.cmd_base._pretty_list(func))
|
||||
|
||||
def test__pretty_paged(self):
|
||||
self.cmd_base.limit = "5"
|
||||
func = mock.Mock(return_value=None)
|
||||
self.cmd_base.verbose = True
|
||||
self.assertIsNone(self.cmd_base._pretty_paged(func))
|
||||
|
||||
self.cmd_base.verbose = False
|
||||
|
||||
class MockIterable(collections.Iterable):
|
||||
links = ["item"]
|
||||
count = 1
|
||||
|
||||
def __iter__(self):
|
||||
return ["item1"]
|
||||
|
||||
def __len__(self):
|
||||
return self.count
|
||||
|
||||
ret = MockIterable()
|
||||
func = mock.Mock(return_value=ret)
|
||||
self.assertIsNone(self.cmd_base._pretty_paged(func))
|
||||
|
||||
ret.count = 0
|
||||
self.assertIsNone(self.cmd_base._pretty_paged(func))
|
||||
|
||||
func = mock.Mock(side_effect=ValueError)
|
||||
self.assertIsNone(self.cmd_base._pretty_paged(func))
|
||||
self.cmd_base.debug = True
|
||||
self.cmd_base.marker = mock.Mock()
|
||||
self.assertRaises(ValueError, self.cmd_base._pretty_paged, func)
|
||||
|
||||
|
||||
class AuthTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthTest, self).setUp()
|
||||
self.orig_sys_exit = sys.exit
|
||||
sys.exit = mock.Mock(return_value=None)
|
||||
self.orig_sys_argv = sys.argv
|
||||
sys.argv = ['fakecmd']
|
||||
self.parser = common.CliOptions().create_optparser(False)
|
||||
self.auth = common.Auth(self.parser)
|
||||
|
||||
def tearDown(self):
|
||||
super(AuthTest, self).tearDown()
|
||||
sys.exit = self.orig_sys_exit
|
||||
sys.argv = self.orig_sys_argv
|
||||
|
||||
def test___init__(self):
|
||||
self.assertIsNone(self.auth.dbaas)
|
||||
self.assertIsNone(self.auth.apikey)
|
||||
|
||||
def test_login(self):
|
||||
self.auth.username = "username"
|
||||
self.auth.apikey = "apikey"
|
||||
self.auth.tenant_id = "tenant_id"
|
||||
self.auth.auth_url = "auth_url"
|
||||
dbaas = mock.Mock()
|
||||
dbaas.authenticate = mock.Mock(return_value=None)
|
||||
dbaas.client = mock.Mock()
|
||||
dbaas.client.auth_token = mock.Mock()
|
||||
dbaas.client.service_url = mock.Mock()
|
||||
self.auth._get_client = mock.Mock(return_value=dbaas)
|
||||
self.auth.login()
|
||||
|
||||
self.auth.debug = True
|
||||
self.auth._get_client = mock.Mock(side_effect=ValueError)
|
||||
self.assertRaises(ValueError, self.auth.login)
|
||||
|
||||
self.auth.debug = False
|
||||
self.auth.login()
|
||||
|
||||
|
||||
class AuthedCommandsBaseTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(AuthedCommandsBaseTest, self).setUp()
|
||||
self.orig_sys_exit = sys.exit
|
||||
sys.exit = mock.Mock(return_value=None)
|
||||
self.orig_sys_argv = sys.argv
|
||||
sys.argv = ['fakecmd']
|
||||
|
||||
def tearDown(self):
|
||||
super(AuthedCommandsBaseTest, self).tearDown()
|
||||
sys.exit = self.orig_sys_exit
|
||||
self.orig_sys_argv = sys.argv
|
||||
|
||||
def test___init__(self):
|
||||
parser = common.CliOptions().create_optparser(False)
|
||||
common.AuthedCommandsBase.debug = True
|
||||
dbaas = mock.Mock()
|
||||
dbaas.authenticate = mock.Mock(return_value=None)
|
||||
dbaas.client = mock.Mock()
|
||||
dbaas.client.auth_token = mock.Mock()
|
||||
dbaas.client.service_url = mock.Mock()
|
||||
dbaas.client.authenticate_with_token = mock.Mock()
|
||||
common.AuthedCommandsBase._get_client = mock.Mock(return_value=dbaas)
|
||||
common.AuthedCommandsBase(parser)
|
||||
|
||||
|
||||
class PaginatedTest(testtools.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(PaginatedTest, self).setUp()
|
||||
self.items_ = ["item1", "item2"]
|
||||
self.next_marker_ = "next-marker"
|
||||
self.links_ = ["link1", "link2"]
|
||||
self.pgn = common.Paginated(self.items_, self.next_marker_,
|
||||
self.links_)
|
||||
|
||||
def tearDown(self):
|
||||
super(PaginatedTest, self).tearDown()
|
||||
|
||||
def test___init__(self):
|
||||
self.assertEqual(self.items_, self.pgn.items)
|
||||
self.assertEqual(self.next_marker_, self.pgn.next)
|
||||
self.assertEqual(self.links_, self.pgn.links)
|
||||
|
||||
def test___len__(self):
|
||||
self.assertEqual(len(self.items_), self.pgn.__len__())
|
||||
|
||||
def test___iter__(self):
|
||||
itr_expected = self.items_.__iter__()
|
||||
itr = self.pgn.__iter__()
|
||||
self.assertEqual(next(itr_expected), next(itr))
|
||||
self.assertEqual(next(itr_expected), next(itr))
|
||||
self.assertRaises(StopIteration, next, itr_expected)
|
||||
self.assertRaises(StopIteration, next, itr)
|
||||
|
||||
def test___getitem__(self):
|
||||
self.assertEqual(self.items_[0], self.pgn.__getitem__(0))
|
||||
|
||||
def test___setitem__(self):
|
||||
self.pgn.__setitem__(0, "new-item")
|
||||
self.assertEqual("new-item", self.pgn.items[0])
|
||||
|
||||
def test___delitem(self):
|
||||
del self.pgn[0]
|
||||
self.assertEqual(1, self.pgn.__len__())
|
||||
|
||||
def test___reversed__(self):
|
||||
itr = self.pgn.__reversed__()
|
||||
self.assertEqual("item2", next(itr))
|
||||
self.assertEqual("item1", next(itr))
|
||||
self.assertRaises(StopIteration, next, itr)
|
||||
|
||||
def test___contains__(self):
|
||||
self.assertTrue(self.pgn.__contains__("item1"))
|
||||
self.assertTrue(self.pgn.__contains__("item2"))
|
||||
self.assertFalse(self.pgn.__contains__("item3"))
|
||||
@@ -1,46 +0,0 @@
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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
|
||||
|
||||
|
||||
class HookableMixin(object):
|
||||
"""Mixin so classes can register and run hooks."""
|
||||
_hooks_map = {}
|
||||
|
||||
@classmethod
|
||||
def add_hook(cls, hook_type, hook_func):
|
||||
if hook_type not in cls._hooks_map:
|
||||
cls._hooks_map[hook_type] = []
|
||||
|
||||
cls._hooks_map[hook_type].append(hook_func)
|
||||
|
||||
@classmethod
|
||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||
for hook_func in hook_funcs:
|
||||
hook_func(*args, **kwargs)
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""Returns environment variables.
|
||||
|
||||
Returns the first environment variable set
|
||||
if none are non-empty, defaults to '' or keyword arg default.
|
||||
"""
|
||||
for v in vars:
|
||||
value = os.environ.get(v, None)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
@@ -1,36 +0,0 @@
|
||||
# Copyright (c) 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from troveclient.compat import base
|
||||
|
||||
|
||||
class Version(base.Resource):
|
||||
"""Version is an opaque instance used to hold version information."""
|
||||
def __repr__(self):
|
||||
return "<Version: %s>" % self.id
|
||||
|
||||
|
||||
class Versions(base.ManagerWithFind):
|
||||
"""Manage :class:`Versions` information."""
|
||||
|
||||
resource_class = Version
|
||||
|
||||
def index(self, url):
|
||||
"""Get a list of all versions.
|
||||
|
||||
:rtype: list of :class:`Versions`.
|
||||
"""
|
||||
resp, body = self.api.client.request(url, "GET")
|
||||
return [self.resource_class(self, res) for res in body['versions']]
|
||||
@@ -1,40 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# Copyright 2013 Rackspace Hosting
|
||||
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""
|
||||
Exception definitions
|
||||
"""
|
||||
|
||||
# alias exceptions from apiclient for users of this module
|
||||
from troveclient.apiclient.exceptions import * # noqa
|
||||
|
||||
|
||||
class NoTokenLookupException(Exception):
|
||||
"""This form of authentication does not support looking up
|
||||
endpoints from an existing token.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResponseFormatError(Exception):
|
||||
"""Could not parse the response format."""
|
||||
pass
|
||||
|
||||
|
||||
class GuestLogNotFoundError(Exception):
|
||||
"""The specified guest log does not exist."""
|
||||
pass
|
||||
@@ -1,39 +0,0 @@
|
||||
# Copyright 2011 OpenStack Foundation
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from troveclient import base
|
||||
from troveclient import utils
|
||||
|
||||
|
||||
class Extension(utils.HookableMixin):
|
||||
"""Extension descriptor."""
|
||||
|
||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
||||
|
||||
def __init__(self, name, module):
|
||||
self.name = name
|
||||
self.module = module
|
||||
self._parse_extension_module()
|
||||
|
||||
def _parse_extension_module(self):
|
||||
self.manager_class = None
|
||||
for attr_name, attr_value in self.module.__dict__.items():
|
||||
if attr_name in self.SUPPORTED_HOOKS:
|
||||
self.add_hook(attr_name, attr_value)
|
||||
elif utils.safe_issubclass(attr_value, base.Manager):
|
||||
self.manager_class = attr_value
|
||||
|
||||
def __repr__(self):
|
||||
return "<Extension '%s'>" % self.name
|
||||
@@ -1,41 +0,0 @@
|
||||
# Copyright 2014 Tesora, 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.
|
||||
|
||||
"""oslo.i18n integration module.
|
||||
|
||||
See http://docs.openstack.org/developer/oslo.i18n/usage.html
|
||||
|
||||
"""
|
||||
|
||||
import oslo_i18n
|
||||
|
||||
|
||||
# NOTE(dhellmann): This reference to o-s-l-o will be replaced by the
|
||||
# application name when this module is synced into the separate
|
||||
# repository. It is OK to have more than one translation function
|
||||
# using the same domain, since there will still only be one message
|
||||
# catalog.
|
||||
_translators = oslo_i18n.TranslatorFactory(domain='python-troveclient')
|
||||
|
||||
# The primary translation function using the well-known name "_"
|
||||
_ = _translators.primary
|
||||
|
||||
# Translators for log levels.
|
||||
#
|
||||
# The abbreviated names are meant to reflect the usual use of a short
|
||||
# name like '_'. The "L" is for "log" and the other letter comes from
|
||||
# the level.
|
||||
_LI = _translators.log_info
|
||||
_LW = _translators.log_warning
|
||||
_LE = _translators.log_error
|
||||
_LC = _translators.log_critical
|
||||
@@ -1,54 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
#
|
||||
|
||||
import logging
|
||||
|
||||
from osc_lib import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_DATABASE_API_VERSION = '1'
|
||||
API_VERSION_OPTION = 'os_database_api_version'
|
||||
API_NAME = 'database'
|
||||
API_VERSIONS = {
|
||||
'1': 'troveclient.v1.client.Client',
|
||||
}
|
||||
|
||||
|
||||
def make_client(instance):
|
||||
"""Returns a database service client"""
|
||||
trove_client = utils.get_client_class(
|
||||
API_NAME,
|
||||
instance._api_version[API_NAME],
|
||||
API_VERSIONS)
|
||||
LOG.debug('Instantiating database client: %s', trove_client)
|
||||
client = trove_client(
|
||||
auth=instance.auth,
|
||||
session=instance.session
|
||||
)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
def build_option_parser(parser):
|
||||
"""Hook to add global options"""
|
||||
parser.add_argument(
|
||||
'--os-database-api-version',
|
||||
metavar='<database-api-version>',
|
||||
default=utils.env(
|
||||
'OS_DATABASE_API_VERSION',
|
||||
default=DEFAULT_DATABASE_API_VERSION),
|
||||
help='Database API version, default=' +
|
||||
DEFAULT_DATABASE_API_VERSION +
|
||||
' (Env: OS_DATABASE_API_VERSION)')
|
||||
return parser
|
||||
@@ -1,66 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Database v1 Backups action implementations"""
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils as osc_utils
|
||||
|
||||
from troveclient.i18n import _
|
||||
|
||||
|
||||
class ListDatabaseBackups(command.Lister):
|
||||
|
||||
_description = _("List database backups")
|
||||
columns = ['ID', 'Instance ID', 'Name', 'Status', 'Parent ID',
|
||||
'Updated']
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListDatabaseBackups, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
dest='limit',
|
||||
metavar='<limit>',
|
||||
default=None,
|
||||
help=_('Return up to N number of the most recent bcakups.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--marker',
|
||||
dest='marker',
|
||||
metavar='<ID>',
|
||||
type=str,
|
||||
default=None,
|
||||
help=_('Begin displaying the results for IDs greater than the'
|
||||
'specified marker. When used with :option:`--limit,` set'
|
||||
'this to the last ID displayed in the previous run.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--datastore',
|
||||
dest='datastore',
|
||||
metavar='<datastore>',
|
||||
default=None,
|
||||
help=_('ID or name of the datastore (to filter backups by).')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
database_backups = self.app.client_manager.database.backups
|
||||
items = database_backups.list(limit=parsed_args.limit,
|
||||
datastore=parsed_args.datastore,
|
||||
marker=parsed_args.marker)
|
||||
backups = items
|
||||
while items.next and not parsed_args.limit:
|
||||
items = database_backups.list(marker=items.next)
|
||||
backups += items
|
||||
backups = [osc_utils.get_item_properties(b, self.columns)
|
||||
for b in backups]
|
||||
return self.columns, backups
|
||||
@@ -1,61 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Database v1 Clusters action implementations"""
|
||||
|
||||
from osc_lib.command import command
|
||||
from osc_lib import utils
|
||||
|
||||
from troveclient.i18n import _
|
||||
|
||||
|
||||
class ListDatabaseClusters(command.Lister):
|
||||
|
||||
_description = _("List database clusters")
|
||||
columns = ['ID', 'Name', 'Datastore', 'Datastore Version',
|
||||
'Task Name']
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListDatabaseClusters, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
dest='limit',
|
||||
metavar='<limit>',
|
||||
type=int,
|
||||
default=None,
|
||||
help=_('Limit the number of results displayed.')
|
||||
)
|
||||
parser.add_argument(
|
||||
'--marker',
|
||||
dest='marker',
|
||||
metavar='<ID>',
|
||||
type=str,
|
||||
default=None,
|
||||
help=_('Begin displaying the results for IDs greater than the'
|
||||
' specified marker. When used with :option:`--limit,` set'
|
||||
' this to the last ID displayed in the previous run.')
|
||||
)
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
database_clusters = self.app.client_manager.database.clusters
|
||||
clusters = database_clusters.list(limit=parsed_args.limit,
|
||||
marker=parsed_args.marker)
|
||||
for cluster in clusters:
|
||||
setattr(cluster, 'datastore_version',
|
||||
cluster.datastore['version'])
|
||||
setattr(cluster, 'datastore', cluster.datastore['type'])
|
||||
setattr(cluster, 'task_name', cluster.task['name'])
|
||||
|
||||
clusters = [utils.get_item_properties(c, self.columns)
|
||||
for c in clusters]
|
||||
return self.columns, clusters
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user