Deprecate murano-metadataclient
Change-Id: I4b5340c79f9b8eaca5aca920033b3c9a850bc064
This commit is contained in:
parent
55dfd0f441
commit
2fce7639f7
191
LICENSE
191
LICENSE
@ -1,191 +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:
|
|
||||||
|
|
||||||
You must give any other recipients of the Work or Derivative Works a copy of
|
|
||||||
this License; and
|
|
||||||
You must cause any modified files to carry prominent notices stating that You
|
|
||||||
changed the files; and
|
|
||||||
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
|
|
||||||
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.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following boilerplate
|
|
||||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
|
||||||
identifying information. (Don't include the brackets!) The text should be
|
|
||||||
enclosed in the appropriate comment syntax for the file format. We also
|
|
||||||
recommend that a file or class name and description of purpose be included on
|
|
||||||
the same "printed page" as the copyright notice for easier identification within
|
|
||||||
third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
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.
|
|
39
README.rst
39
README.rst
@ -1,36 +1,5 @@
|
|||||||
Murano
|
DEPRECATED: murano-metadataclient
|
||||||
======
|
=================================
|
||||||
Murano Project introduces an application catalog, which allows application
|
|
||||||
developers and cloud administrators to publish various cloud-ready
|
|
||||||
applications in a browsable categorised catalog, which may be used by the
|
|
||||||
cloud users (including the inexperienced ones) to pick-up the needed
|
|
||||||
applications and services and composes the reliable environments out of them
|
|
||||||
in a “push-the-button” manner.
|
|
||||||
|
|
||||||
murano-metadataclient
|
**Warning** - this repository is deprecated. All functionality has been moved
|
||||||
---------------------
|
to `python-muranoclient <https://git.openstack.org/cgit/stackforge/python-muranoclient>`__
|
||||||
murano-metadataclient is a python bindings for murano-repository project.
|
|
||||||
|
|
||||||
Project Resources
|
|
||||||
-----------------
|
|
||||||
* `Murano at Launchpad <http://launchpad.net/murano>`__
|
|
||||||
* `Wiki <https://wiki.openstack.org/wiki/Murano>`__
|
|
||||||
* `Code Review <https://review.openstack.org/>`__
|
|
||||||
* `Sources <https://wiki.openstack.org/wiki/Murano/SourceCode>`__
|
|
||||||
* `Developers Guide <http://murano-docs.github.io/latest/developers-guide/content/ch02.html>`__
|
|
||||||
|
|
||||||
How To Participate
|
|
||||||
------------------
|
|
||||||
If you would like to ask some questions or make proposals, feel free to reach
|
|
||||||
us on #murano IRC channel at FreeNode. Typically somebody from our team will
|
|
||||||
be online at IRC from 6:00 to 20:00 UTC. You can also contact Murano community
|
|
||||||
directly by openstack-dev@lists.openstack.org adding [Murano] to a subject.
|
|
||||||
|
|
||||||
We’re holding public weekly meetings on Tuesdays at 17:00 UTC
|
|
||||||
on #openstack-meeting-alt IRC channel at FreeNode.
|
|
||||||
|
|
||||||
If you want to contribute either to docs or to code, simply send us change
|
|
||||||
request via `gerrit <https://review.openstack.org/>`__.
|
|
||||||
You can `file bugs <https://bugs.launchpad.net/murano/+filebug>`__ and
|
|
||||||
`register blueprints <https://blueprints.launchpad.net/murano/+addspec>`__ on
|
|
||||||
Launchpad.
|
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
# Copyright (c) 2013 Mirantis, 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.
|
|
||||||
|
|
||||||
from metadataclient.common import utils
|
|
||||||
|
|
||||||
|
|
||||||
def Client(version, *args, **kwargs):
|
|
||||||
module = utils.import_versioned_module(version, 'client')
|
|
||||||
client_class = getattr(module, 'Client')
|
|
||||||
return client_class(*args, **kwargs)
|
|
@ -1,167 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Base utilities to build API operation managers and objects on top of.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import copy
|
|
||||||
|
|
||||||
|
|
||||||
# Python 2.4 compat
|
|
||||||
try:
|
|
||||||
all
|
|
||||||
except NameError:
|
|
||||||
def all(iterable):
|
|
||||||
return True not in (not x for x in iterable)
|
|
||||||
|
|
||||||
|
|
||||||
def getid(obj):
|
|
||||||
"""
|
|
||||||
Abstracts the common pattern of allowing both an object or an object's ID
|
|
||||||
(UUID) as a parameter when dealing with relationships.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
return obj.id
|
|
||||||
except AttributeError:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class Manager(object):
|
|
||||||
"""
|
|
||||||
Managers interact with a particular type of API (servers, flavors, images,
|
|
||||||
etc.) and provide CRUD operations for them.
|
|
||||||
"""
|
|
||||||
resource_class = None
|
|
||||||
|
|
||||||
def __init__(self, api):
|
|
||||||
self.api = api
|
|
||||||
|
|
||||||
def _list(self, url, response_key=None, obj_class=None,
|
|
||||||
body=None, headers={}):
|
|
||||||
|
|
||||||
resp, body = self.api.json_request('GET', url, headers=headers)
|
|
||||||
|
|
||||||
if obj_class is None:
|
|
||||||
obj_class = self.resource_class
|
|
||||||
|
|
||||||
if response_key:
|
|
||||||
if not response_key in body:
|
|
||||||
body[response_key] = []
|
|
||||||
data = body[response_key]
|
|
||||||
else:
|
|
||||||
data = body
|
|
||||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
|
||||||
|
|
||||||
def _delete(self, url, headers={}):
|
|
||||||
self.api.raw_request('DELETE', url, headers=headers)
|
|
||||||
|
|
||||||
def _update(self, url, body, response_key=None, headers={}):
|
|
||||||
resp, body = self.api.json_request('PUT', url, body=body,
|
|
||||||
headers=headers)
|
|
||||||
# PUT requests may not return a body
|
|
||||||
if body:
|
|
||||||
if response_key:
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
return self.resource_class(self, body)
|
|
||||||
|
|
||||||
def _create(self, url, body=None, response_key=None,
|
|
||||||
return_raw=False, headers={}):
|
|
||||||
|
|
||||||
if body:
|
|
||||||
resp, body = self.api.json_request('POST', url,
|
|
||||||
body=body, headers=headers)
|
|
||||||
else:
|
|
||||||
resp, body = self.api.json_request('POST', url, headers=headers)
|
|
||||||
if return_raw:
|
|
||||||
if response_key:
|
|
||||||
return body[response_key]
|
|
||||||
return body
|
|
||||||
if response_key:
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
return self.resource_class(self, body)
|
|
||||||
|
|
||||||
def _get(self, url, response_key=None, return_raw=False, headers={}):
|
|
||||||
resp, body = self.api.json_request('GET', url, headers=headers)
|
|
||||||
if return_raw:
|
|
||||||
if response_key:
|
|
||||||
return body[response_key]
|
|
||||||
return body
|
|
||||||
if response_key:
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
return self.resource_class(self, body)
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(object):
|
|
||||||
"""
|
|
||||||
A resource represents a particular instance of an object (tenant, user,
|
|
||||||
etc). This is pretty much just a bag for attributes.
|
|
||||||
|
|
||||||
:param manager: Manager object
|
|
||||||
:param info: dictionary representing resource attributes
|
|
||||||
:param loaded: prevent lazy-loading if set to True
|
|
||||||
"""
|
|
||||||
def __init__(self, manager, info, loaded=False):
|
|
||||||
self.manager = manager
|
|
||||||
self._info = info
|
|
||||||
self._add_details(info)
|
|
||||||
self._loaded = loaded
|
|
||||||
|
|
||||||
def _add_details(self, info):
|
|
||||||
for (k, v) in info.iteritems():
|
|
||||||
setattr(self, k, v)
|
|
||||||
|
|
||||||
def __getattr__(self, k):
|
|
||||||
if k not in self.__dict__:
|
|
||||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
|
||||||
if not self.is_loaded():
|
|
||||||
self.get()
|
|
||||||
return self.__getattr__(k)
|
|
||||||
|
|
||||||
raise AttributeError(k)
|
|
||||||
else:
|
|
||||||
return self.__dict__[k]
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
|
||||||
k != 'manager')
|
|
||||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
|
||||||
return "<%s %s>" % (self.__class__.__name__, info)
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
|
||||||
self.set_loaded(True)
|
|
||||||
if not hasattr(self.manager, 'get'):
|
|
||||||
return
|
|
||||||
|
|
||||||
new = self.manager.get(self.id)
|
|
||||||
if new:
|
|
||||||
self._add_details(new._info)
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if not isinstance(other, self.__class__):
|
|
||||||
return False
|
|
||||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
|
||||||
return self.id == other.id
|
|
||||||
return self._info == other._info
|
|
||||||
|
|
||||||
def is_loaded(self):
|
|
||||||
return self._loaded
|
|
||||||
|
|
||||||
def set_loaded(self, val):
|
|
||||||
self._loaded = val
|
|
||||||
|
|
||||||
def to_dict(self):
|
|
||||||
return copy.deepcopy(self._info)
|
|
@ -1,178 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class BaseException(Exception):
|
|
||||||
"""An error occurred."""
|
|
||||||
def __init__(self, message=None):
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.message or self.__class__.__doc__
|
|
||||||
|
|
||||||
|
|
||||||
class CommandError(BaseException):
|
|
||||||
"""Invalid usage of CLI."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidEndpoint(BaseException):
|
|
||||||
"""The provided endpoint is invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
class CommunicationError(BaseException):
|
|
||||||
"""Unable to communicate with server."""
|
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPException(ClientException):
|
|
||||||
"""Base exception for all HTTP-derived exceptions."""
|
|
||||||
code = 'N/A'
|
|
||||||
|
|
||||||
def __init__(self, details=None):
|
|
||||||
self.details = details or self.__class__.__name__
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s (HTTP %s)" % (self.details, self.code)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMultipleChoices(HTTPException):
|
|
||||||
code = 300
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
self.details = ("Requested version of OpenStack Images API is not"
|
|
||||||
"available.")
|
|
||||||
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
|
|
||||||
self.details)
|
|
||||||
|
|
||||||
|
|
||||||
class BadRequest(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPBadRequest(BadRequest):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 401
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPUnauthorized(Unauthorized):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 403
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPForbidden(Forbidden):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 404
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPNotFound(NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethodNotAllowed(HTTPException):
|
|
||||||
code = 405
|
|
||||||
|
|
||||||
|
|
||||||
class Conflict(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 409
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPConflict(Conflict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OverLimit(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 413
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPOverLimit(OverLimit):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPInternalServerError(HTTPException):
|
|
||||||
code = 500
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPNotImplemented(HTTPException):
|
|
||||||
code = 501
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPBadGateway(HTTPException):
|
|
||||||
code = 502
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceUnavailable(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 503
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPServiceUnavailable(ServiceUnavailable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
#NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
|
|
||||||
# classes
|
|
||||||
_code_map = {}
|
|
||||||
for obj_name in dir(sys.modules[__name__]):
|
|
||||||
if obj_name.startswith('HTTP'):
|
|
||||||
obj = getattr(sys.modules[__name__], obj_name)
|
|
||||||
_code_map[obj.code] = obj
|
|
||||||
|
|
||||||
|
|
||||||
def from_response(response, body=None):
|
|
||||||
"""Return an instance of an HTTPException based on httplib response."""
|
|
||||||
cls = _code_map.get(response.status, HTTPException)
|
|
||||||
if body:
|
|
||||||
details = body.replace('\n\n', '\n')
|
|
||||||
return cls(details=details)
|
|
||||||
|
|
||||||
return cls()
|
|
||||||
|
|
||||||
|
|
||||||
class NoTokenLookupException(Exception):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointNotFound(Exception):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SSLConfigurationError(BaseException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SSLCertificateError(BaseException):
|
|
||||||
pass
|
|
@ -1,492 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import copy
|
|
||||||
import errno
|
|
||||||
import hashlib
|
|
||||||
import httplib
|
|
||||||
import logging
|
|
||||||
import posixpath
|
|
||||||
import socket
|
|
||||||
import StringIO
|
|
||||||
import struct
|
|
||||||
import urlparse
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
import OpenSSL
|
|
||||||
|
|
||||||
from metadataclient.common import exceptions as exc
|
|
||||||
from metadataclient.common import utils
|
|
||||||
from metadataclient.openstack.common import strutils
|
|
||||||
|
|
||||||
try:
|
|
||||||
from eventlet import patcher
|
|
||||||
# Handle case where we are running in a monkey patched environment
|
|
||||||
if patcher.is_monkey_patched('socket'):
|
|
||||||
from eventlet.green.httplib import HTTPSConnection
|
|
||||||
from eventlet.green.OpenSSL.SSL import GreenConnection as Connection
|
|
||||||
from eventlet.greenio import GreenSocket
|
|
||||||
# TODO(mclaren): A getsockopt workaround: see 'getsockopt' doc string
|
|
||||||
GreenSocket.getsockopt = utils.getsockopt
|
|
||||||
else:
|
|
||||||
raise ImportError
|
|
||||||
except ImportError:
|
|
||||||
from httplib import HTTPSConnection
|
|
||||||
from OpenSSL.SSL import Connection as Connection
|
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
|
||||||
USER_AGENT = 'python-metadatalient'
|
|
||||||
CHUNKSIZE = 1024 * 64 # 64kB
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
|
||||||
|
|
||||||
def __init__(self, endpoint, **kwargs):
|
|
||||||
self.endpoint = endpoint
|
|
||||||
endpoint_parts = self.parse_endpoint(self.endpoint)
|
|
||||||
self.endpoint_scheme = endpoint_parts.scheme
|
|
||||||
self.endpoint_hostname = endpoint_parts.hostname
|
|
||||||
self.endpoint_port = endpoint_parts.port
|
|
||||||
self.endpoint_path = endpoint_parts.path
|
|
||||||
|
|
||||||
self.connection_class = self.get_connection_class(self.endpoint_scheme)
|
|
||||||
self.connection_kwargs = self.get_connection_kwargs(
|
|
||||||
self.endpoint_scheme, **kwargs)
|
|
||||||
|
|
||||||
self.identity_headers = kwargs.get('identity_headers')
|
|
||||||
self.auth_token = kwargs.get('token')
|
|
||||||
if self.identity_headers:
|
|
||||||
if self.identity_headers.get('X-Auth-Token'):
|
|
||||||
self.auth_token = self.identity_headers.get('X-Auth-Token')
|
|
||||||
del self.identity_headers['X-Auth-Token']
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def parse_endpoint(endpoint):
|
|
||||||
return urlparse.urlparse(endpoint)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connection_class(scheme):
|
|
||||||
if scheme == 'https':
|
|
||||||
return VerifiedHTTPSConnection
|
|
||||||
else:
|
|
||||||
return httplib.HTTPConnection
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_connection_kwargs(scheme, **kwargs):
|
|
||||||
_kwargs = {'timeout': float(kwargs.get('timeout', 600))}
|
|
||||||
|
|
||||||
if scheme == 'https':
|
|
||||||
_kwargs['cacert'] = kwargs.get('cacert', None)
|
|
||||||
_kwargs['cert_file'] = kwargs.get('cert_file', None)
|
|
||||||
_kwargs['key_file'] = kwargs.get('key_file', None)
|
|
||||||
_kwargs['insecure'] = kwargs.get('insecure', False)
|
|
||||||
_kwargs['ssl_compression'] = kwargs.get('ssl_compression', True)
|
|
||||||
|
|
||||||
return _kwargs
|
|
||||||
|
|
||||||
def get_connection(self):
|
|
||||||
_class = self.connection_class
|
|
||||||
try:
|
|
||||||
return _class(self.endpoint_hostname, self.endpoint_port,
|
|
||||||
**self.connection_kwargs)
|
|
||||||
except httplib.InvalidURL:
|
|
||||||
raise exc.InvalidEndpoint()
|
|
||||||
|
|
||||||
def log_curl_request(self, method, url, kwargs):
|
|
||||||
curl = ['curl -i -X %s' % method]
|
|
||||||
|
|
||||||
for (key, value) in kwargs['headers'].items():
|
|
||||||
header = '-H \'%s: %s\'' % (key, value)
|
|
||||||
curl.append(header)
|
|
||||||
|
|
||||||
conn_params_fmt = [
|
|
||||||
('key_file', '--key %s'),
|
|
||||||
('cert_file', '--cert %s'),
|
|
||||||
('cacert', '--cacert %s'),
|
|
||||||
]
|
|
||||||
for (key, fmt) in conn_params_fmt:
|
|
||||||
value = self.connection_kwargs.get(key)
|
|
||||||
if value:
|
|
||||||
curl.append(fmt % value)
|
|
||||||
|
|
||||||
if self.connection_kwargs.get('insecure'):
|
|
||||||
curl.append('-k')
|
|
||||||
|
|
||||||
if kwargs.get('body') is not None:
|
|
||||||
curl.append('-d \'%s\'' % kwargs['body'])
|
|
||||||
|
|
||||||
curl.append('%s%s' % (self.endpoint, url))
|
|
||||||
LOG.debug(strutils.safe_encode(' '.join(curl)))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def log_http_response(resp, body=None):
|
|
||||||
status = (resp.version / 10.0, resp.status, resp.reason)
|
|
||||||
dump = ['\nHTTP/%.1f %s %s' % status]
|
|
||||||
dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()])
|
|
||||||
dump.append('')
|
|
||||||
if body:
|
|
||||||
dump.extend([body, ''])
|
|
||||||
LOG.debug(strutils.safe_encode('\n'.join(dump)))
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def encode_headers(headers):
|
|
||||||
"""Encodes headers.
|
|
||||||
|
|
||||||
Note: This should be used right before
|
|
||||||
sending anything out.
|
|
||||||
|
|
||||||
:param headers: Headers to encode
|
|
||||||
:returns: Dictionary with encoded headers'
|
|
||||||
names and values
|
|
||||||
"""
|
|
||||||
to_str = strutils.safe_encode
|
|
||||||
return dict([(to_str(h), to_str(v)) for h, v in headers.iteritems()])
|
|
||||||
|
|
||||||
def _http_request(self, url, method, **kwargs):
|
|
||||||
"""Send an http request with the specified characteristics.
|
|
||||||
|
|
||||||
Wrapper around httplib.HTTP(S)Connection.request to handle tasks such
|
|
||||||
as setting headers and error handling.
|
|
||||||
"""
|
|
||||||
# Copy the kwargs so we can reuse the original in case of redirects
|
|
||||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
|
||||||
kwargs['headers'].setdefault('User-Agent', USER_AGENT)
|
|
||||||
if self.auth_token:
|
|
||||||
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
|
||||||
|
|
||||||
if self.identity_headers:
|
|
||||||
for k, v in self.identity_headers.iteritems():
|
|
||||||
kwargs['headers'].setdefault(k, v)
|
|
||||||
|
|
||||||
self.log_curl_request(method, url, kwargs)
|
|
||||||
conn = self.get_connection()
|
|
||||||
|
|
||||||
# Note(flaper87): Before letting headers / url fly,
|
|
||||||
# they should be encoded otherwise httplib will
|
|
||||||
# complain. If we decide to rely on python-request
|
|
||||||
# this wont be necessary anymore.
|
|
||||||
kwargs['headers'] = self.encode_headers(kwargs['headers'])
|
|
||||||
|
|
||||||
try:
|
|
||||||
if self.endpoint_path:
|
|
||||||
url = '%s/%s' % (self.endpoint_path, url)
|
|
||||||
conn_url = posixpath.normpath(url)
|
|
||||||
# Note(flaper87): Ditto, headers / url
|
|
||||||
# encoding to make httplib happy.
|
|
||||||
conn_url = strutils.safe_encode(conn_url)
|
|
||||||
if kwargs['headers'].get('Transfer-Encoding') == 'chunked':
|
|
||||||
conn.putrequest(method, conn_url)
|
|
||||||
for header, value in kwargs['headers'].items():
|
|
||||||
conn.putheader(header, value)
|
|
||||||
conn.endheaders()
|
|
||||||
chunk = kwargs['body'].read(CHUNKSIZE)
|
|
||||||
# Chunk it, baby...
|
|
||||||
while chunk:
|
|
||||||
conn.send('%x\r\n%s\r\n' % (len(chunk), chunk))
|
|
||||||
chunk = kwargs['body'].read(CHUNKSIZE)
|
|
||||||
conn.send('0\r\n\r\n')
|
|
||||||
else:
|
|
||||||
conn.request(method, conn_url, **kwargs)
|
|
||||||
resp = conn.getresponse()
|
|
||||||
except socket.gaierror as e:
|
|
||||||
message = "Error finding address for %s: %s" % (
|
|
||||||
self.endpoint_hostname, e)
|
|
||||||
raise exc.InvalidEndpoint(message=message)
|
|
||||||
except (socket.error, socket.timeout) as e:
|
|
||||||
endpoint = self.endpoint
|
|
||||||
message = "Error communicating with %(endpoint)s %(e)s" % locals()
|
|
||||||
raise exc.CommunicationError(message=message)
|
|
||||||
|
|
||||||
body_iter = ResponseBodyIterator(resp)
|
|
||||||
|
|
||||||
# Read body into string if it isn't obviously image data
|
|
||||||
if resp.getheader('content-type', None) != 'application/octet-stream':
|
|
||||||
body_str = ''.join([chunk for chunk in body_iter])
|
|
||||||
self.log_http_response(resp, body_str)
|
|
||||||
body_iter = StringIO.StringIO(body_str)
|
|
||||||
else:
|
|
||||||
self.log_http_response(resp)
|
|
||||||
|
|
||||||
if 400 <= resp.status < 600:
|
|
||||||
LOG.error("Request returned failure status.")
|
|
||||||
raise exc.from_response(resp, body_str)
|
|
||||||
elif resp.status in (301, 302, 305):
|
|
||||||
# Redirected. Reissue the request to the new location.
|
|
||||||
return self._http_request(resp['location'], method, **kwargs)
|
|
||||||
elif resp.status == 300:
|
|
||||||
raise exc.from_response(resp)
|
|
||||||
|
|
||||||
return resp, body_iter
|
|
||||||
|
|
||||||
def json_request(self, method, url, **kwargs):
|
|
||||||
kwargs.setdefault('headers', {})
|
|
||||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
|
||||||
|
|
||||||
if 'body' in kwargs:
|
|
||||||
kwargs['body'] = json.dumps(kwargs['body'])
|
|
||||||
|
|
||||||
resp, body_iter = self._http_request(url, method, **kwargs)
|
|
||||||
|
|
||||||
if 'application/json' in resp.getheader('content-type', None):
|
|
||||||
body = ''.join([chunk for chunk in body_iter])
|
|
||||||
try:
|
|
||||||
body = json.loads(body)
|
|
||||||
except ValueError:
|
|
||||||
LOG.error('Could not decode response body as JSON')
|
|
||||||
else:
|
|
||||||
body = None
|
|
||||||
|
|
||||||
return resp, body
|
|
||||||
|
|
||||||
def raw_request(self, method, url, **kwargs):
|
|
||||||
kwargs.setdefault('headers', {})
|
|
||||||
kwargs['headers'].setdefault('Content-Type',
|
|
||||||
'application/octet-stream')
|
|
||||||
if 'body' in kwargs:
|
|
||||||
if (hasattr(kwargs['body'], 'read')
|
|
||||||
and method.lower() in ('post', 'put')):
|
|
||||||
# We use 'Transfer-Encoding: chunked' because
|
|
||||||
# body size may not always be known in advance.
|
|
||||||
kwargs['headers']['Transfer-Encoding'] = 'chunked'
|
|
||||||
return self._http_request(url, method, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenSSLConnectionDelegator(object):
|
|
||||||
"""
|
|
||||||
An OpenSSL.SSL.Connection delegator.
|
|
||||||
|
|
||||||
Supplies an additional 'makefile' method which httplib requires
|
|
||||||
and is not present in OpenSSL.SSL.Connection.
|
|
||||||
|
|
||||||
Note: Since it is not possible to inherit from OpenSSL.SSL.Connection
|
|
||||||
a delegator must be used.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.connection = Connection(*args, **kwargs)
|
|
||||||
|
|
||||||
def __getattr__(self, name):
|
|
||||||
return getattr(self.connection, name)
|
|
||||||
|
|
||||||
def makefile(self, *args, **kwargs):
|
|
||||||
# Making sure socket is closed when this file is closed
|
|
||||||
# since we now avoid closing socket on connection close
|
|
||||||
# see new close method under VerifiedHTTPSConnection
|
|
||||||
kwargs['close'] = True
|
|
||||||
|
|
||||||
return socket._fileobject(self.connection, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class VerifiedHTTPSConnection(HTTPSConnection):
|
|
||||||
"""
|
|
||||||
Extended HTTPSConnection which uses the OpenSSL library
|
|
||||||
for enhanced SSL support.
|
|
||||||
Note: Much of this functionality can eventually be replaced
|
|
||||||
with native Python 3.3 code.
|
|
||||||
"""
|
|
||||||
def __init__(self, host, port=None, key_file=None, cert_file=None,
|
|
||||||
cacert=None, timeout=None, insecure=False,
|
|
||||||
ssl_compression=True):
|
|
||||||
HTTPSConnection.__init__(self, host, port,
|
|
||||||
key_file=key_file,
|
|
||||||
cert_file=cert_file)
|
|
||||||
self.key_file = key_file
|
|
||||||
self.cert_file = cert_file
|
|
||||||
self.timeout = timeout
|
|
||||||
self.insecure = insecure
|
|
||||||
self.ssl_compression = ssl_compression
|
|
||||||
self.cacert = cacert
|
|
||||||
self.setcontext()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def host_matches_cert(host, x509):
|
|
||||||
"""
|
|
||||||
Verify that the the x509 certificate we have received
|
|
||||||
from 'host' correctly identifies the server we are
|
|
||||||
connecting to, ie that the certificate's Common Name
|
|
||||||
or a Subject Alternative Name matches 'host'.
|
|
||||||
"""
|
|
||||||
# First see if we can match the CN
|
|
||||||
if x509.get_subject().commonName == host:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Also try Subject Alternative Names for a match
|
|
||||||
san_list = None
|
|
||||||
for i in xrange(x509.get_extension_count()):
|
|
||||||
ext = x509.get_extension(i)
|
|
||||||
if ext.get_short_name() == 'subjectAltName':
|
|
||||||
san_list = str(ext)
|
|
||||||
for san in ''.join(san_list.split()).split(','):
|
|
||||||
if san == "DNS:%s" % host:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Server certificate does not match host
|
|
||||||
msg = ('Host "%s" does not match x509 certificate contents: '
|
|
||||||
'CommonName "%s"' % (host, x509.get_subject().commonName))
|
|
||||||
if san_list is not None:
|
|
||||||
msg = msg + ', subjectAltName "%s"' % san_list
|
|
||||||
raise exc.SSLCertificateError(msg)
|
|
||||||
|
|
||||||
def verify_callback(self, connection, x509, errnum,
|
|
||||||
depth, preverify_ok):
|
|
||||||
# NOTE(leaman): preverify_ok may be a non-boolean type
|
|
||||||
preverify_ok = bool(preverify_ok)
|
|
||||||
if x509.has_expired():
|
|
||||||
msg = "SSL Certificate expired on '%s'" % x509.get_notAfter()
|
|
||||||
raise exc.SSLCertificateError(msg)
|
|
||||||
|
|
||||||
if depth == 0 and preverify_ok:
|
|
||||||
# We verify that the host matches against the last
|
|
||||||
# certificate in the chain
|
|
||||||
return self.host_matches_cert(self.host, x509)
|
|
||||||
else:
|
|
||||||
# Pass through OpenSSL's default result
|
|
||||||
return preverify_ok
|
|
||||||
|
|
||||||
def setcontext(self):
|
|
||||||
"""
|
|
||||||
Set up the OpenSSL context.
|
|
||||||
"""
|
|
||||||
self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD)
|
|
||||||
|
|
||||||
if self.ssl_compression is False:
|
|
||||||
self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION
|
|
||||||
|
|
||||||
if self.insecure is not True:
|
|
||||||
self.context.set_verify(OpenSSL.SSL.VERIFY_PEER,
|
|
||||||
self.verify_callback)
|
|
||||||
else:
|
|
||||||
self.context.set_verify(OpenSSL.SSL.VERIFY_NONE,
|
|
||||||
lambda *args: True)
|
|
||||||
|
|
||||||
if self.cert_file:
|
|
||||||
try:
|
|
||||||
self.context.use_certificate_file(self.cert_file)
|
|
||||||
except Exception as e:
|
|
||||||
msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e)
|
|
||||||
raise exc.SSLConfigurationError(msg)
|
|
||||||
if self.key_file is None:
|
|
||||||
# We support having key and cert in same file
|
|
||||||
try:
|
|
||||||
self.context.use_privatekey_file(self.cert_file)
|
|
||||||
except Exception as e:
|
|
||||||
msg = ('No key file specified and unable to load key '
|
|
||||||
'from "%s" %s' % (self.cert_file, e))
|
|
||||||
raise exc.SSLConfigurationError(msg)
|
|
||||||
|
|
||||||
if self.key_file:
|
|
||||||
try:
|
|
||||||
self.context.use_privatekey_file(self.key_file)
|
|
||||||
except Exception as e:
|
|
||||||
msg = 'Unable to load key from "%s" %s' % (self.key_file, e)
|
|
||||||
raise exc.SSLConfigurationError(msg)
|
|
||||||
|
|
||||||
if self.cacert:
|
|
||||||
try:
|
|
||||||
self.context.load_verify_locations(self.cacert)
|
|
||||||
except Exception as e:
|
|
||||||
msg = 'Unable to load CA from "%s"' % (self.cacert, e)
|
|
||||||
raise exc.SSLConfigurationError(msg)
|
|
||||||
else:
|
|
||||||
self.context.set_default_verify_paths()
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
"""
|
|
||||||
Connect to an SSL port using the OpenSSL library and apply
|
|
||||||
per-connection parameters.
|
|
||||||
"""
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
||||||
if self.timeout is not None:
|
|
||||||
# '0' microseconds
|
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO,
|
|
||||||
struct.pack('fL', self.timeout, 0))
|
|
||||||
self.sock = OpenSSLConnectionDelegator(self.context, sock)
|
|
||||||
self.sock.connect((self.host, self.port))
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
if self.sock:
|
|
||||||
# Removing reference to socket but don't close it yet.
|
|
||||||
# Response close will close both socket and associated
|
|
||||||
# file. Closing socket too soon will cause response
|
|
||||||
# reads to fail with socket IO error 'Bad file descriptor'.
|
|
||||||
self.sock = None
|
|
||||||
|
|
||||||
# Calling close on HTTPConnection to continue doing that cleanup.
|
|
||||||
HTTPSConnection.close(self)
|
|
||||||
|
|
||||||
|
|
||||||
class ResponseBodyIterator(object):
|
|
||||||
"""
|
|
||||||
A class that acts as an iterator over an HTTP response.
|
|
||||||
|
|
||||||
This class will also check response body integrity when iterating over
|
|
||||||
the instance and if a checksum was supplied using `set_checksum` method,
|
|
||||||
else by default the class will not do any integrity check.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, resp):
|
|
||||||
self._resp = resp
|
|
||||||
self._checksum = None
|
|
||||||
self._size = int(resp.getheader('content-length', 0))
|
|
||||||
self._end_reached = False
|
|
||||||
|
|
||||||
def set_checksum(self, checksum):
|
|
||||||
"""
|
|
||||||
Set checksum to check against when iterating over this instance.
|
|
||||||
|
|
||||||
:raise: AttributeError if iterator is already consumed.
|
|
||||||
"""
|
|
||||||
if self._end_reached:
|
|
||||||
raise AttributeError("Can't set checksum for an already consumed"
|
|
||||||
" iterator")
|
|
||||||
self._checksum = checksum
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return int(self._size)
|
|
||||||
|
|
||||||
def __iter__(self):
|
|
||||||
md5sum = hashlib.md5()
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
chunk = self.next()
|
|
||||||
except StopIteration:
|
|
||||||
self._end_reached = True
|
|
||||||
# NOTE(mouad): Check image integrity when the end of response
|
|
||||||
# body is reached.
|
|
||||||
md5sum = md5sum.hexdigest()
|
|
||||||
if self._checksum is not None and md5sum != self._checksum:
|
|
||||||
raise IOError(errno.EPIPE,
|
|
||||||
'Corrupted image. Checksum was %s '
|
|
||||||
'expected %s' % (md5sum, self._checksum))
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
yield chunk
|
|
||||||
md5sum.update(chunk)
|
|
||||||
|
|
||||||
def next(self):
|
|
||||||
chunk = self._resp.read(CHUNKSIZE)
|
|
||||||
if chunk:
|
|
||||||
return chunk
|
|
||||||
else:
|
|
||||||
raise StopIteration()
|
|
@ -1,132 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import uuid
|
|
||||||
|
|
||||||
import os
|
|
||||||
from metadataclient.common import exceptions
|
|
||||||
import prettytable
|
|
||||||
from metadataclient.openstack.common import importutils
|
|
||||||
|
|
||||||
|
|
||||||
# Decorator for cli-args
|
|
||||||
def arg(*args, **kwargs):
|
|
||||||
def _decorator(func):
|
|
||||||
# Because of the sematics of decorator composition if we just append
|
|
||||||
# to the options list positional options will appear to be backwards.
|
|
||||||
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
|
||||||
return func
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
|
|
||||||
def pretty_choice_list(l):
|
|
||||||
return ', '.join("'%s'" % i for i in l)
|
|
||||||
|
|
||||||
|
|
||||||
def print_list(objs, fields, field_labels, formatters={}, sortby=0):
|
|
||||||
pt = prettytable.PrettyTable([f for f in field_labels], caching=False)
|
|
||||||
pt.align = 'l'
|
|
||||||
|
|
||||||
for o in objs:
|
|
||||||
row = []
|
|
||||||
for field in fields:
|
|
||||||
if field in formatters:
|
|
||||||
row.append(formatters[field](o))
|
|
||||||
else:
|
|
||||||
data = getattr(o, field, None) or ''
|
|
||||||
row.append(data)
|
|
||||||
pt.add_row(row)
|
|
||||||
print pt.get_string(sortby=field_labels[sortby])
|
|
||||||
|
|
||||||
|
|
||||||
def print_dict(d, formatters={}):
|
|
||||||
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
|
|
||||||
pt.align = 'l'
|
|
||||||
|
|
||||||
for field in d.keys():
|
|
||||||
if field in formatters:
|
|
||||||
pt.add_row([field, formatters[field](d[field])])
|
|
||||||
else:
|
|
||||||
pt.add_row([field, d[field]])
|
|
||||||
print pt.get_string(sortby='Property')
|
|
||||||
|
|
||||||
|
|
||||||
def find_resource(manager, name_or_id):
|
|
||||||
"""Helper for the _find_* methods."""
|
|
||||||
# first try to get entity as integer id
|
|
||||||
try:
|
|
||||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
|
||||||
return manager.get(int(name_or_id))
|
|
||||||
except exceptions.NotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# now try to get entity as uuid
|
|
||||||
try:
|
|
||||||
uuid.UUID(str(name_or_id))
|
|
||||||
return manager.get(name_or_id)
|
|
||||||
except (ValueError, exceptions.NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# finally try to find entity by name
|
|
||||||
try:
|
|
||||||
return manager.find(name=name_or_id)
|
|
||||||
except exceptions.NotFound:
|
|
||||||
msg = "No %s with a name or ID of '%s' exists." % \
|
|
||||||
(manager.resource_class.__name__.lower(), name_or_id)
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def string_to_bool(arg):
|
|
||||||
return arg.strip().lower() in ('t', 'true', 'yes', '1')
|
|
||||||
|
|
||||||
|
|
||||||
def env(*vars, **kwargs):
|
|
||||||
"""Search for the first defined of possibly many env vars
|
|
||||||
|
|
||||||
Returns the first environment variable defined in vars, or
|
|
||||||
returns the default defined in kwargs.
|
|
||||||
"""
|
|
||||||
for v in vars:
|
|
||||||
value = os.environ.get(v, None)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return kwargs.get('default', '')
|
|
||||||
|
|
||||||
|
|
||||||
def import_versioned_module(version, submodule=None):
|
|
||||||
module = 'muranoclient.v%s' % version
|
|
||||||
if submodule:
|
|
||||||
module = '.'.join((module, submodule))
|
|
||||||
return importutils.import_module(module)
|
|
||||||
|
|
||||||
|
|
||||||
def exit(msg=''):
|
|
||||||
if msg:
|
|
||||||
print >> sys.stderr, msg
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def getsockopt(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
A function which allows us to monkey patch eventlet's
|
|
||||||
GreenSocket, adding a required 'getsockopt' method.
|
|
||||||
TODO: (mclaren) we can remove this once the eventlet fix
|
|
||||||
(https://bitbucket.org/eventlet/eventlet/commits/609f230)
|
|
||||||
lands in mainstream packages.
|
|
||||||
NOTE: Already in 0.13, but we can't be sure that all clients
|
|
||||||
that use python client also use newest eventlet
|
|
||||||
"""
|
|
||||||
return self.fd.getsockopt(*args, **kwargs)
|
|
@ -1,178 +0,0 @@
|
|||||||
# Copyright 2012 OpenStack LLC.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
class BaseException(Exception):
|
|
||||||
"""An error occurred."""
|
|
||||||
def __init__(self, message=None):
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.message or self.__class__.__doc__
|
|
||||||
|
|
||||||
|
|
||||||
class CommandError(BaseException):
|
|
||||||
"""Invalid usage of CLI."""
|
|
||||||
|
|
||||||
|
|
||||||
class InvalidEndpoint(BaseException):
|
|
||||||
"""The provided endpoint is invalid."""
|
|
||||||
|
|
||||||
|
|
||||||
class CommunicationError(BaseException):
|
|
||||||
"""Unable to communicate with server."""
|
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPException(ClientException):
|
|
||||||
"""Base exception for all HTTP-derived exceptions."""
|
|
||||||
code = 'N/A'
|
|
||||||
|
|
||||||
def __init__(self, details=None):
|
|
||||||
self.details = details or self.__class__.__name__
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "%s (HTTP %s)" % (self.details, self.code)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMultipleChoices(HTTPException):
|
|
||||||
code = 300
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
self.details = ("Requested version of OpenStack Images API is not"
|
|
||||||
"available.")
|
|
||||||
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
|
|
||||||
self.details)
|
|
||||||
|
|
||||||
|
|
||||||
class BadRequest(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 400
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPBadRequest(BadRequest):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 401
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPUnauthorized(Unauthorized):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 403
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPForbidden(Forbidden):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 404
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPNotFound(NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPMethodNotAllowed(HTTPException):
|
|
||||||
code = 405
|
|
||||||
|
|
||||||
|
|
||||||
class Conflict(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 409
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPConflict(Conflict):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class OverLimit(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 413
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPOverLimit(OverLimit):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPInternalServerError(HTTPException):
|
|
||||||
code = 500
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPNotImplemented(HTTPException):
|
|
||||||
code = 501
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPBadGateway(HTTPException):
|
|
||||||
code = 502
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceUnavailable(HTTPException):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
code = 503
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPServiceUnavailable(ServiceUnavailable):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
#NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
|
|
||||||
# classes
|
|
||||||
_code_map = {}
|
|
||||||
for obj_name in dir(sys.modules[__name__]):
|
|
||||||
if obj_name.startswith('HTTP'):
|
|
||||||
obj = getattr(sys.modules[__name__], obj_name)
|
|
||||||
_code_map[obj.code] = obj
|
|
||||||
|
|
||||||
|
|
||||||
def from_response(response, body=None):
|
|
||||||
"""Return an instance of an HTTPException based on httplib response."""
|
|
||||||
cls = _code_map.get(response.status, HTTPException)
|
|
||||||
if body:
|
|
||||||
details = body.replace('\n\n', '\n')
|
|
||||||
return cls(details=details)
|
|
||||||
|
|
||||||
return cls()
|
|
||||||
|
|
||||||
|
|
||||||
class NoTokenLookupException(Exception):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointNotFound(Exception):
|
|
||||||
"""DEPRECATED!"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SSLConfigurationError(BaseException):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class SSLCertificateError(BaseException):
|
|
||||||
pass
|
|
@ -1,50 +0,0 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright 2012 Red Hat, 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
gettext for openstack-common modules.
|
|
||||||
|
|
||||||
Usual usage in an openstack.common module:
|
|
||||||
|
|
||||||
from glanceclient.openstack.common.gettextutils import _
|
|
||||||
"""
|
|
||||||
|
|
||||||
import gettext
|
|
||||||
import os
|
|
||||||
|
|
||||||
_localedir = os.environ.get('glanceclient'.upper() + '_LOCALEDIR')
|
|
||||||
_t = gettext.translation('glanceclient', localedir=_localedir, fallback=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _(msg):
|
|
||||||
return _t.ugettext(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def install(domain):
|
|
||||||
"""Install a _() function using the given translation domain.
|
|
||||||
|
|
||||||
Given a translation domain, install a _() function using gettext's
|
|
||||||
install() function.
|
|
||||||
|
|
||||||
The main difference from gettext.install() is that we allow
|
|
||||||
overriding the default localedir (e.g. /usr/share/locale) using
|
|
||||||
a translation-domain-specific environment variable (e.g.
|
|
||||||
NOVA_LOCALEDIR).
|
|
||||||
"""
|
|
||||||
gettext.install(domain,
|
|
||||||
localedir=os.environ.get(domain.upper() + '_LOCALEDIR'),
|
|
||||||
unicode=True)
|
|
@ -1,67 +0,0 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Import related utilities and helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
|
|
||||||
def import_class(import_str):
|
|
||||||
"""Returns a class from a string including module and class."""
|
|
||||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
|
||||||
try:
|
|
||||||
__import__(mod_str)
|
|
||||||
return getattr(sys.modules[mod_str], class_str)
|
|
||||||
except (ValueError, AttributeError):
|
|
||||||
raise ImportError('Class %s cannot be found (%s)' %
|
|
||||||
(class_str,
|
|
||||||
traceback.format_exception(*sys.exc_info())))
|
|
||||||
|
|
||||||
|
|
||||||
def import_object(import_str, *args, **kwargs):
|
|
||||||
"""Import a class and return an instance of it."""
|
|
||||||
return import_class(import_str)(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def import_object_ns(name_space, import_str, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Import a class and return an instance of it, first by trying
|
|
||||||
to find the class in a default namespace, then failing back to
|
|
||||||
a full path if not found in the default namespace.
|
|
||||||
"""
|
|
||||||
import_value = "%s.%s" % (name_space, import_str)
|
|
||||||
try:
|
|
||||||
return import_class(import_value)(*args, **kwargs)
|
|
||||||
except ImportError:
|
|
||||||
return import_class(import_str)(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def import_module(import_str):
|
|
||||||
"""Import a module."""
|
|
||||||
__import__(import_str)
|
|
||||||
return sys.modules[import_str]
|
|
||||||
|
|
||||||
|
|
||||||
def try_import(import_str, default=None):
|
|
||||||
"""Try to import a module and if it fails return default."""
|
|
||||||
try:
|
|
||||||
return import_module(import_str)
|
|
||||||
except ImportError:
|
|
||||||
return default
|
|
@ -1,150 +0,0 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
System-level utilities and helper functions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from metadataclient.openstack.common.gettextutils import _
|
|
||||||
|
|
||||||
|
|
||||||
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
|
|
||||||
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
|
|
||||||
|
|
||||||
|
|
||||||
def int_from_bool_as_string(subject):
|
|
||||||
"""
|
|
||||||
Interpret a string as a boolean and return either 1 or 0.
|
|
||||||
|
|
||||||
Any string value in:
|
|
||||||
|
|
||||||
('True', 'true', 'On', 'on', '1')
|
|
||||||
|
|
||||||
is interpreted as a boolean True.
|
|
||||||
|
|
||||||
Useful for JSON-decoded stuff and config file parsing
|
|
||||||
"""
|
|
||||||
return bool_from_string(subject) and 1 or 0
|
|
||||||
|
|
||||||
|
|
||||||
def bool_from_string(subject, strict=False):
|
|
||||||
"""
|
|
||||||
Interpret a string as a boolean.
|
|
||||||
|
|
||||||
A case-insensitive match is performed such that strings matching 't',
|
|
||||||
'true', 'on', 'y', 'yes', or '1' are considered True and, when
|
|
||||||
`strict=False`, anything else is considered False.
|
|
||||||
|
|
||||||
Useful for JSON-decoded stuff and config file parsing.
|
|
||||||
|
|
||||||
If `strict=True`, unrecognized values, including None, will raise a
|
|
||||||
ValueError which is useful when parsing values passed in from an API call.
|
|
||||||
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
|
|
||||||
"""
|
|
||||||
if not isinstance(subject, basestring):
|
|
||||||
subject = str(subject)
|
|
||||||
|
|
||||||
lowered = subject.strip().lower()
|
|
||||||
|
|
||||||
if lowered in TRUE_STRINGS:
|
|
||||||
return True
|
|
||||||
elif lowered in FALSE_STRINGS:
|
|
||||||
return False
|
|
||||||
elif strict:
|
|
||||||
acceptable = ', '.join(
|
|
||||||
"'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
|
|
||||||
msg = _("Unrecognized value '%(val)s', acceptable values are:"
|
|
||||||
" %(acceptable)s") % {'val': subject,
|
|
||||||
'acceptable': acceptable}
|
|
||||||
raise ValueError(msg)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def safe_decode(text, incoming=None, errors='strict'):
|
|
||||||
"""
|
|
||||||
Decodes incoming str using `incoming` if they're
|
|
||||||
not already unicode.
|
|
||||||
|
|
||||||
:param incoming: Text's current encoding
|
|
||||||
:param errors: Errors handling policy. See here for valid
|
|
||||||
values http://docs.python.org/2/library/codecs.html
|
|
||||||
:returns: text or a unicode `incoming` encoded
|
|
||||||
representation of it.
|
|
||||||
:raises TypeError: If text is not an isntance of basestring
|
|
||||||
"""
|
|
||||||
if not isinstance(text, basestring):
|
|
||||||
raise TypeError("%s can't be decoded" % type(text))
|
|
||||||
|
|
||||||
if isinstance(text, unicode):
|
|
||||||
return text
|
|
||||||
|
|
||||||
if not incoming:
|
|
||||||
incoming = (sys.stdin.encoding or
|
|
||||||
sys.getdefaultencoding())
|
|
||||||
|
|
||||||
try:
|
|
||||||
return text.decode(incoming, errors)
|
|
||||||
except UnicodeDecodeError:
|
|
||||||
# Note(flaper87) If we get here, it means that
|
|
||||||
# sys.stdin.encoding / sys.getdefaultencoding
|
|
||||||
# didn't return a suitable encoding to decode
|
|
||||||
# text. This happens mostly when global LANG
|
|
||||||
# var is not set correctly and there's no
|
|
||||||
# default encoding. In this case, most likely
|
|
||||||
# python will use ASCII or ANSI encoders as
|
|
||||||
# default encodings but they won't be capable
|
|
||||||
# of decoding non-ASCII characters.
|
|
||||||
#
|
|
||||||
# Also, UTF-8 is being used since it's an ASCII
|
|
||||||
# extension.
|
|
||||||
return text.decode('utf-8', errors)
|
|
||||||
|
|
||||||
|
|
||||||
def safe_encode(text, incoming=None,
|
|
||||||
encoding='utf-8', errors='strict'):
|
|
||||||
"""
|
|
||||||
Encodes incoming str/unicode using `encoding`. If
|
|
||||||
incoming is not specified, text is expected to
|
|
||||||
be encoded with current python's default encoding.
|
|
||||||
(`sys.getdefaultencoding`)
|
|
||||||
|
|
||||||
:param incoming: Text's current encoding
|
|
||||||
:param encoding: Expected encoding for text (Default UTF-8)
|
|
||||||
:param errors: Errors handling policy. See here for valid
|
|
||||||
values http://docs.python.org/2/library/codecs.html
|
|
||||||
:returns: text or a bytestring `encoding` encoded
|
|
||||||
representation of it.
|
|
||||||
:raises TypeError: If text is not an isntance of basestring
|
|
||||||
"""
|
|
||||||
if not isinstance(text, basestring):
|
|
||||||
raise TypeError("%s can't be encoded" % type(text))
|
|
||||||
|
|
||||||
if not incoming:
|
|
||||||
incoming = (sys.stdin.encoding or
|
|
||||||
sys.getdefaultencoding())
|
|
||||||
|
|
||||||
if isinstance(text, unicode):
|
|
||||||
return text.encode(encoding, errors)
|
|
||||||
elif text and encoding != incoming:
|
|
||||||
# Decode text before encoding it with `encoding`
|
|
||||||
text = safe_decode(text, incoming, errors)
|
|
||||||
return text.encode(encoding, errors)
|
|
||||||
|
|
||||||
return text
|
|
@ -1,322 +0,0 @@
|
|||||||
# Copyright (c) 2013 Mirantis, 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Command-line interface to the Murano Metadata Repository API.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
|
||||||
import sys
|
|
||||||
import httplib2
|
|
||||||
from keystoneclient.v2_0 import client as ksclient
|
|
||||||
|
|
||||||
import metadataclient
|
|
||||||
from metadataclient.common import exceptions
|
|
||||||
from metadataclient.common import utils
|
|
||||||
|
|
||||||
|
|
||||||
class MuranoRepositoryShell(object):
|
|
||||||
|
|
||||||
def get_base_parser(self):
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
prog='muranorepository',
|
|
||||||
description=__doc__.strip(),
|
|
||||||
epilog='See "metadata help COMMAND" '
|
|
||||||
'for help on a specific command.',
|
|
||||||
add_help=False,
|
|
||||||
formatter_class=HelpFormatter,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Global arguments
|
|
||||||
parser.add_argument('-h', '--help',
|
|
||||||
action='store_true',
|
|
||||||
help=argparse.SUPPRESS,
|
|
||||||
)
|
|
||||||
|
|
||||||
parser.add_argument('-d', '--debug',
|
|
||||||
default=bool(utils.env('METADATACLIENT_DEBUG')),
|
|
||||||
action='store_true',
|
|
||||||
help='Defaults to env[METADATACLIENT_DEBUG]')
|
|
||||||
|
|
||||||
parser.add_argument('-v', '--verbose',
|
|
||||||
default=False, action="store_true",
|
|
||||||
help="Print more verbose output")
|
|
||||||
|
|
||||||
parser.add_argument('--get-schema',
|
|
||||||
default=False, action="store_true",
|
|
||||||
dest='get_schema',
|
|
||||||
help='Force retrieving the schema used to generate'
|
|
||||||
' portions of the help text rather than using'
|
|
||||||
' a cached copy. Ignored with api version 1')
|
|
||||||
|
|
||||||
parser.add_argument('-k', '--insecure',
|
|
||||||
default=False,
|
|
||||||
action='store_true',
|
|
||||||
help='Explicitly allow metadataclient to perform '
|
|
||||||
'\"insecure SSL\" (https) requests. The server\'s '
|
|
||||||
'certificate will not be verified against any '
|
|
||||||
'certificate authorities. This option should '
|
|
||||||
'be used with caution.')
|
|
||||||
|
|
||||||
parser.add_argument('--cert-file',
|
|
||||||
help='Path of certificate file to use in SSL '
|
|
||||||
'connection. This file can optionally be '
|
|
||||||
'prepended with the private key.')
|
|
||||||
|
|
||||||
parser.add_argument('--key-file',
|
|
||||||
help='Path of client key to use in SSL '
|
|
||||||
'connection. This option is not necessary '
|
|
||||||
'if your key is prepended to your cert file.')
|
|
||||||
|
|
||||||
parser.add_argument('--os-cacert',
|
|
||||||
metavar='<ca-certificate-file>',
|
|
||||||
dest='os_cacert',
|
|
||||||
default=utils.env('OS_CACERT'),
|
|
||||||
help='Path of CA TLS certificate(s) used to '
|
|
||||||
'verify the remote server\'s certificate. '
|
|
||||||
'Without this option metadata looks for the '
|
|
||||||
'default system CA certificates.')
|
|
||||||
|
|
||||||
parser.add_argument('--ca-file',
|
|
||||||
dest='os_cacert',
|
|
||||||
help='DEPRECATED! Use --os-cacert.')
|
|
||||||
|
|
||||||
parser.add_argument('--timeout',
|
|
||||||
default=600,
|
|
||||||
help='Number of seconds to wait for a response')
|
|
||||||
|
|
||||||
parser.add_argument('--os-tenant-id',
|
|
||||||
default=utils.env('OS_TENANT_ID'),
|
|
||||||
help='Defaults to env[OS_TENANT_ID]')
|
|
||||||
|
|
||||||
parser.add_argument('--os-tenant-name',
|
|
||||||
default=utils.env('OS_TENANT_NAME'),
|
|
||||||
help='Defaults to env[OS_TENANT_NAME]')
|
|
||||||
|
|
||||||
parser.add_argument('--os-auth-url',
|
|
||||||
default=utils.env('OS_AUTH_URL'),
|
|
||||||
help='Defaults to env[OS_AUTH_URL]')
|
|
||||||
|
|
||||||
parser.add_argument('--os-region-name',
|
|
||||||
default=utils.env('OS_REGION_NAME'),
|
|
||||||
help='Defaults to env[OS_REGION_NAME]')
|
|
||||||
|
|
||||||
parser.add_argument('--os-auth-token',
|
|
||||||
default=utils.env('OS_AUTH_TOKEN'),
|
|
||||||
help='Defaults to env[OS_AUTH_TOKEN]')
|
|
||||||
|
|
||||||
parser.add_argument('--os-service-type',
|
|
||||||
default=utils.env('OS_SERVICE_TYPE'),
|
|
||||||
help='Defaults to env[OS_SERVICE_TYPE]')
|
|
||||||
|
|
||||||
parser.add_argument('--os-endpoint-type',
|
|
||||||
default=utils.env('OS_ENDPOINT_TYPE'),
|
|
||||||
help='Defaults to env[OS_ENDPOINT_TYPE]')
|
|
||||||
|
|
||||||
parser.add_argument('--murano-metadata-url',
|
|
||||||
default=utils.env('MURANO_METADATA_URL'),
|
|
||||||
help='Defaults to env[MURANO_METADATA_URL]')
|
|
||||||
|
|
||||||
parser.add_argument('--murano_metadata-api-version',
|
|
||||||
default=utils.env(
|
|
||||||
'MURANO_METADATA_API_VERSION', default='1'),
|
|
||||||
help='Defaults to env[MURANO_METADATA_API_VERSION]'
|
|
||||||
' or 1')
|
|
||||||
return parser
|
|
||||||
|
|
||||||
def get_subcommand_parser(self, version):
|
|
||||||
parser = self.get_base_parser()
|
|
||||||
|
|
||||||
self.subcommands = {}
|
|
||||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
|
||||||
submodule = utils.import_versioned_module(version, 'shell')
|
|
||||||
self._find_actions(subparsers, submodule)
|
|
||||||
self._find_actions(subparsers, self)
|
|
||||||
|
|
||||||
return parser
|
|
||||||
|
|
||||||
def _find_actions(self, subparsers, actions_module):
|
|
||||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
|
||||||
# I prefer to be hypen-separated instead of underscores.
|
|
||||||
command = attr[3:].replace('_', '-')
|
|
||||||
callback = getattr(actions_module, attr)
|
|
||||||
desc = callback.__doc__ or ''
|
|
||||||
help = desc.strip().split('\n')[0]
|
|
||||||
arguments = getattr(callback, 'arguments', [])
|
|
||||||
|
|
||||||
subparser = subparsers.add_parser(command,
|
|
||||||
help=help,
|
|
||||||
description=desc,
|
|
||||||
add_help=False,
|
|
||||||
formatter_class=HelpFormatter
|
|
||||||
)
|
|
||||||
subparser.add_argument('-h', '--help',
|
|
||||||
action='help',
|
|
||||||
help=argparse.SUPPRESS,
|
|
||||||
)
|
|
||||||
self.subcommands[command] = subparser
|
|
||||||
for (args, kwargs) in arguments:
|
|
||||||
subparser.add_argument(*args, **kwargs)
|
|
||||||
subparser.set_defaults(func=callback)
|
|
||||||
|
|
||||||
def _get_ksclient(self, **kwargs):
|
|
||||||
"""Get an endpoint and auth token from Keystone.
|
|
||||||
|
|
||||||
:param username: name of user
|
|
||||||
:param password: user's password
|
|
||||||
:param tenant_id: unique identifier of tenant
|
|
||||||
:param tenant_name: name of tenant
|
|
||||||
:param auth_url: endpoint to authenticate against
|
|
||||||
"""
|
|
||||||
return ksclient.Client(username=kwargs.get('username'),
|
|
||||||
password=kwargs.get('password'),
|
|
||||||
tenant_id=kwargs.get('tenant_id'),
|
|
||||||
tenant_name=kwargs.get('tenant_name'),
|
|
||||||
auth_url=kwargs.get('auth_url'),
|
|
||||||
insecure=kwargs.get('insecure'))
|
|
||||||
|
|
||||||
def _get_endpoint(self, client, **kwargs):
|
|
||||||
"""Get an endpoint using the provided keystone client."""
|
|
||||||
return client.service_catalog.url_for(
|
|
||||||
service_type=kwargs.get('service_type') or 'metering',
|
|
||||||
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
|
|
||||||
|
|
||||||
def _setup_debugging(self, debug):
|
|
||||||
if debug:
|
|
||||||
logging.basicConfig(
|
|
||||||
format="%(levelname)s (%(module)s:%(lineno)d) %(message)s",
|
|
||||||
level=logging.DEBUG)
|
|
||||||
|
|
||||||
httplib2.debuglevel = 1
|
|
||||||
else:
|
|
||||||
logging.basicConfig(
|
|
||||||
format="%(levelname)s %(message)s",
|
|
||||||
level=logging.INFO)
|
|
||||||
|
|
||||||
def main(self, argv):
|
|
||||||
# Parse args once to find version
|
|
||||||
parser = self.get_base_parser()
|
|
||||||
(options, args) = parser.parse_known_args(argv)
|
|
||||||
self._setup_debugging(options.debug)
|
|
||||||
|
|
||||||
# build available subcommands based on version
|
|
||||||
api_version = options.murano_metadata_api_version
|
|
||||||
subcommand_parser = self.get_subcommand_parser(api_version)
|
|
||||||
self.parser = subcommand_parser
|
|
||||||
|
|
||||||
# Handle top-level --help/-h before attempting to parse
|
|
||||||
# a command off the command line
|
|
||||||
if options.help or not argv:
|
|
||||||
self.do_help(options)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# Parse args again and call whatever callback was selected
|
|
||||||
args = subcommand_parser.parse_args(argv)
|
|
||||||
|
|
||||||
# Short-circuit and deal with help command right away.
|
|
||||||
if args.func == self.do_help:
|
|
||||||
self.do_help(args)
|
|
||||||
return 0
|
|
||||||
|
|
||||||
if args.os_auth_token and args.murano_metadata_url:
|
|
||||||
token = args.os_auth_token
|
|
||||||
endpoint = args.murano_metadata_url
|
|
||||||
else:
|
|
||||||
if not args.os_username:
|
|
||||||
raise exceptions.CommandError("You must provide a username "
|
|
||||||
"via either --os-username "
|
|
||||||
"or via env[OS_USERNAME]")
|
|
||||||
|
|
||||||
if not args.os_password:
|
|
||||||
raise exceptions.CommandError("You must provide a password "
|
|
||||||
"via either --os-password "
|
|
||||||
"or via env[OS_PASSWORD]")
|
|
||||||
|
|
||||||
if not (args.os_tenant_id or args.os_tenant_name):
|
|
||||||
raise exceptions.CommandError("You must provide a tenant_id "
|
|
||||||
"via either --os-tenant-id "
|
|
||||||
"or via env[OS_TENANT_ID]")
|
|
||||||
|
|
||||||
if not args.os_auth_url:
|
|
||||||
raise exceptions.CommandError("You must provide an auth url "
|
|
||||||
"via either --os-auth-url or "
|
|
||||||
"via env[OS_AUTH_URL]")
|
|
||||||
kwargs = {
|
|
||||||
'username': args.os_username,
|
|
||||||
'password': args.os_password,
|
|
||||||
'tenant_id': args.os_tenant_id,
|
|
||||||
'tenant_name': args.os_tenant_name,
|
|
||||||
'auth_url': args.os_auth_url,
|
|
||||||
'service_type': args.os_service_type,
|
|
||||||
'endpoint_type': args.os_endpoint_type,
|
|
||||||
'insecure': args.insecure
|
|
||||||
}
|
|
||||||
_ksclient = self._get_ksclient(**kwargs)
|
|
||||||
token = args.os_auth_token or _ksclient.auth_token
|
|
||||||
|
|
||||||
url = args.murano_metadata_url
|
|
||||||
endpoint = url or self._get_endpoint(_ksclient, **kwargs)
|
|
||||||
|
|
||||||
kwargs = {
|
|
||||||
'token': token,
|
|
||||||
'insecure': args.insecure,
|
|
||||||
'timeout': args.timeout,
|
|
||||||
'ca_file': args.ca_file,
|
|
||||||
'cert_file': args.cert_file,
|
|
||||||
'key_file': args.key_file,
|
|
||||||
}
|
|
||||||
|
|
||||||
client = metadataclient.Client(api_version, endpoint, **kwargs)
|
|
||||||
|
|
||||||
try:
|
|
||||||
args.func(client, args)
|
|
||||||
except exceptions.Unauthorized:
|
|
||||||
msg = "Invalid OpenStack Identity credentials."
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
|
|
||||||
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
|
||||||
help='Display help for <subcommand>')
|
|
||||||
def do_help(self, args):
|
|
||||||
"""
|
|
||||||
Display help about this program or one of its subcommands.
|
|
||||||
"""
|
|
||||||
if getattr(args, 'command', None):
|
|
||||||
if args.command in self.subcommands:
|
|
||||||
self.subcommands[args.command].print_help()
|
|
||||||
else:
|
|
||||||
msg = "'%s' is not a valid subcommand"
|
|
||||||
raise exceptions.CommandError(msg % args.command)
|
|
||||||
else:
|
|
||||||
self.parser.print_help()
|
|
||||||
|
|
||||||
|
|
||||||
class HelpFormatter(argparse.HelpFormatter):
|
|
||||||
def start_section(self, heading):
|
|
||||||
# Title-case the headings
|
|
||||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
|
||||||
super(HelpFormatter, self).start_section(heading)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
try:
|
|
||||||
MuranoRepositoryShell().main(sys.argv[1:])
|
|
||||||
|
|
||||||
except Exception, e:
|
|
||||||
print >> sys.stderr, e
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
@ -1,15 +0,0 @@
|
|||||||
# Copyright (c) 2013 Mirantis, 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.
|
|
||||||
|
|
||||||
from metadataclient.v1.client import Client
|
|
@ -1,36 +0,0 @@
|
|||||||
# Copyright (c) 2013 Mirantis, 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.
|
|
||||||
|
|
||||||
|
|
||||||
from metadataclient.common import http
|
|
||||||
from metadataclient.v1 import metadata_client
|
|
||||||
from metadataclient.v1 import metadata_admin
|
|
||||||
|
|
||||||
|
|
||||||
class Client(http.HTTPClient):
|
|
||||||
"""Client for the Murano Metadata Repository v1 API.
|
|
||||||
|
|
||||||
:param string endpoint: A user-supplied endpoint URL for the glance
|
|
||||||
service.
|
|
||||||
:param string token: Token for authentication.
|
|
||||||
:param integer timeout: Allows customization of the timeout for client
|
|
||||||
http requests. (optional)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initialize a new client for the Murano Metadata Client v1 API."""
|
|
||||||
super(Client, self).__init__(*args, **kwargs)
|
|
||||||
self.http_client = http.HTTPClient(*args, **kwargs)
|
|
||||||
self.metadata_client = metadata_client.Controller(self)
|
|
||||||
self.metadata_admin = metadata_admin.Controller(self)
|
|
@ -1,234 +0,0 @@
|
|||||||
# Copyright (c) 2013 Mirantis, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
import StringIO
|
|
||||||
import types
|
|
||||||
from os.path import dirname, basename
|
|
||||||
|
|
||||||
from metadataclient import exc
|
|
||||||
from urllib import quote, urlencode
|
|
||||||
|
|
||||||
|
|
||||||
class Wrapper(object):
|
|
||||||
def __init__(self, entity_id, **kwargs):
|
|
||||||
self.id = entity_id
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if isinstance(self.id, types.StringTypes):
|
|
||||||
return self.id.replace('##', '/')
|
|
||||||
else:
|
|
||||||
return str(self.id)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return 'RowWrapper({0})'.format(str(self))
|
|
||||||
|
|
||||||
|
|
||||||
class Controller(object):
|
|
||||||
def __init__(self, http_client):
|
|
||||||
self.http_client = http_client
|
|
||||||
|
|
||||||
def list_services(self):
|
|
||||||
resp, body = self.http_client.json_request('GET', '/admin/services')
|
|
||||||
services = body.get('services', None)
|
|
||||||
if services is not None:
|
|
||||||
return [Wrapper(service['full_service_name'], **service)
|
|
||||||
for service in services]
|
|
||||||
else:
|
|
||||||
raise exc.HTTPInternalServerError()
|
|
||||||
|
|
||||||
def get_service_files(self, data_type=None, service=None):
|
|
||||||
all_files = []
|
|
||||||
|
|
||||||
def get_files(_data_type):
|
|
||||||
included_files = {}
|
|
||||||
if service:
|
|
||||||
resp, body = self.http_client.json_request(
|
|
||||||
'GET',
|
|
||||||
'/admin/services/{service}'.format(service=service))
|
|
||||||
for path in body.get(_data_type, []):
|
|
||||||
included_files[path] = True
|
|
||||||
|
|
||||||
resp, body = self.http_client.json_request(
|
|
||||||
'GET', '/admin/{data_type}'.format(data_type=_data_type))
|
|
||||||
files = body.get(_data_type, [])
|
|
||||||
|
|
||||||
return [Wrapper('{0}##{1}'.format(_data_type, path),
|
|
||||||
path=dirname(path), filename=basename(path),
|
|
||||||
selected=included_files.get(path, False),
|
|
||||||
data_type=_data_type)
|
|
||||||
for path in files]
|
|
||||||
|
|
||||||
if data_type:
|
|
||||||
all_files.extend(get_files(data_type))
|
|
||||||
else:
|
|
||||||
# FixME: need to get list of data types directly from server
|
|
||||||
for data_type in ('ui', 'workflows', 'heat', 'agent', 'scripts'):
|
|
||||||
all_files.extend(get_files(data_type))
|
|
||||||
|
|
||||||
return all_files
|
|
||||||
|
|
||||||
def get_service_info(self, service):
|
|
||||||
resp, body = self.http_client.json_request(
|
|
||||||
'GET', '/admin/services/{service}/info'.format(service=service))
|
|
||||||
return body
|
|
||||||
|
|
||||||
def download_service(self, service):
|
|
||||||
resp, body = self.http_client.raw_request(
|
|
||||||
'GET', '/client/services/{service}'.format(service=service))
|
|
||||||
return body
|
|
||||||
|
|
||||||
def upload_service(self, data):
|
|
||||||
resp, body = self.http_client.raw_request(
|
|
||||||
'POST', '/admin/services/', body=data)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def delete_service(self, service):
|
|
||||||
resp, body = self.http_client.raw_request(
|
|
||||||
'DELETE', '/admin/services/{service}'.format(service=service))
|
|
||||||
return body
|
|
||||||
|
|
||||||
def toggle_enabled(self, service):
|
|
||||||
resp, body = self.http_client.raw_request(
|
|
||||||
'POST', '/admin/services/{service}/toggle_enabled'.format(
|
|
||||||
service=service))
|
|
||||||
return body
|
|
||||||
|
|
||||||
def list_ui(self, path=None):
|
|
||||||
if path:
|
|
||||||
url = quote('/admin/ui/{path}'.format(path=path))
|
|
||||||
else:
|
|
||||||
url = '/admin/ui'
|
|
||||||
resp, body = self.http_client.json_request('GET', url)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def list_agent(self, path=None):
|
|
||||||
if path:
|
|
||||||
url = quote('/admin/agent/{path}'.format(path=path))
|
|
||||||
else:
|
|
||||||
url = '/admin/agent'
|
|
||||||
resp, body = self.http_client.json_request('GET', url)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def list_scripts(self, path=None):
|
|
||||||
if path:
|
|
||||||
url = quote('/admin/scripts/{path}'.format(path=path))
|
|
||||||
else:
|
|
||||||
url = '/admin/scripts'
|
|
||||||
resp, body = self.http_client.json_request('GET', url)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def list_workflows(self, path=None):
|
|
||||||
if path:
|
|
||||||
url = quote('/admin/workflows/{path}'.format(path=path))
|
|
||||||
else:
|
|
||||||
url = '/admin/workflows'
|
|
||||||
resp, body = self.http_client.json_request('GET', url)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def list_heat(self, path=None):
|
|
||||||
if path:
|
|
||||||
url = quote('/admin/heat/{path}'.format(path=path))
|
|
||||||
else:
|
|
||||||
url = '/admin/heat'
|
|
||||||
resp, body = self.http_client.json_request('GET', url)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def list_manifests(self, path=None):
|
|
||||||
if path:
|
|
||||||
url = quote('/admin/manifests/{path}'.format(path=path))
|
|
||||||
else:
|
|
||||||
url = '/admin/manifests'
|
|
||||||
resp, body = self.http_client.json_request('GET', url)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def upload_file(self, data_type, file_data, file_name=None):
|
|
||||||
if file_name:
|
|
||||||
params = urlencode({'filename': file_name})
|
|
||||||
url = '/admin/{0}?{1}'.format(data_type, params)
|
|
||||||
else:
|
|
||||||
url = '/admin/{0}'.format(data_type)
|
|
||||||
hdrs = {'Content-Type': 'application/octet-stream'}
|
|
||||||
resp, body = self.http_client.raw_request('POST', url,
|
|
||||||
headers=hdrs,
|
|
||||||
body=file_data)
|
|
||||||
return resp
|
|
||||||
|
|
||||||
def _update_service(self, service_files, service_id, service_info):
|
|
||||||
service_info.update(service_files)
|
|
||||||
url = quote('/admin/services/{service}'.format(service=service_id))
|
|
||||||
resp, body = self.http_client.json_request('PUT', url,
|
|
||||||
body=service_info)
|
|
||||||
return resp, body
|
|
||||||
|
|
||||||
def upload_file_to_service(self, data_type, file_data,
|
|
||||||
file_name, service_id):
|
|
||||||
self.upload_file(data_type, file_data, file_name)
|
|
||||||
service_info = self.get_service_info(service_id)
|
|
||||||
resp, service_files = self.http_client.json_request(
|
|
||||||
'GET', '/admin/services/{service}'.format(service=service_id))
|
|
||||||
existing_files = service_files.get(data_type)
|
|
||||||
if existing_files:
|
|
||||||
service_files[data_type].append(file_name)
|
|
||||||
else:
|
|
||||||
service_files[data_type] = [file_name]
|
|
||||||
resp, body = self._update_service(service_files,
|
|
||||||
service_id,
|
|
||||||
service_info)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def upload_file_to_dir(self, data_type, path, file_data):
|
|
||||||
url = quote('/admin/{0}/{1}'.format(data_type, path))
|
|
||||||
hdrs = {'Content-Type': 'application/octet-stream'}
|
|
||||||
self.http_client.raw_request('POST', url,
|
|
||||||
headers=hdrs,
|
|
||||||
body=file_data)
|
|
||||||
|
|
||||||
def get_file(self, data_type, file_path):
|
|
||||||
url = quote('/admin/{0}/{1}'.format(data_type, file_path))
|
|
||||||
resp, body = self.http_client.raw_request('GET', url)
|
|
||||||
body_str = ''.join([chunk for chunk in body])
|
|
||||||
return StringIO.StringIO(body_str)
|
|
||||||
|
|
||||||
def create_directory(self, data_type, dir_name):
|
|
||||||
url = '/admin/{0}/{1}'.format(data_type, dir_name)
|
|
||||||
self.http_client.json_request('PUT', url)
|
|
||||||
|
|
||||||
def delete(self, data_type, path):
|
|
||||||
url = quote('/admin/{0}/{1}'.format(data_type, path))
|
|
||||||
self.http_client.raw_request('DELETE', url)
|
|
||||||
|
|
||||||
def delete_from_service(self, data_type, filename, service_id):
|
|
||||||
service_info = self.get_service_info(service_id)
|
|
||||||
resp, service_files = self.http_client.json_request(
|
|
||||||
'GET', '/admin/services/{service}'.format(service=service_id))
|
|
||||||
files = service_files.get(data_type)
|
|
||||||
if filename in files:
|
|
||||||
service_files[data_type].remove(filename)
|
|
||||||
resp, body = self._update_service(service_files,
|
|
||||||
service_id,
|
|
||||||
service_info)
|
|
||||||
if resp.status == 200:
|
|
||||||
url = quote('/admin/{0}/{1}'.format(data_type, filename))
|
|
||||||
self.http_client.raw_request('DELETE', url)
|
|
||||||
return body
|
|
||||||
|
|
||||||
def create_or_update_service(self, service, json_data):
|
|
||||||
# Increment version in case of modification
|
|
||||||
json_data['service_version'] += 1
|
|
||||||
|
|
||||||
json_data['version'] = u'0.1' # Version of metadata
|
|
||||||
url = quote('/admin/services/{service}'.format(service=service))
|
|
||||||
resp, body = self.http_client.json_request('PUT', url, body=json_data)
|
|
||||||
return body
|
|
@ -1,43 +0,0 @@
|
|||||||
# Copyright (c) 2013 Mirantis, 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.
|
|
||||||
|
|
||||||
|
|
||||||
class Controller(object):
|
|
||||||
def __init__(self, http_client):
|
|
||||||
self.http_client = http_client
|
|
||||||
|
|
||||||
def _get_data(self, endpoint_type, hash_sum=None):
|
|
||||||
if hash_sum:
|
|
||||||
url = '/client/{0}?hash={1}'.format(endpoint_type, hash_sum)
|
|
||||||
else:
|
|
||||||
url = '/client/{0}'.format(endpoint_type)
|
|
||||||
return self.http_client.raw_request('GET', url)
|
|
||||||
|
|
||||||
def get_ui_data(self, hash_sum=None):
|
|
||||||
"""
|
|
||||||
Download tar.gz with ui metadata. Returns a tuple
|
|
||||||
(status, body_iterator) where status can be either 200 or 304. In the
|
|
||||||
304 case there is no sense in iterating with body_iterator.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._get_data('ui', hash_sum=hash_sum)
|
|
||||||
|
|
||||||
def get_conductor_data(self, hash_sum=None):
|
|
||||||
"""
|
|
||||||
Download tar.gz with conductor metadata. Returns a tuple
|
|
||||||
(status, body_iterator) where status can be either 200 or 304. In the
|
|
||||||
304 case there is no sense in iterating with body_iterator.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._get_data('conductor', hash_sum=hash_sum)
|
|
@ -1,23 +0,0 @@
|
|||||||
# Copyright (c) 2013 Mirantis, 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.
|
|
||||||
|
|
||||||
from muranoclient.common import utils
|
|
||||||
|
|
||||||
|
|
||||||
def do_environment_list(cc, args={}):
|
|
||||||
"""List the environments"""
|
|
||||||
environments = cc.environments.list()
|
|
||||||
field_labels = ['ID', 'Name', 'Created', 'Updated']
|
|
||||||
fields = ['id', 'name', 'created', 'updated']
|
|
||||||
utils.print_list(environments, fields, field_labels, sortby=0)
|
|
@ -1,9 +0,0 @@
|
|||||||
pbr>=0.6,<1.0
|
|
||||||
argparse
|
|
||||||
PrettyTable>=0.7,<0.8
|
|
||||||
python-keystoneclient>=0.6.0
|
|
||||||
httplib2>=0.7.5
|
|
||||||
iso8601>=0.1.8
|
|
||||||
six>=1.5.2
|
|
||||||
Babel>=1.3
|
|
||||||
pyOpenSSL>=0.11
|
|
43
setup.cfg
43
setup.cfg
@ -1,43 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name = murano-metadataclient
|
|
||||||
version = 0.4
|
|
||||||
summary = murano-metadataclient
|
|
||||||
description-file =
|
|
||||||
README.rst
|
|
||||||
license = Apache License, Version 2.0
|
|
||||||
author = Mirantis, Inc.
|
|
||||||
author-email = murano-all@lists.openstack.org
|
|
||||||
home-page = htts://launchpad.net/murano
|
|
||||||
classifier =
|
|
||||||
Development Status :: 4 - Beta
|
|
||||||
Environment :: Console
|
|
||||||
Intended Audience :: Developers
|
|
||||||
Intended Audience :: Information Technology
|
|
||||||
License :: OSI Approved :: Apache Software License
|
|
||||||
Operating System :: OS Independent
|
|
||||||
Programming Language :: Python
|
|
||||||
|
|
||||||
[files]
|
|
||||||
packages =
|
|
||||||
metadataclient
|
|
||||||
|
|
||||||
[entry_points]
|
|
||||||
console_scripts =
|
|
||||||
murano-metadataclient = metadataclient.shell:main
|
|
||||||
|
|
||||||
[global]
|
|
||||||
setup-hooks =
|
|
||||||
pbr.hooks.setup_hook
|
|
||||||
|
|
||||||
[egg_info]
|
|
||||||
tag_build =
|
|
||||||
tag_date = 0
|
|
||||||
tag_svn_revision = 0
|
|
||||||
|
|
||||||
[build_sphinx]
|
|
||||||
source-dir = doc/source
|
|
||||||
build-dir = doc/build
|
|
||||||
all_files = 1
|
|
||||||
|
|
||||||
[upload_sphinx]
|
|
||||||
upload-dir = doc/build/html
|
|
22
setup.py
22
setup.py
@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# 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
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
setup_requires=['pbr'],
|
|
||||||
pbr=True)
|
|
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
mock>=1.0
|
|
||||||
anyjson>=0.3.3
|
|
||||||
mox>=0.5.3
|
|
||||||
nose
|
|
||||||
nose-exclude
|
|
||||||
nosexcover
|
|
||||||
openstack.nose_plugin>=0.7
|
|
||||||
nosehtmloutput>=0.0.3
|
|
||||||
pep8==1.3.3
|
|
||||||
sphinx>=1.1.2,<1.2
|
|
||||||
unittest2
|
|
||||||
httpretty>=0.6.3
|
|
||||||
|
|
52
tox.ini
52
tox.ini
@ -1,52 +0,0 @@
|
|||||||
[tox]
|
|
||||||
envlist = pep8,pyflakes
|
|
||||||
|
|
||||||
[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
|
|
||||||
deps = -r{toxinidir}/requirements.txt
|
|
||||||
-r{toxinidir}/test-requirements.txt
|
|
||||||
#commands = nosetests
|
|
||||||
|
|
||||||
[testenv:pep8]
|
|
||||||
deps = pep8==1.3.3
|
|
||||||
commands = pep8 --repeat --show-source metadataclient setup.py
|
|
||||||
|
|
||||||
[testenv:venv]
|
|
||||||
commands = {posargs}
|
|
||||||
|
|
||||||
[testenv:cover]
|
|
||||||
#commands = nosetests --cover-erase --cover-package=metadataclient --with-xcoverage
|
|
||||||
|
|
||||||
[tox:jenkins]
|
|
||||||
downloadcache = ~/cache/pip
|
|
||||||
|
|
||||||
[testenv:jenkins26]
|
|
||||||
basepython = python2.6
|
|
||||||
|
|
||||||
[testenv:jenkins27]
|
|
||||||
basepython = python2.7
|
|
||||||
|
|
||||||
[testenv:py33]
|
|
||||||
basepython = python2.7
|
|
||||||
deps=
|
|
||||||
commands=
|
|
||||||
|
|
||||||
[testenv:docs]
|
|
||||||
commands = ls
|
|
||||||
|
|
||||||
[testenv:pyflakes]
|
|
||||||
deps = flake8
|
|
||||||
#commands = flake8
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
# H301 one import per line
|
|
||||||
# H302 import only modules
|
|
||||||
ignore = H301,H302,F401,F812
|
|
||||||
show-source = true
|
|
||||||
builtins = _
|
|
||||||
exclude=.venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,tools
|
|
Loading…
x
Reference in New Issue
Block a user