Retire Solum: remove repo content
Solum project is retiring - https://review.opendev.org/c/openstack/governance/+/919211 this commit remove the content of this project repo Change-Id: Ide6f26a5437553c19a40b402c56e931f51d025eb
This commit is contained in:

committed by
Ghanshyam

parent
b9b0d8314c
commit
31e8a21890
@@ -1,7 +0,0 @@
|
|||||||
[run]
|
|
||||||
branch = True
|
|
||||||
source = solumclient
|
|
||||||
omit = solumclient/tests/*
|
|
||||||
|
|
||||||
[report]
|
|
||||||
ignore_errors = True
|
|
53
.gitignore
vendored
53
.gitignore
vendored
@@ -1,53 +0,0 @@
|
|||||||
*.py[cod]
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Packages
|
|
||||||
*.egg
|
|
||||||
*.egg-info
|
|
||||||
dist
|
|
||||||
build
|
|
||||||
eggs
|
|
||||||
parts
|
|
||||||
bin
|
|
||||||
var
|
|
||||||
sdist
|
|
||||||
develop-eggs
|
|
||||||
.installed.cfg
|
|
||||||
lib
|
|
||||||
lib64
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
.coverage
|
|
||||||
.tox
|
|
||||||
.idea
|
|
||||||
cover
|
|
||||||
nosetests.xml
|
|
||||||
.stestr/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
|
|
||||||
# Mr Developer
|
|
||||||
.mr.developer.cfg
|
|
||||||
.project
|
|
||||||
.pydevproject
|
|
||||||
|
|
||||||
# Complexity
|
|
||||||
output/*.html
|
|
||||||
output/*/index.html
|
|
||||||
|
|
||||||
# Sphinx
|
|
||||||
doc/build
|
|
||||||
|
|
||||||
# pbr generates these
|
|
||||||
AUTHORS
|
|
||||||
ChangeLog
|
|
||||||
|
|
||||||
# Editors
|
|
||||||
*~
|
|
||||||
.*.swp
|
|
3
.mailmap
3
.mailmap
@@ -1,3 +0,0 @@
|
|||||||
# Format is:
|
|
||||||
# <preferred e-mail> <other e-mail 1>
|
|
||||||
# <preferred e-mail> <other e-mail 2>
|
|
@@ -1,3 +0,0 @@
|
|||||||
[DEFAULT]
|
|
||||||
test_path=${OS_TEST_PATH:-./solumclient/tests}
|
|
||||||
top_dir=./
|
|
@@ -1,6 +0,0 @@
|
|||||||
- project:
|
|
||||||
templates:
|
|
||||||
- check-requirements
|
|
||||||
- openstack-cover-jobs
|
|
||||||
- openstack-python3-jobs
|
|
||||||
- publish-openstack-docs-pti
|
|
@@ -1,16 +0,0 @@
|
|||||||
If you would like to contribute to the development of OpenStack,
|
|
||||||
you must follow the steps in this page:
|
|
||||||
|
|
||||||
https://docs.openstack.org/infra/manual/developers.html
|
|
||||||
|
|
||||||
Once those steps have been completed, changes to OpenStack
|
|
||||||
should be submitted for review via the Gerrit tool, following
|
|
||||||
the workflow documented at:
|
|
||||||
|
|
||||||
https://docs.openstack.org/infra/manual/developers.html#development-workflow
|
|
||||||
|
|
||||||
Pull requests submitted through GitHub will be ignored.
|
|
||||||
|
|
||||||
Bugs should be filed on Launchpad, not GitHub:
|
|
||||||
|
|
||||||
https://bugs.launchpad.net/python-solumclient
|
|
@@ -1,4 +0,0 @@
|
|||||||
Solum Client Style Commandments
|
|
||||||
===============================
|
|
||||||
|
|
||||||
Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/
|
|
175
LICENSE
175
LICENSE
@@ -1,175 +0,0 @@
|
|||||||
|
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
41
README.rst
41
README.rst
@@ -1,35 +1,10 @@
|
|||||||
========================
|
This project is no longer maintained.
|
||||||
Team and repository tags
|
|
||||||
========================
|
|
||||||
|
|
||||||
.. image:: https://governance.openstack.org/tc/badges/python-solumclient.svg
|
The contents of this repository are still available in the Git
|
||||||
:target: https://governance.openstack.org/tc/reference/tags/index.html
|
source code management system. To see the contents of this
|
||||||
|
repository before it reached its end of life, please check out the
|
||||||
|
previous commit with "git checkout HEAD^1".
|
||||||
|
|
||||||
.. Change things from this point on
|
For any further questions, please email
|
||||||
|
openstack-discuss@lists.openstack.org or join #openstack-dev on
|
||||||
Python bindings to the Solum API
|
OFTC.
|
||||||
================================
|
|
||||||
|
|
||||||
This is a client library for Solum built on the Solum API.
|
|
||||||
|
|
||||||
* Free software: Apache license
|
|
||||||
* Documentation: https://wiki.openstack.org/wiki/Solum
|
|
||||||
|
|
||||||
Installation
|
|
||||||
============
|
|
||||||
|
|
||||||
At the command line::
|
|
||||||
|
|
||||||
$ pip install python-solumclient
|
|
||||||
|
|
||||||
Or, if you have virtualenvwrapper installed::
|
|
||||||
|
|
||||||
$ mkvirtualenv python-solumclient
|
|
||||||
$ pip install python-solumclient
|
|
||||||
|
|
||||||
Usage
|
|
||||||
========
|
|
||||||
|
|
||||||
To use python-solumclient in a project::
|
|
||||||
|
|
||||||
import solumclient
|
|
||||||
|
@@ -1,311 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# Copyright 2014 - Rackspace Hosting
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Setup everything for a Github-hosted repo to use solum as its CI tool.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import getpass
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import string
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import httplib2
|
|
||||||
import six
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from solumclient import client as solum_client
|
|
||||||
from solumclient.common import cliutils
|
|
||||||
|
|
||||||
|
|
||||||
SOLUM_API_VERSION = '1'
|
|
||||||
CREDENTIALS = {}
|
|
||||||
PLAN_TEMPLATE = {"version": 1,
|
|
||||||
"name": "chef",
|
|
||||||
"description": "chef testy",
|
|
||||||
"artifacts": []}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_solum_client():
|
|
||||||
args = {}
|
|
||||||
args['os_username'] = os.getenv('OS_USERNAME', '')
|
|
||||||
args['os_password'] = os.getenv('OS_PASSWORD', '')
|
|
||||||
args['os_tenant_name'] = os.getenv('OS_TENANT_NAME', '')
|
|
||||||
args['os_auth_url'] = os.getenv('OS_AUTH_URL', '')
|
|
||||||
args['solum_url'] = os.getenv('SOLUM_URL', '')
|
|
||||||
|
|
||||||
try:
|
|
||||||
client = solum_client.get_client(SOLUM_API_VERSION, **args)
|
|
||||||
return client
|
|
||||||
except Exception as ex:
|
|
||||||
print("Error in getting Solum client: %s" % ex)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_token(git_url):
|
|
||||||
# Get an OAuth token with the scope of 'repo' for the user
|
|
||||||
if git_url in CREDENTIALS and 'token' in CREDENTIALS[git_url]:
|
|
||||||
return CREDENTIALS[git_url]['token']
|
|
||||||
|
|
||||||
repo_pat = re.compile(r'github\.com[:/](.+?)/(.+?)($|/$|\.git$|\.git/$)')
|
|
||||||
match = repo_pat.search(git_url)
|
|
||||||
if match:
|
|
||||||
user_org_name = match.group(1)
|
|
||||||
repo = match.group(2)
|
|
||||||
else:
|
|
||||||
print('Failed parsing %s' % git_url)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
full_repo_name = '/'.join([user_org_name, repo])
|
|
||||||
username = six.moves.input("Username for repo '%s' [%s]: " %
|
|
||||||
(full_repo_name, user_org_name))
|
|
||||||
if not username:
|
|
||||||
username = user_org_name
|
|
||||||
password = getpass.getpass("Password: ")
|
|
||||||
# TODO(james_li): add support for two-factor auth
|
|
||||||
CREDENTIALS[git_url] = {}
|
|
||||||
CREDENTIALS[git_url]['user'] = username
|
|
||||||
CREDENTIALS[git_url]['password'] = password
|
|
||||||
CREDENTIALS[git_url]['full_repo'] = full_repo_name
|
|
||||||
|
|
||||||
http = httplib2.Http()
|
|
||||||
auth = base64.encodestring(username + ':' + password)
|
|
||||||
headers = {'Authorization': 'Basic ' + auth,
|
|
||||||
'Content-Type': 'application/json'}
|
|
||||||
# 'note' field has to be unique
|
|
||||||
note = 'Solum-status-' + ''.join(random.sample(string.lowercase, 5))
|
|
||||||
data = {'scopes': 'repo', 'note': note}
|
|
||||||
|
|
||||||
# TODO(james_li): make the url configurable
|
|
||||||
resp, content = http.request('https://api.github.com/authorizations',
|
|
||||||
'POST', headers=headers,
|
|
||||||
body=json.dumps(data))
|
|
||||||
|
|
||||||
if resp['status'] == '201' or resp['status'] == '200':
|
|
||||||
content_dict = json.loads(content)
|
|
||||||
CREDENTIALS[git_url]['token'] = str(content_dict['token'])
|
|
||||||
return CREDENTIALS[git_url]['token']
|
|
||||||
else:
|
|
||||||
print('Failed to get token from Github')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def _filter_trigger_url(url):
|
|
||||||
filtered_url = url
|
|
||||||
url_pattern = re.compile(r'^(http://)(.+)')
|
|
||||||
match = url_pattern.search(url)
|
|
||||||
if match:
|
|
||||||
filtered_url = ''.join(['https://', match.group(2)])
|
|
||||||
else:
|
|
||||||
print('Cannot filter trigger url, to use the original one')
|
|
||||||
return filtered_url
|
|
||||||
|
|
||||||
|
|
||||||
def get_planfile(git_uri, app_name, cmd, public):
|
|
||||||
plan_dict = dict.copy(PLAN_TEMPLATE)
|
|
||||||
plan_dict['name'] = app_name
|
|
||||||
plan_dict['description'] = git_uri # Put repo uri as plan desc.
|
|
||||||
arti = {"name": "chef", "artifact_type": "chef",
|
|
||||||
"content": {}, "language_pack": "auto"}
|
|
||||||
arti['content']['href'] = git_uri
|
|
||||||
if not public:
|
|
||||||
arti['content']['private'] = True
|
|
||||||
arti['unittest_cmd'] = cmd
|
|
||||||
plan_dict['artifacts'].append(arti)
|
|
||||||
|
|
||||||
# Create a Github token and insert it into plan file
|
|
||||||
for arti in plan_dict['artifacts']:
|
|
||||||
arti['status_token'] = _get_token(arti['content']['href'])
|
|
||||||
plan_file = tempfile.NamedTemporaryFile(suffix='.yaml',
|
|
||||||
prefix='solum_',
|
|
||||||
delete=False)
|
|
||||||
plan_file.write(yaml.dump(plan_dict, default_flow_style=False))
|
|
||||||
plan_file_name = plan_file.name
|
|
||||||
plan_file.close()
|
|
||||||
return plan_file_name
|
|
||||||
|
|
||||||
|
|
||||||
def create_plan(client, plan_file):
|
|
||||||
cmd = ['solum', 'app', 'create', plan_file]
|
|
||||||
print(' '.join(cmd))
|
|
||||||
with open(plan_file) as definition_file:
|
|
||||||
definition = definition_file.read()
|
|
||||||
|
|
||||||
plan = client.plans.create(definition)
|
|
||||||
fields = ['uuid', 'name', 'description', 'uri']
|
|
||||||
data = dict([(f, getattr(plan, f, ''))
|
|
||||||
for f in fields])
|
|
||||||
cliutils.print_dict(data, wrap=72)
|
|
||||||
|
|
||||||
if data['uri'] is None:
|
|
||||||
print('Error: no uri found in plan creation')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# get public keys in the case of private repos
|
|
||||||
artifacts = getattr(plan, 'artifacts', [])
|
|
||||||
for arti in artifacts:
|
|
||||||
content = getattr(arti, 'content', {})
|
|
||||||
if 'public_key' in content and 'href' in content:
|
|
||||||
CREDENTIALS[content['href']]['pub_key'] = content['public_key']
|
|
||||||
|
|
||||||
return data['uri']
|
|
||||||
|
|
||||||
|
|
||||||
def create_assembly(client, app_name, plan_uri):
|
|
||||||
cmd = ['solum', 'assembly', 'create', app_name, plan_uri]
|
|
||||||
print(' '.join(cmd))
|
|
||||||
assembly = client.assemblies.create(name=app_name, plan_uri=plan_uri)
|
|
||||||
|
|
||||||
fields = ['uuid', 'name', 'description', 'status', 'application_uri',
|
|
||||||
'trigger_uri']
|
|
||||||
data = dict([(f, getattr(assembly, f, ''))
|
|
||||||
for f in fields])
|
|
||||||
cliutils.print_dict(data, wrap=72)
|
|
||||||
|
|
||||||
trigger_uri = data['trigger_uri']
|
|
||||||
if trigger_uri is None:
|
|
||||||
print('Error in trigger uri')
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
return trigger_uri
|
|
||||||
|
|
||||||
|
|
||||||
def create_webhook(trigger_uri):
|
|
||||||
# Create github web hooks for pull requests
|
|
||||||
for key in CREDENTIALS.keys():
|
|
||||||
user = CREDENTIALS[key]['user']
|
|
||||||
password = CREDENTIALS[key]['password']
|
|
||||||
auth = base64.encodestring(user + ':' + password)
|
|
||||||
http = httplib2.Http()
|
|
||||||
# TODO(james_li): make this url configurable
|
|
||||||
github_url = ('https://api.github.com/repos/%s/hooks' %
|
|
||||||
CREDENTIALS[key]['full_repo'])
|
|
||||||
headers = {'Authorization': 'Basic ' + auth,
|
|
||||||
'Content-Type': 'application/json'}
|
|
||||||
data = {'name': 'web',
|
|
||||||
'events': ['pull_request', 'commit_comment'],
|
|
||||||
'config': {'content_type': 'json',
|
|
||||||
'url': trigger_uri}}
|
|
||||||
|
|
||||||
resp, _ = http.request(github_url, 'POST',
|
|
||||||
headers=headers,
|
|
||||||
body=json.dumps(data))
|
|
||||||
|
|
||||||
if resp['status'] != '201' and resp['status'] != '200':
|
|
||||||
print("Failed to create web hooks")
|
|
||||||
print("Make sure you have access to repo '%s'" % key)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def add_ssh_keys(args):
|
|
||||||
if args.public:
|
|
||||||
return
|
|
||||||
|
|
||||||
# add public keys
|
|
||||||
for key in CREDENTIALS.keys():
|
|
||||||
if ('pub_key' in CREDENTIALS[key] and
|
|
||||||
CREDENTIALS[key]['pub_key'] is not None):
|
|
||||||
user = CREDENTIALS[key]['user']
|
|
||||||
password = CREDENTIALS[key]['password']
|
|
||||||
auth = base64.encodestring(user + ':' + password)
|
|
||||||
http = httplib2.Http()
|
|
||||||
if args.user_key:
|
|
||||||
# TODO(james_li): make the url configurable
|
|
||||||
github_url = 'https://api.github.com/user/keys'
|
|
||||||
else:
|
|
||||||
github_url = ('https://api.github.com/repos/%s/keys' %
|
|
||||||
CREDENTIALS[key]['full_repo'])
|
|
||||||
headers = {'Authorization': 'Basic ' + auth,
|
|
||||||
'Content-Type': 'application/json'}
|
|
||||||
data = {'title': 'devops@Solum',
|
|
||||||
'key': CREDENTIALS[key]['pub_key']}
|
|
||||||
|
|
||||||
resp, _ = http.request(github_url, 'POST',
|
|
||||||
headers=headers,
|
|
||||||
body=json.dumps(data))
|
|
||||||
|
|
||||||
if resp['status'] != '201' and resp['status'] != '200':
|
|
||||||
if args.user_key:
|
|
||||||
print("Failed to add a ssh key to the account %s" % user)
|
|
||||||
else:
|
|
||||||
print("Failed to add a deploy key to the repo %s" % key)
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_args(args):
|
|
||||||
if len(args.command) == 0 or len(args.git_uri) == 0:
|
|
||||||
print("Please input for --test-cmd and --git-uri")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
# try to use a correct git uri
|
|
||||||
pat = re.compile(r'github\.com[:/](.+?)/(.+?)($|/.*$|\.git$|\.git/.*$)')
|
|
||||||
match = pat.search(args.git_uri)
|
|
||||||
if match:
|
|
||||||
user_org_name = match.group(1)
|
|
||||||
repo = match.group(2)
|
|
||||||
if args.public:
|
|
||||||
correct_uri = 'https://github.com/%s/%s' % (user_org_name, repo)
|
|
||||||
else:
|
|
||||||
correct_uri = 'git@github.com:%s/%s.git' % (user_org_name, repo)
|
|
||||||
return correct_uri
|
|
||||||
else:
|
|
||||||
print("The input git uri seems not right")
|
|
||||||
if args.public:
|
|
||||||
print("The correct format is: https://github.com/<USER>/<REPO>")
|
|
||||||
else:
|
|
||||||
print("The correct format is: git@github.com:<USER>/<REPO>.git")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main(args):
|
|
||||||
git_uri = validate_args(args)
|
|
||||||
client = _get_solum_client()
|
|
||||||
plan_file = get_planfile(git_uri, args.app_name, args.command, args.public)
|
|
||||||
print('\n')
|
|
||||||
print("************************* Starting setup *************************")
|
|
||||||
print('\n')
|
|
||||||
plan_uri = create_plan(client, plan_file)
|
|
||||||
add_ssh_keys(args)
|
|
||||||
try:
|
|
||||||
os.remove(plan_file)
|
|
||||||
except OSError:
|
|
||||||
print('Cannot remove %s. Skip and move forward...' % plan_file)
|
|
||||||
|
|
||||||
trigger_uri = create_assembly(client, args.app_name, plan_uri)
|
|
||||||
create_webhook(_filter_trigger_url(trigger_uri))
|
|
||||||
print('Successfully created Solum plan, assembly and webhooks!')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument('app_name', help="app name")
|
|
||||||
parser.add_argument('--git-uri', required=True, dest='git_uri',
|
|
||||||
help="git repo uri")
|
|
||||||
parser.add_argument('--test-cmd', required=True, dest='command',
|
|
||||||
help="entrypoint to run tests")
|
|
||||||
parser.add_argument('--public', action='store_true', default=False,
|
|
||||||
dest='public', help="public repo, defaults to False")
|
|
||||||
parser.add_argument('--user-key', action='store_true', default=False,
|
|
||||||
dest='user_key', help="add SSH key to the user account"
|
|
||||||
" rather than the repo,"
|
|
||||||
" defaults to False")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
main(args)
|
|
@@ -1,5 +0,0 @@
|
|||||||
# The order of packages is significant, because pip processes them in the order
|
|
||||||
# of appearance. Changing the order has an impact on the overall integration
|
|
||||||
# process, which may cause wedges in the gate later.
|
|
||||||
openstackdocstheme>=2.2.1 # Apache-2.0
|
|
||||||
sphinx>=2.0.0,!=2.1.0 # BSD
|
|
@@ -1,30 +0,0 @@
|
|||||||
===========================
|
|
||||||
Command-line Tool Reference
|
|
||||||
===========================
|
|
||||||
|
|
||||||
In order to use the CLI, you must provide your OpenStack username,
|
|
||||||
password, tenant, and auth endpoint. Use the corresponding
|
|
||||||
configuration options (``--os-username``, ``--os-password``,
|
|
||||||
``--os-tenant-id``, and ``--os-auth-url``) or set them in environment
|
|
||||||
variables::
|
|
||||||
|
|
||||||
export OS_USERNAME=user
|
|
||||||
export OS_PASSWORD=pass
|
|
||||||
export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b
|
|
||||||
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
|
|
||||||
|
|
||||||
The command line tool will attempt to reauthenticate using your
|
|
||||||
provided credentials for every request. You can override this behavior
|
|
||||||
by manually supplying an auth token using ``--os-solum-url`` and
|
|
||||||
``--os-auth-token``. You can alternatively set these environment
|
|
||||||
variables::
|
|
||||||
|
|
||||||
export OS_SOLUM_URL=http://solum.example.org:9777/
|
|
||||||
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
|
|
||||||
|
|
||||||
Once you've configured your authentication parameters, you can run
|
|
||||||
``solum help`` to see a complete listing of available commands.
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
|
|
||||||
solum
|
|
@@ -1,108 +0,0 @@
|
|||||||
=============================================================================
|
|
||||||
Software Development Lifecycle Automation service (solum) command-line client
|
|
||||||
=============================================================================
|
|
||||||
|
|
||||||
The solum client is the command-line interface (CLI) for
|
|
||||||
the Software Development Lifecycle Automation service (solum) API and its
|
|
||||||
extensions.
|
|
||||||
|
|
||||||
This chapter documents :command:`solum` version ``2.4.0``.
|
|
||||||
|
|
||||||
.. _solum_command_usage:
|
|
||||||
|
|
||||||
solum usage
|
|
||||||
~~~~~~~~~~~
|
|
||||||
|
|
||||||
``solum help``
|
|
||||||
Show this help message.
|
|
||||||
|
|
||||||
``solum info``
|
|
||||||
Show Solum endpoint and API release version.
|
|
||||||
|
|
||||||
``solum --version``
|
|
||||||
Show current Solum client version and exit.
|
|
||||||
|
|
||||||
``solum lp help``
|
|
||||||
Show a help message specific to languagepack commands.
|
|
||||||
|
|
||||||
``solum lp create <NAME> <GIT_REPO_URL>``
|
|
||||||
Create a new language pack from a git repo.
|
|
||||||
|
|
||||||
``solum lp list``
|
|
||||||
Print and index of all available language packs.
|
|
||||||
|
|
||||||
``solum lp show <NAME|UUID>``
|
|
||||||
Print the details of a language pack.
|
|
||||||
|
|
||||||
``solum lp delete <NAME|UUID>``
|
|
||||||
Destroy a language pack.
|
|
||||||
|
|
||||||
``solum lp logs <NAME|UUID>``
|
|
||||||
Show logs for a language pack.
|
|
||||||
|
|
||||||
``solum app help``
|
|
||||||
Show a help message specific to app commands.
|
|
||||||
|
|
||||||
``solum app list``
|
|
||||||
Print an index of all deployed applications.
|
|
||||||
|
|
||||||
``solum app show <NAME|UUID>``
|
|
||||||
Print detailed information about one application.
|
|
||||||
|
|
||||||
``solum app create``
|
|
||||||
Register a new application with Solum.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
solum app create [--app-file <AppFile>] [--git-url <GIT_URL>]
|
|
||||||
[--lp <LANGUAGEPACK>]
|
|
||||||
[--param-file <PARAMFILE>]
|
|
||||||
[--setup-trigger]
|
|
||||||
[--trigger-workflow <CUSTOM-WORKFLOW>]
|
|
||||||
<CUSTOM-WORKFLOW>=(unittest | build | unittest+build)
|
|
||||||
|
|
||||||
Without the ``--trigger-workflow`` flag, the workflow ``unittest+build+deploy``
|
|
||||||
is triggered (this is the default workflow).
|
|
||||||
|
|
||||||
``solum app deploy <NAME|UUID>``
|
|
||||||
Deploy an application, building any applicable artifacts first.
|
|
||||||
du-id is optional flag. It can be used to pass in ID of a previously
|
|
||||||
created deployment unit. If passed, this command will deploy the du
|
|
||||||
referenced by the provided ``du-id`` instead of building one first.
|
|
||||||
|
|
||||||
``solum app delete <NAME|UUID>``
|
|
||||||
Delete an application and all related artifacts.
|
|
||||||
|
|
||||||
``solum app logs <NAME|UUID> [--wf-id <wf-id>]``
|
|
||||||
Show the logs of an application for all the workflows.
|
|
||||||
``wf-id`` is optional flag which can be used to pass in ID of one of
|
|
||||||
the existing workflows. If provided, the logs only for that workflow
|
|
||||||
are displayed.
|
|
||||||
|
|
||||||
``solum app scale <APP_NAME|UUID> <target>``
|
|
||||||
|
|
||||||
``solum workflow list <APP_NAME|UUID>``
|
|
||||||
List all application workflows.
|
|
||||||
|
|
||||||
``solum workflow show <APP_NAME|UUID> <WORKFLOW_ID|UUID>``
|
|
||||||
Print the details of a workflow.
|
|
||||||
|
|
||||||
``solum workflow logs <APP_NAME|UUID> <WORKFLOW_ID|UUID>``
|
|
||||||
List all the logs of a given workflow.
|
|
||||||
|
|
||||||
**SOON TO BE DEPRECATED:**
|
|
||||||
|
|
||||||
``solum oldapp create``
|
|
||||||
Register a new application with Solum.
|
|
||||||
|
|
||||||
.. code-block:: console
|
|
||||||
|
|
||||||
solum oldapp create [--plan-file <PLANFILE>] [--git-url <GIT_URL>]
|
|
||||||
[--lp <LANGUAGEPACK>] [--run-cmd <RUN_CMD>]
|
|
||||||
[--unittest-cmd <UNITTEST_CMD>]
|
|
||||||
[--name <NAME>] [--port <PORT>]
|
|
||||||
[--param-file <PARAMFILE>]
|
|
||||||
[--desc <DESCRIPTION>]
|
|
||||||
[--setup-trigger]
|
|
||||||
[--private-repo]
|
|
||||||
[--trigger-workflow <WORKFLOW>]
|
|
@@ -1,83 +0,0 @@
|
|||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
|
||||||
# -- General configuration ----------------------------------------------------
|
|
||||||
|
|
||||||
# Add any Sphinx extension module names here, as strings. They can be
|
|
||||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
|
||||||
extensions = [
|
|
||||||
'sphinx.ext.autodoc',
|
|
||||||
'openstackdocstheme',
|
|
||||||
]
|
|
||||||
|
|
||||||
# autodoc generation is a bit aggressive and a nuisance when doing heavy
|
|
||||||
# text edit cycles.
|
|
||||||
# execute "export SPHINX_DEBUG=1" in your terminal to disable
|
|
||||||
|
|
||||||
# The suffix of source filenames.
|
|
||||||
source_suffix = '.rst'
|
|
||||||
|
|
||||||
# The master toctree document.
|
|
||||||
master_doc = 'index'
|
|
||||||
|
|
||||||
# General information about the project.
|
|
||||||
copyright = u'2013, OpenStack Foundation'
|
|
||||||
|
|
||||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
|
||||||
add_function_parentheses = True
|
|
||||||
|
|
||||||
# If true, the current module name will be prepended to all description
|
|
||||||
# unit titles (such as .. function::).
|
|
||||||
add_module_names = True
|
|
||||||
|
|
||||||
# The name of the Pygments (syntax highlighting) style to use.
|
|
||||||
pygments_style = 'native'
|
|
||||||
|
|
||||||
# -- Options for HTML output --------------------------------------------------
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
|
||||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
|
||||||
# html_theme_path = ["."]
|
|
||||||
# html_theme = '_theme'
|
|
||||||
# html_static_path = ['static']
|
|
||||||
|
|
||||||
# The theme to use for HTML and HTML Help pages. Major themes that come with
|
|
||||||
# Sphinx are currently 'default' and 'sphinxdoc'.
|
|
||||||
#html_theme = 'nature'
|
|
||||||
html_theme = 'openstackdocs'
|
|
||||||
|
|
||||||
# openstackdocstheme options
|
|
||||||
openstackdocs_repo_name = 'openstack/python-solumclient'
|
|
||||||
openstackdocs_bug_project = 'python-solumclient'
|
|
||||||
openstackdocs_bug_tag = ''
|
|
||||||
|
|
||||||
# Output file base name for HTML help builder.
|
|
||||||
htmlhelp_basename = 'python-solumclientdoc'
|
|
||||||
|
|
||||||
# Grouping the document tree into LaTeX files. List of tuples
|
|
||||||
# (source start file, target name, title, author, documentclass
|
|
||||||
# [howto/manual]).
|
|
||||||
latex_documents = [
|
|
||||||
('index',
|
|
||||||
'python-solumclient.tex',
|
|
||||||
u'python-solumclient Documentation',
|
|
||||||
u'OpenStack Foundation', 'manual'),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Example configuration for intersphinx: refer to the Python standard library.
|
|
||||||
#intersphinx_mapping = {'http://docs.python.org/': None}
|
|
@@ -1,12 +0,0 @@
|
|||||||
================================
|
|
||||||
python-solumclient documentation
|
|
||||||
================================
|
|
||||||
|
|
||||||
This is a client for the OpenStack Application Lifecycle Management API.
|
|
||||||
There's a Python API (the :mod:`solumclient` module) and a
|
|
||||||
:doc:`command-line script <cli/solum>` (installed as :program:`solum`).
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
|
|
||||||
cli/index
|
|
@@ -1,6 +0,0 @@
|
|||||||
---
|
|
||||||
upgrade:
|
|
||||||
- |
|
|
||||||
Python 2.7 support has been dropped. Last release of python-solumclient
|
|
||||||
to support python 2.7 is OpenStack Train. The minimum version of Python now
|
|
||||||
supported by python-solumclient is Python 3.6.
|
|
@@ -1,22 +0,0 @@
|
|||||||
# Requirements lower bounds listed here are our best effort to keep them up to
|
|
||||||
# date but we do not test them so no guarantee of having them all correct. If
|
|
||||||
# you find any incorrect lower bounds, let us know or propose a fix.
|
|
||||||
|
|
||||||
# The order of packages is significant, because pip processes them in the order
|
|
||||||
# of appearance. Changing the order has an impact on the overall integration
|
|
||||||
# process, which may cause wedges in the gate later.
|
|
||||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
|
||||||
Babel!=2.4.0,>=2.3.4 # BSD
|
|
||||||
oslo.i18n>=3.15.3 # Apache-2.0
|
|
||||||
oslo.config>=5.2.0 # Apache-2.0
|
|
||||||
oslo.utils>=3.33.0 # Apache-2.0
|
|
||||||
oslo.log>=3.36.0 # Apache-2.0
|
|
||||||
httplib2>=0.18.1 # MIT
|
|
||||||
iso8601>=0.1.11 # MIT
|
|
||||||
jsonschema>=3.2.0 # MIT
|
|
||||||
requests>=2.14.2 # Apache-2.0
|
|
||||||
python-keystoneclient>=3.8.0 # Apache-2.0
|
|
||||||
PyYAML>=3.13 # MIT
|
|
||||||
stevedore>=2.0.1 # Apache-2.0
|
|
||||||
PrettyTable>=0.7.2 # BSD
|
|
||||||
debtcollector>=1.2.0 # Apache-2.0
|
|
46
setup.cfg
46
setup.cfg
@@ -1,46 +0,0 @@
|
|||||||
[metadata]
|
|
||||||
name = python-solumclient
|
|
||||||
summary = Client library for Solum API
|
|
||||||
description-file =
|
|
||||||
README.rst
|
|
||||||
author = OpenStack
|
|
||||||
author-email = openstack-discuss@lists.openstack.org
|
|
||||||
home-page = https://docs.openstack.org/python-solumclient/latest/
|
|
||||||
python-requires = >=3.6
|
|
||||||
classifier =
|
|
||||||
Environment :: OpenStack
|
|
||||||
Intended Audience :: Information Technology
|
|
||||||
Intended Audience :: System Administrators
|
|
||||||
License :: OSI Approved :: Apache Software License
|
|
||||||
Operating System :: POSIX :: Linux
|
|
||||||
Programming Language :: Python
|
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
|
||||||
Programming Language :: Python :: 3 :: Only
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3.6
|
|
||||||
Programming Language :: Python :: 3.7
|
|
||||||
Programming Language :: Python :: 3.8
|
|
||||||
|
|
||||||
[files]
|
|
||||||
packages =
|
|
||||||
solumclient
|
|
||||||
scripts =
|
|
||||||
contrib/setup-tools/solum-app-setup.py
|
|
||||||
|
|
||||||
[entry_points]
|
|
||||||
console_scripts =
|
|
||||||
solum = solumclient.solum:main
|
|
||||||
|
|
||||||
[compile_catalog]
|
|
||||||
directory = solumclient/locale
|
|
||||||
domain = solumclient
|
|
||||||
|
|
||||||
[update_catalog]
|
|
||||||
domain = solumclient
|
|
||||||
output_dir = solumclient/locale
|
|
||||||
input_file = solumclient/locale/solumclient.pot
|
|
||||||
|
|
||||||
[extract_messages]
|
|
||||||
keywords = _ gettext ngettext l_ lazy_gettext
|
|
||||||
mapping_file = babel.cfg
|
|
||||||
output_file = solumclient/locale/solumclient.pot
|
|
20
setup.py
20
setup.py
@@ -1,20 +0,0 @@
|
|||||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import setuptools
|
|
||||||
|
|
||||||
setuptools.setup(
|
|
||||||
setup_requires=['pbr>=2.0.0'],
|
|
||||||
pbr=True)
|
|
@@ -1,19 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import pbr.version
|
|
||||||
|
|
||||||
|
|
||||||
__version__ = pbr.version.VersionInfo(
|
|
||||||
'python-solumclient').version_string()
|
|
@@ -1,63 +0,0 @@
|
|||||||
# Copyright 2014 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import client as api_client
|
|
||||||
from solumclient.common import auth
|
|
||||||
from solumclient.common import client
|
|
||||||
|
|
||||||
API_NAME = 'builder'
|
|
||||||
VERSION_MAP = {
|
|
||||||
'1': 'solumclient.builder.v1.client.Client',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def Client(version, **kwargs):
|
|
||||||
client_class = api_client.BaseClient.get_class(API_NAME, version,
|
|
||||||
VERSION_MAP)
|
|
||||||
keystone_auth = auth.KeystoneAuthPlugin(
|
|
||||||
username=kwargs.get('username'),
|
|
||||||
password=kwargs.get('password'),
|
|
||||||
tenant_name=kwargs.get('tenant_name'),
|
|
||||||
token=kwargs.get('token'),
|
|
||||||
auth_url=kwargs.get('auth_url'),
|
|
||||||
endpoint=kwargs.get('endpoint'))
|
|
||||||
http_client = client.HTTPClient(keystone_auth)
|
|
||||||
return client_class(http_client)
|
|
||||||
|
|
||||||
|
|
||||||
def get_client(api_version, **kwargs):
|
|
||||||
"""Get an authtenticated client.
|
|
||||||
|
|
||||||
This is based on the credentials in the keyword args.
|
|
||||||
|
|
||||||
:param api_version: the API version to use
|
|
||||||
:param kwargs: keyword args containing credentials, either:
|
|
||||||
* os_auth_token: pre-existing token to re-use
|
|
||||||
* endpoint: solum API endpoint
|
|
||||||
or:
|
|
||||||
* os_username: name of user
|
|
||||||
* os_password: user's password
|
|
||||||
* os_auth_url: endpoint to authenticate against
|
|
||||||
* os_tenant_name: name of tenant
|
|
||||||
"""
|
|
||||||
cli_kwargs = {
|
|
||||||
'username': kwargs.get('os_username'),
|
|
||||||
'password': kwargs.get('os_password'),
|
|
||||||
'tenant_name': kwargs.get('os_tenant_name'),
|
|
||||||
'token': kwargs.get('os_auth_token'),
|
|
||||||
'auth_url': kwargs.get('os_auth_url'),
|
|
||||||
'endpoint': kwargs.get('solum_url')
|
|
||||||
}
|
|
||||||
|
|
||||||
return Client(api_version, **cli_kwargs)
|
|
@@ -1,27 +0,0 @@
|
|||||||
# Copyright 2014 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.builder.v1 import image
|
|
||||||
from solumclient.common.apiclient import client
|
|
||||||
|
|
||||||
|
|
||||||
class Client(client.BaseClient):
|
|
||||||
"""Client for the Solum v1 API."""
|
|
||||||
|
|
||||||
service_type = "image_builder"
|
|
||||||
|
|
||||||
def __init__(self, http_client, extensions=None):
|
|
||||||
"""Initialize a new client for the Builder v1 API."""
|
|
||||||
super(Client, self).__init__(http_client, extensions)
|
|
||||||
self.images = image.ImageManager(self)
|
|
@@ -1,41 +0,0 @@
|
|||||||
# Copyright 2014 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base as apiclient_base
|
|
||||||
from solumclient.common import base as solum_base
|
|
||||||
|
|
||||||
|
|
||||||
class Image(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Image %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class ImageManager(solum_base.CrudManager, solum_base.FindMixin):
|
|
||||||
resource_class = Image
|
|
||||||
collection_key = 'images'
|
|
||||||
key = 'image'
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
return super(ImageManager, self).create(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
return super(ImageManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def list(self, **kwargs):
|
|
||||||
return super(ImageManager, self).list(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
name_or_uuid = kwargs['name_or_id']
|
|
||||||
return super(ImageManager, self).get(base_url="/v1",
|
|
||||||
image_id=name_or_uuid)
|
|
@@ -1,235 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 contextlib
|
|
||||||
import time
|
|
||||||
|
|
||||||
from keystoneclient import adapter
|
|
||||||
from oslo_utils import strutils
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import client as api_client
|
|
||||||
from solumclient.common import auth
|
|
||||||
from solumclient.common import exc
|
|
||||||
|
|
||||||
API_NAME = 'solum'
|
|
||||||
VERSION_MAP = {
|
|
||||||
'1': 'solumclient.v1.client.Client',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def Client(version, *args, **kwargs):
|
|
||||||
client_class = api_client.BaseClient.get_class(API_NAME, version,
|
|
||||||
VERSION_MAP)
|
|
||||||
kwargs['token'] = kwargs.get('token') or kwargs.get('auth_token')
|
|
||||||
return client_class(version, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def _adjust_params(kwargs):
|
|
||||||
timeout = kwargs.get('timeout')
|
|
||||||
if timeout is not None:
|
|
||||||
timeout = int(timeout)
|
|
||||||
if timeout <= 0:
|
|
||||||
timeout = None
|
|
||||||
|
|
||||||
insecure = strutils.bool_from_string(kwargs.get('insecure'))
|
|
||||||
verify = kwargs.get('verify')
|
|
||||||
if verify is None:
|
|
||||||
if insecure:
|
|
||||||
verify = False
|
|
||||||
else:
|
|
||||||
verify = kwargs.get('cacert') or True
|
|
||||||
|
|
||||||
cert = kwargs.get('cert_file')
|
|
||||||
key = kwargs.get('key_file')
|
|
||||||
if cert and key:
|
|
||||||
cert = cert, key
|
|
||||||
return {'verify': verify, 'cert': cert, 'timeout': timeout}
|
|
||||||
|
|
||||||
|
|
||||||
def get_client(version, **kwargs):
|
|
||||||
"""Get an authenticated client, based on the credentials in the kwargs.
|
|
||||||
|
|
||||||
:param api_version: the API version to use ('1')
|
|
||||||
:param kwargs: keyword args containing credentials, either:
|
|
||||||
|
|
||||||
* session: a keystoneauth/keystoneclient session object
|
|
||||||
* service_type: The default service_type for URL discovery
|
|
||||||
* service_name: The default service_name for URL discovery
|
|
||||||
* interface: The default interface for URL discovery
|
|
||||||
(Default: public)
|
|
||||||
* region_name: The default region_name for URL discovery
|
|
||||||
* endpoint_override: Always use this endpoint URL for requests
|
|
||||||
for this solumclient
|
|
||||||
* auth: An auth plugin to use instead of the session one
|
|
||||||
* user_agent: The User-Agent string to set
|
|
||||||
(Default is python-solumclient)
|
|
||||||
* connect_retries: the maximum number of retries that should be
|
|
||||||
attempted for connection errors
|
|
||||||
* logger: A logging object
|
|
||||||
|
|
||||||
or (DEPRECATED):
|
|
||||||
|
|
||||||
* os_token: pre-existing token to re-use
|
|
||||||
* os_endpoint: Solum API endpoint
|
|
||||||
|
|
||||||
or (DEPRECATED):
|
|
||||||
|
|
||||||
* os_username: name of user
|
|
||||||
* os_password: user's password
|
|
||||||
* os_user_id: user's id
|
|
||||||
* os_user_domain_id: the domain id of the user
|
|
||||||
* os_user_domain_name: the domain name of the user
|
|
||||||
* os_project_id: the user project id
|
|
||||||
* os_tenant_id: V2 alternative to os_project_id
|
|
||||||
* os_project_name: the user project name
|
|
||||||
* os_tenant_name: V2 alternative to os_project_name
|
|
||||||
* os_project_domain_name: domain name for the user project
|
|
||||||
* os_project_domain_id: domain id for the user project
|
|
||||||
* os_auth_url: endpoint to authenticate against
|
|
||||||
* os_cert|os_cacert: path of CA TLS certificate
|
|
||||||
* os_key: SSL private key
|
|
||||||
* insecure: allow insecure SSL (no cert verification)
|
|
||||||
"""
|
|
||||||
endpoint = kwargs.get('os_endpoint')
|
|
||||||
|
|
||||||
cli_kwargs = {
|
|
||||||
'username': kwargs.get('os_username'),
|
|
||||||
'password': kwargs.get('os_password'),
|
|
||||||
'tenant_id': (kwargs.get('os_tenant_id')
|
|
||||||
or kwargs.get('os_project_id')),
|
|
||||||
'tenant_name': (kwargs.get('os_tenant_name')
|
|
||||||
or kwargs.get('os_project_name')),
|
|
||||||
'auth_url': kwargs.get('os_auth_url'),
|
|
||||||
'region_name': kwargs.get('os_region_name'),
|
|
||||||
'service_type': kwargs.get('os_service_type'),
|
|
||||||
'endpoint_type': kwargs.get('os_endpoint_type'),
|
|
||||||
'cacert': kwargs.get('os_cacert'),
|
|
||||||
'cert_file': kwargs.get('os_cert'),
|
|
||||||
'key_file': kwargs.get('os_key'),
|
|
||||||
'token': kwargs.get('os_token') or kwargs.get('os_auth_token'),
|
|
||||||
'user_domain_name': kwargs.get('os_user_domain_name'),
|
|
||||||
'user_domain_id': kwargs.get('os_user_domain_id'),
|
|
||||||
'project_domain_name': kwargs.get('os_project_domain_name'),
|
|
||||||
'project_domain_id': kwargs.get('os_project_domain_id'),
|
|
||||||
}
|
|
||||||
|
|
||||||
cli_kwargs.update(kwargs)
|
|
||||||
cli_kwargs.update(_adjust_params(cli_kwargs))
|
|
||||||
|
|
||||||
return Client(version, endpoint, **cli_kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def get_auth_plugin(**kwargs):
|
|
||||||
auth_plugin = auth.KeystoneAuthPlugin(
|
|
||||||
auth_url=kwargs.get('auth_url'),
|
|
||||||
service_type=kwargs.get('service_type'),
|
|
||||||
token=kwargs.get('token'),
|
|
||||||
endpoint_type=kwargs.get('endpoint_type'),
|
|
||||||
endpoint=kwargs.get('endpoint'),
|
|
||||||
cacert=kwargs.get('cacert'),
|
|
||||||
tenant_id=kwargs.get('project_id') or kwargs.get('tenant_id'),
|
|
||||||
username=kwargs.get('username'),
|
|
||||||
password=kwargs.get('password'),
|
|
||||||
tenant_name=kwargs.get('tenant_name') or kwargs.get('project_name'),
|
|
||||||
user_domain_name=kwargs.get('user_domain_name'),
|
|
||||||
user_domain_id=kwargs.get('user_domain_id'),
|
|
||||||
project_domain_name=kwargs.get('project_domain_name'),
|
|
||||||
project_domain_id=kwargs.get('project_domain_id')
|
|
||||||
)
|
|
||||||
return auth_plugin
|
|
||||||
|
|
||||||
|
|
||||||
LEGACY_OPTS = ('auth_plugin', 'auth_url', 'token', 'insecure', 'cacert',
|
|
||||||
'tenant_id', 'project_id', 'username', 'password',
|
|
||||||
'project_name', 'tenant_name',
|
|
||||||
'user_domain_name', 'user_domain_id',
|
|
||||||
'project_domain_name', 'project_domain_id',
|
|
||||||
'key_file', 'cert_file', 'verify', 'timeout', 'cert')
|
|
||||||
|
|
||||||
|
|
||||||
def construct_http_client(**kwargs):
|
|
||||||
kwargs = kwargs.copy()
|
|
||||||
if kwargs.get('session') is not None:
|
|
||||||
# Drop legacy options
|
|
||||||
for opt in LEGACY_OPTS:
|
|
||||||
kwargs.pop(opt, None)
|
|
||||||
|
|
||||||
service_type_get = kwargs.pop('service_type',
|
|
||||||
'application_deployment')
|
|
||||||
|
|
||||||
return SessionClient(
|
|
||||||
session=kwargs.pop('session'),
|
|
||||||
service_type=service_type_get or 'application_deployment',
|
|
||||||
interface=kwargs.pop('interface', kwargs.pop('endpoint_type',
|
|
||||||
'publicURL')),
|
|
||||||
region_name=kwargs.pop('region_name', None),
|
|
||||||
user_agent=kwargs.pop('user_agent', 'python-solumclient'),
|
|
||||||
auth=kwargs.get('auth', None),
|
|
||||||
timings=kwargs.pop('timings', None),
|
|
||||||
**kwargs)
|
|
||||||
else:
|
|
||||||
return api_client.BaseClient(api_client.HTTPClient(
|
|
||||||
auth_plugin=kwargs.get('auth_plugin'),
|
|
||||||
region_name=kwargs.get('region_name'),
|
|
||||||
endpoint_type=kwargs.get('endpoint_type'),
|
|
||||||
original_ip=kwargs.get('original_ip'),
|
|
||||||
verify=kwargs.get('verify'),
|
|
||||||
cert=kwargs.get('cert'),
|
|
||||||
timeout=kwargs.get('timeout'),
|
|
||||||
timings=kwargs.get('timings'),
|
|
||||||
keyring_saver=kwargs.get('keyring_saver'),
|
|
||||||
debug=kwargs.get('debug'),
|
|
||||||
user_agent=kwargs.get('user_agent'),
|
|
||||||
http=kwargs.get('http')
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
|
||||||
def record_time(times, enabled, *args):
|
|
||||||
"""Record the time of a specific action.
|
|
||||||
|
|
||||||
:param times: A list of tuples holds time data.
|
|
||||||
:type times: list
|
|
||||||
:param enabled: Whether timing is enabled.
|
|
||||||
:type enabled: bool
|
|
||||||
:param args: Other data to be stored besides time data, these args
|
|
||||||
will be joined to a string.
|
|
||||||
"""
|
|
||||||
if not enabled:
|
|
||||||
yield
|
|
||||||
else:
|
|
||||||
start = time.time()
|
|
||||||
yield
|
|
||||||
end = time.time()
|
|
||||||
times.append((' '.join(args), start, end))
|
|
||||||
|
|
||||||
|
|
||||||
class SessionClient(adapter.LegacyJsonAdapter):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.times = []
|
|
||||||
self.timings = kwargs.pop('timings', False)
|
|
||||||
super(SessionClient, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def request(self, url, method, **kwargs):
|
|
||||||
kwargs.setdefault('headers', kwargs.get('headers', {}))
|
|
||||||
raise_exc = kwargs.pop('raise_exc', True)
|
|
||||||
with record_time(self.times, self.timings, method, url):
|
|
||||||
resp, body = super(SessionClient, self).request(url,
|
|
||||||
method,
|
|
||||||
raise_exc=False,
|
|
||||||
**kwargs)
|
|
||||||
|
|
||||||
if raise_exc and resp.status_code >= 400:
|
|
||||||
raise exc.from_response(resp, body)
|
|
||||||
return resp
|
|
@@ -1,216 +0,0 @@
|
|||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# Copyright 2013 Spanish National Research Council.
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
# E0202: An attribute inherited from %s hide this method
|
|
||||||
# pylint: disable=E0202
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import argparse
|
|
||||||
import os
|
|
||||||
|
|
||||||
from stevedore import extension
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
|
|
||||||
|
|
||||||
_discovered_plugins = {}
|
|
||||||
|
|
||||||
|
|
||||||
def discover_auth_systems():
|
|
||||||
"""Discover the available auth-systems.
|
|
||||||
|
|
||||||
This won't take into account the old style auth-systems.
|
|
||||||
"""
|
|
||||||
global _discovered_plugins
|
|
||||||
_discovered_plugins = {}
|
|
||||||
|
|
||||||
def add_plugin(ext):
|
|
||||||
_discovered_plugins[ext.name] = ext.plugin
|
|
||||||
|
|
||||||
ep_namespace = "solumclient.common.apiclient.auth"
|
|
||||||
mgr = extension.ExtensionManager(ep_namespace)
|
|
||||||
mgr.map(add_plugin)
|
|
||||||
|
|
||||||
|
|
||||||
def load_auth_system_opts(parser):
|
|
||||||
"""Load options needed by the available auth-systems into a parser.
|
|
||||||
|
|
||||||
This function will try to populate the parser with options from the
|
|
||||||
available plugins.
|
|
||||||
"""
|
|
||||||
group = parser.add_argument_group("Common auth options")
|
|
||||||
BaseAuthPlugin.add_common_opts(group)
|
|
||||||
for name, auth_plugin in _discovered_plugins.items():
|
|
||||||
group = parser.add_argument_group(
|
|
||||||
"Auth-system '%s' options" % name,
|
|
||||||
conflict_handler="resolve")
|
|
||||||
auth_plugin.add_opts(group)
|
|
||||||
|
|
||||||
|
|
||||||
def load_plugin(auth_system):
|
|
||||||
try:
|
|
||||||
plugin_class = _discovered_plugins[auth_system]
|
|
||||||
except KeyError:
|
|
||||||
raise exceptions.AuthSystemNotFound(auth_system)
|
|
||||||
return plugin_class(auth_system=auth_system)
|
|
||||||
|
|
||||||
|
|
||||||
def load_plugin_from_args(args):
|
|
||||||
"""Load required plugin and populate it with options.
|
|
||||||
|
|
||||||
Try to guess auth system if it is not specified. Systems are tried in
|
|
||||||
alphabetical order.
|
|
||||||
|
|
||||||
:type args: argparse.Namespace
|
|
||||||
:raises: AuthPluginOptionsMissing
|
|
||||||
"""
|
|
||||||
auth_system = args.os_auth_system
|
|
||||||
if auth_system:
|
|
||||||
plugin = load_plugin(auth_system)
|
|
||||||
plugin.parse_opts(args)
|
|
||||||
plugin.sufficient_options()
|
|
||||||
return plugin
|
|
||||||
|
|
||||||
for plugin_auth_system in sorted(_discovered_plugins.keys()):
|
|
||||||
plugin_class = _discovered_plugins[plugin_auth_system]
|
|
||||||
plugin = plugin_class()
|
|
||||||
plugin.parse_opts(args)
|
|
||||||
try:
|
|
||||||
plugin.sufficient_options()
|
|
||||||
except exceptions.AuthPluginOptionsMissing:
|
|
||||||
continue
|
|
||||||
return plugin
|
|
||||||
raise exceptions.AuthPluginOptionsMissing(["auth_system"])
|
|
||||||
|
|
||||||
|
|
||||||
class BaseAuthPlugin(object, metaclass=abc.ABCMeta):
|
|
||||||
"""Base class for authentication plugins.
|
|
||||||
|
|
||||||
An authentication plugin needs to override at least the authenticate
|
|
||||||
method to be a valid plugin.
|
|
||||||
"""
|
|
||||||
|
|
||||||
auth_system = None
|
|
||||||
opt_names = []
|
|
||||||
common_opt_names = [
|
|
||||||
"auth_system",
|
|
||||||
"username",
|
|
||||||
"password",
|
|
||||||
"tenant_name",
|
|
||||||
"token",
|
|
||||||
"auth_url",
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, auth_system=None, **kwargs):
|
|
||||||
self.auth_system = auth_system or self.auth_system
|
|
||||||
self.opts = dict((name, kwargs.get(name))
|
|
||||||
for name in self.opt_names)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _parser_add_opt(parser, opt):
|
|
||||||
"""Add an option to parser in two variants.
|
|
||||||
|
|
||||||
:param opt: option name (with underscores)
|
|
||||||
"""
|
|
||||||
dashed_opt = opt.replace("_", "-")
|
|
||||||
env_var = "OS_%s" % opt.upper()
|
|
||||||
arg_default = os.environ.get(env_var, "")
|
|
||||||
arg_help = "Defaults to env[%s]." % env_var
|
|
||||||
parser.add_argument(
|
|
||||||
"--os-%s" % dashed_opt,
|
|
||||||
metavar="<%s>" % dashed_opt,
|
|
||||||
default=arg_default,
|
|
||||||
help=arg_help)
|
|
||||||
parser.add_argument(
|
|
||||||
"--os_%s" % opt,
|
|
||||||
metavar="<%s>" % dashed_opt,
|
|
||||||
help=argparse.SUPPRESS)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_opts(cls, parser):
|
|
||||||
"""Populate the parser with the options for this plugin."""
|
|
||||||
for opt in cls.opt_names:
|
|
||||||
# use `BaseAuthPlugin.common_opt_names` since it is never
|
|
||||||
# changed in child classes
|
|
||||||
if opt not in BaseAuthPlugin.common_opt_names:
|
|
||||||
cls._parser_add_opt(parser, opt)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_common_opts(cls, parser):
|
|
||||||
"""Add options that are common for several plugins."""
|
|
||||||
for opt in cls.common_opt_names:
|
|
||||||
cls._parser_add_opt(parser, opt)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_opt(opt_name, args):
|
|
||||||
"""Return option name and value.
|
|
||||||
|
|
||||||
:param opt_name: name of the option, e.g., "username"
|
|
||||||
:param args: parsed arguments
|
|
||||||
"""
|
|
||||||
return (opt_name, getattr(args, "os_%s" % opt_name, None))
|
|
||||||
|
|
||||||
def parse_opts(self, args):
|
|
||||||
"""Parse the actual auth-system options if any.
|
|
||||||
|
|
||||||
This method is expected to populate the attribute `self.opts` with a
|
|
||||||
dict containing the options and values needed to make authentication.
|
|
||||||
"""
|
|
||||||
self.opts.update(dict(self.get_opt(opt_name, args)
|
|
||||||
for opt_name in self.opt_names))
|
|
||||||
|
|
||||||
def authenticate(self, http_client):
|
|
||||||
"""Authenticate using plugin defined method.
|
|
||||||
|
|
||||||
The method usually analyses `self.opts` and performs
|
|
||||||
a request to authentication server.
|
|
||||||
|
|
||||||
:param http_client: client object that needs authentication
|
|
||||||
:type http_client: HTTPClient
|
|
||||||
:raises: AuthorizationFailure
|
|
||||||
"""
|
|
||||||
self.sufficient_options()
|
|
||||||
self._do_authenticate(http_client)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def _do_authenticate(self, http_client):
|
|
||||||
"""Protected method for authentication."""
|
|
||||||
|
|
||||||
def sufficient_options(self):
|
|
||||||
"""Check if all required options are present.
|
|
||||||
|
|
||||||
:raises: AuthPluginOptionsMissing
|
|
||||||
"""
|
|
||||||
missing = [opt
|
|
||||||
for opt in self.opt_names
|
|
||||||
if not self.opts.get(opt)]
|
|
||||||
if missing:
|
|
||||||
raise exceptions.AuthPluginOptionsMissing(missing)
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def token_and_endpoint(self, endpoint_type, service_type):
|
|
||||||
"""Return token and endpoint.
|
|
||||||
|
|
||||||
:param service_type: Service type of the endpoint
|
|
||||||
:type service_type: string
|
|
||||||
:param endpoint_type: Type of endpoint.
|
|
||||||
Possible values: public or publicURL,
|
|
||||||
internal or internalURL,
|
|
||||||
admin or adminURL
|
|
||||||
:type endpoint_type: string
|
|
||||||
:returns: tuple of token and endpoint strings
|
|
||||||
:raises: EndpointException
|
|
||||||
"""
|
|
@@ -1,517 +0,0 @@
|
|||||||
# Copyright 2010 Jacob Kaplan-Moss
|
|
||||||
# Copyright 2011 OpenStack Foundation
|
|
||||||
# Copyright 2012 Grid Dynamics
|
|
||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Base utilities to build API operation managers and objects on top of.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# E1102: %s is not callable
|
|
||||||
# pylint: disable=E1102
|
|
||||||
|
|
||||||
import abc
|
|
||||||
import copy
|
|
||||||
|
|
||||||
from urllib import parse
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.i18n import _
|
|
||||||
|
|
||||||
from oslo_utils import strutils
|
|
||||||
|
|
||||||
|
|
||||||
def getid(obj):
|
|
||||||
"""Return id if argument is a Resource.
|
|
||||||
|
|
||||||
Abstracts the common pattern of allowing both an object or an object's ID
|
|
||||||
(UUID) as a parameter when dealing with relationships.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if obj.uuid:
|
|
||||||
return obj.uuid
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return obj.id
|
|
||||||
except AttributeError:
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
|
|
||||||
class HookableMixin(object):
|
|
||||||
"""Mixin so classes can register and run hooks."""
|
|
||||||
_hooks_map = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def add_hook(cls, hook_type, hook_func):
|
|
||||||
"""Add a new hook of specified type.
|
|
||||||
|
|
||||||
:param cls: class that registers hooks
|
|
||||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
|
||||||
:param hook_func: hook function
|
|
||||||
"""
|
|
||||||
if hook_type not in cls._hooks_map:
|
|
||||||
cls._hooks_map[hook_type] = []
|
|
||||||
|
|
||||||
cls._hooks_map[hook_type].append(hook_func)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
|
||||||
"""Run all hooks of specified type.
|
|
||||||
|
|
||||||
:param cls: class that registers hooks
|
|
||||||
:param hook_type: hook type, e.g., '__pre_parse_args__'
|
|
||||||
:param args: args to be passed to every hook function
|
|
||||||
:param kwargs: kwargs to be passed to every hook function
|
|
||||||
"""
|
|
||||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
|
||||||
for hook_func in hook_funcs:
|
|
||||||
hook_func(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseManager(HookableMixin):
|
|
||||||
"""Basic manager type providing common operations.
|
|
||||||
|
|
||||||
Managers interact with a particular type of API (servers, flavors, images,
|
|
||||||
etc.) and provide CRUD operations for them.
|
|
||||||
"""
|
|
||||||
resource_class = None
|
|
||||||
|
|
||||||
def __init__(self, client):
|
|
||||||
"""Initializes BaseManager with `client`.
|
|
||||||
|
|
||||||
:param client: instance of BaseClient descendant for HTTP requests
|
|
||||||
"""
|
|
||||||
super(BaseManager, self).__init__()
|
|
||||||
self.client = client
|
|
||||||
|
|
||||||
def _list(self, url, response_key=None, obj_class=None, json=None):
|
|
||||||
"""List the collection.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'servers'. If response_key is None - all response body
|
|
||||||
will be used.
|
|
||||||
:param obj_class: class for constructing the returned objects
|
|
||||||
(self.resource_class will be used by default)
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
"""
|
|
||||||
if json:
|
|
||||||
body = self.client.post(url, json=json).json()
|
|
||||||
else:
|
|
||||||
body = self.client.get(url).json()
|
|
||||||
|
|
||||||
if obj_class is None:
|
|
||||||
obj_class = self.resource_class
|
|
||||||
|
|
||||||
data = body[response_key] if response_key is not None else body
|
|
||||||
# NOTE(ja): keystone returns values as list as {'values': [ ... ]}
|
|
||||||
# unlike other services which just return the list...
|
|
||||||
try:
|
|
||||||
data = data['values']
|
|
||||||
except (KeyError, TypeError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
|
||||||
|
|
||||||
def _get(self, url, response_key=None):
|
|
||||||
"""Get an object from collection.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'server'. If response_key is None - all response body
|
|
||||||
will be used.
|
|
||||||
"""
|
|
||||||
body = self.client.get(url).json()
|
|
||||||
data = body[response_key] if response_key is not None else body
|
|
||||||
return self.resource_class(self, data, loaded=True)
|
|
||||||
|
|
||||||
def _head(self, url):
|
|
||||||
"""Retrieve request headers for an object.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
"""
|
|
||||||
resp = self.client.head(url)
|
|
||||||
return resp.status_code == 204
|
|
||||||
|
|
||||||
def _post(self, url, json, response_key=None, return_raw=False):
|
|
||||||
"""Create an object.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'server'. If response_key is None - all response body
|
|
||||||
will be used.
|
|
||||||
:param return_raw: flag to force returning raw JSON instead of
|
|
||||||
Python object of self.resource_class
|
|
||||||
"""
|
|
||||||
body = self.client.post(url, json=json).json()
|
|
||||||
data = body[response_key] if response_key is not None else body
|
|
||||||
if return_raw:
|
|
||||||
return data
|
|
||||||
return self.resource_class(self, data)
|
|
||||||
|
|
||||||
def _put(self, url, json=None, response_key=None):
|
|
||||||
"""Update an object with PUT method.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'servers'. If response_key is None - all response body
|
|
||||||
will be used.
|
|
||||||
"""
|
|
||||||
resp = self.client.put(url, json=json)
|
|
||||||
# PUT requests may not return a body
|
|
||||||
if resp.content:
|
|
||||||
body = resp.json()
|
|
||||||
if response_key is not None:
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
else:
|
|
||||||
return self.resource_class(self, body)
|
|
||||||
|
|
||||||
def _patch(self, url, json=None, response_key=None):
|
|
||||||
"""Update an object with PATCH method.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers'
|
|
||||||
:param json: data that will be encoded as JSON and passed in POST
|
|
||||||
request (GET will be sent by default)
|
|
||||||
:param response_key: the key to be looked up in response dictionary,
|
|
||||||
e.g., 'servers'. If response_key is None - all response body
|
|
||||||
will be used.
|
|
||||||
"""
|
|
||||||
body = self.client.patch(url, json=json).json()
|
|
||||||
if response_key is not None:
|
|
||||||
return self.resource_class(self, body[response_key])
|
|
||||||
else:
|
|
||||||
return self.resource_class(self, body)
|
|
||||||
|
|
||||||
def _delete(self, url):
|
|
||||||
"""Delete an object.
|
|
||||||
|
|
||||||
:param url: a partial URL, e.g., '/servers/my-server'
|
|
||||||
"""
|
|
||||||
return self.client.delete(url)
|
|
||||||
|
|
||||||
|
|
||||||
class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta):
|
|
||||||
"""Manager with additional `find()`/`findall()` methods."""
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def list(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
"""Find a single item with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
This isn't very efficient: it loads the entire list then filters on
|
|
||||||
the Python side.
|
|
||||||
"""
|
|
||||||
matches = self.findall(**kwargs)
|
|
||||||
num_matches = len(matches)
|
|
||||||
if num_matches == 0:
|
|
||||||
msg = _("No %(name)s matching %(args)s.") % {
|
|
||||||
'name': self.resource_class.__name__,
|
|
||||||
'args': kwargs
|
|
||||||
}
|
|
||||||
raise exceptions.NotFound(msg)
|
|
||||||
elif num_matches > 1:
|
|
||||||
raise exceptions.NoUniqueMatch()
|
|
||||||
else:
|
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
def findall(self, **kwargs):
|
|
||||||
"""Find all items with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
This isn't very efficient: it loads the entire list then filters on
|
|
||||||
the Python side.
|
|
||||||
"""
|
|
||||||
found = []
|
|
||||||
searches = kwargs.items()
|
|
||||||
|
|
||||||
for obj in self.list():
|
|
||||||
try:
|
|
||||||
if all(getattr(obj, attr) == value
|
|
||||||
for (attr, value) in searches):
|
|
||||||
found.append(obj)
|
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
|
||||||
class CrudManager(BaseManager):
|
|
||||||
"""Base manager class for manipulating entities.
|
|
||||||
|
|
||||||
Children of this class are expected to define a `collection_key` and `key`.
|
|
||||||
|
|
||||||
- `collection_key`: Usually a plural noun by convention (e.g. `entities`);
|
|
||||||
used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
|
|
||||||
objects containing a list of member resources (e.g. `{'entities': [{},
|
|
||||||
{}, {}]}`).
|
|
||||||
- `key`: Usually a singular noun by convention (e.g. `entity`); used to
|
|
||||||
refer to an individual member of the collection.
|
|
||||||
|
|
||||||
"""
|
|
||||||
collection_key = None
|
|
||||||
key = None
|
|
||||||
|
|
||||||
def build_url(self, base_url=None, **kwargs):
|
|
||||||
"""Builds a resource URL for the given kwargs.
|
|
||||||
|
|
||||||
Given an example collection where `collection_key = 'entities'` and
|
|
||||||
`key = 'entity'`, the following URL's could be generated.
|
|
||||||
|
|
||||||
By default, the URL will represent a collection of entities, e.g.::
|
|
||||||
|
|
||||||
/entities
|
|
||||||
|
|
||||||
If kwargs contains an `entity_id`, then the URL will represent a
|
|
||||||
specific member, e.g.::
|
|
||||||
|
|
||||||
/entities/{entity_id}
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
url = base_url if base_url is not None else ''
|
|
||||||
|
|
||||||
url += '/%s' % self.collection_key
|
|
||||||
|
|
||||||
# do we have a specific entity?
|
|
||||||
entity_id = kwargs.get('%s_id' % self.key)
|
|
||||||
if entity_id is not None:
|
|
||||||
url += '/%s' % entity_id
|
|
||||||
|
|
||||||
return url
|
|
||||||
|
|
||||||
def _filter_kwargs(self, kwargs):
|
|
||||||
"""Drop null values and handle ids."""
|
|
||||||
for key, ref in kwargs.copy().items():
|
|
||||||
if ref is None:
|
|
||||||
kwargs.pop(key)
|
|
||||||
else:
|
|
||||||
if isinstance(ref, Resource):
|
|
||||||
kwargs.pop(key)
|
|
||||||
kwargs['%s_id' % key] = getid(ref)
|
|
||||||
return kwargs
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._post(
|
|
||||||
self.build_url(**kwargs),
|
|
||||||
{self.key: kwargs},
|
|
||||||
self.key)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._get(
|
|
||||||
self.build_url(**kwargs),
|
|
||||||
self.key)
|
|
||||||
|
|
||||||
def head(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._head(self.build_url(**kwargs))
|
|
||||||
|
|
||||||
def list(self, base_url=None, **kwargs):
|
|
||||||
"""List the collection.
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
return self._list(
|
|
||||||
'%(base_url)s%(query)s' % {
|
|
||||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
|
||||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
|
||||||
},
|
|
||||||
self.collection_key)
|
|
||||||
|
|
||||||
def put(self, base_url=None, **kwargs):
|
|
||||||
"""Update an element.
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
return self._put(self.build_url(base_url=base_url, **kwargs))
|
|
||||||
|
|
||||||
def update(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
params = kwargs.copy()
|
|
||||||
params.pop('%s_id' % self.key)
|
|
||||||
|
|
||||||
return self._patch(
|
|
||||||
self.build_url(**kwargs),
|
|
||||||
{self.key: params},
|
|
||||||
self.key)
|
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
return self._delete(
|
|
||||||
self.build_url(**kwargs))
|
|
||||||
|
|
||||||
def find(self, base_url=None, **kwargs):
|
|
||||||
"""Find a single item with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
rl = self._list(
|
|
||||||
'%(base_url)s%(query)s' % {
|
|
||||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
|
||||||
'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
|
|
||||||
},
|
|
||||||
self.collection_key)
|
|
||||||
num = len(rl)
|
|
||||||
|
|
||||||
if num == 0:
|
|
||||||
msg = _("No %(name)s matching %(args)s.") % {
|
|
||||||
'name': self.resource_class.__name__,
|
|
||||||
'args': kwargs
|
|
||||||
}
|
|
||||||
raise exceptions.NotFound(404, msg)
|
|
||||||
elif num > 1:
|
|
||||||
raise exceptions.NoUniqueMatch
|
|
||||||
else:
|
|
||||||
return rl[0]
|
|
||||||
|
|
||||||
|
|
||||||
class Extension(HookableMixin):
|
|
||||||
"""Extension descriptor."""
|
|
||||||
|
|
||||||
SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
|
|
||||||
manager_class = None
|
|
||||||
|
|
||||||
def __init__(self, name, module):
|
|
||||||
super(Extension, self).__init__()
|
|
||||||
self.name = name
|
|
||||||
self.module = module
|
|
||||||
self._parse_extension_module()
|
|
||||||
|
|
||||||
def _parse_extension_module(self):
|
|
||||||
self.manager_class = None
|
|
||||||
for attr_name, attr_value in self.module.__dict__.items():
|
|
||||||
if attr_name in self.SUPPORTED_HOOKS:
|
|
||||||
self.add_hook(attr_name, attr_value)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
if issubclass(attr_value, BaseManager):
|
|
||||||
self.manager_class = attr_value
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Extension '%s'>" % self.name
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(object):
|
|
||||||
"""Base class for OpenStack resources (tenant, user, etc.).
|
|
||||||
|
|
||||||
This is pretty much just a bag for attributes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
HUMAN_ID = False
|
|
||||||
NAME_ATTR = 'name'
|
|
||||||
|
|
||||||
def __init__(self, manager, info, loaded=False):
|
|
||||||
"""Populate and bind to a manager.
|
|
||||||
|
|
||||||
:param manager: BaseManager object
|
|
||||||
:param info: dictionary representing resource attributes
|
|
||||||
:param loaded: prevent lazy-loading if set to True
|
|
||||||
"""
|
|
||||||
self.manager = manager
|
|
||||||
self._info = info
|
|
||||||
self._add_details(info)
|
|
||||||
self._loaded = loaded
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
reprkeys = sorted(k
|
|
||||||
for k in self.__dict__.keys()
|
|
||||||
if k[0] != '_' and k != 'manager')
|
|
||||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
|
||||||
return "<%s %s>" % (self.__class__.__name__, info)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def human_id(self):
|
|
||||||
"""Human-readable ID which can be used for bash completion."""
|
|
||||||
if self.HUMAN_ID:
|
|
||||||
name = getattr(self, self.NAME_ATTR, None)
|
|
||||||
if name is not None:
|
|
||||||
return strutils.to_slug(name)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _add_details(self, info):
|
|
||||||
for (k, v) in info.items():
|
|
||||||
try:
|
|
||||||
setattr(self, k, v)
|
|
||||||
self._info[k] = v
|
|
||||||
except AttributeError:
|
|
||||||
# In this case we already defined the attribute on the class
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __getattr__(self, k):
|
|
||||||
if k not in self.__dict__:
|
|
||||||
# NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
|
||||||
if not self.is_loaded():
|
|
||||||
self.get()
|
|
||||||
return self.__getattr__(k)
|
|
||||||
|
|
||||||
raise AttributeError(k)
|
|
||||||
else:
|
|
||||||
return self.__dict__[k]
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
"""Support for lazy loading details.
|
|
||||||
|
|
||||||
Some clients, such as novaclient have the option to lazy load the
|
|
||||||
details, details which can be loaded with this function.
|
|
||||||
"""
|
|
||||||
# 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, Resource):
|
|
||||||
return NotImplemented
|
|
||||||
# two resources of different types are not equal
|
|
||||||
if not isinstance(other, self.__class__):
|
|
||||||
return False
|
|
||||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
|
||||||
return self.id == other.id
|
|
||||||
return self._info == other._info
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
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,377 +0,0 @@
|
|||||||
# Copyright 2010 Jacob Kaplan-Moss
|
|
||||||
# Copyright 2011 OpenStack Foundation
|
|
||||||
# Copyright 2011 Piston Cloud Computing, Inc.
|
|
||||||
# Copyright 2013 Alessio Ababilov
|
|
||||||
# Copyright 2013 Grid Dynamics
|
|
||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
OpenStack Client interface. Handles the REST calls and responses.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# E0202: An attribute inherited from %s hide this method
|
|
||||||
# pylint: disable=E0202
|
|
||||||
|
|
||||||
import time
|
|
||||||
|
|
||||||
try:
|
|
||||||
import simplejson as json
|
|
||||||
except ImportError:
|
|
||||||
import json
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
from oslo_utils import importutils
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(object):
|
|
||||||
"""This client handles sending HTTP requests to OpenStack servers.
|
|
||||||
|
|
||||||
Features:
|
|
||||||
|
|
||||||
- share authentication information between several clients to different
|
|
||||||
services (e.g., for compute and image clients);
|
|
||||||
- reissue authentication request for expired tokens;
|
|
||||||
- encode/decode JSON bodies;
|
|
||||||
- raise exceptions on HTTP errors;
|
|
||||||
- pluggable authentication;
|
|
||||||
- store authentication information in a keyring;
|
|
||||||
- store time spent for requests;
|
|
||||||
- register clients for particular services, so one can use
|
|
||||||
`http_client.identity` or `http_client.compute`;
|
|
||||||
- log requests and responses in a format that is easy to copy-and-paste
|
|
||||||
into terminal and send the same request with curl.
|
|
||||||
"""
|
|
||||||
|
|
||||||
user_agent = "solumclient.common.apiclient"
|
|
||||||
|
|
||||||
def __init__(self,
|
|
||||||
auth_plugin,
|
|
||||||
region_name=None,
|
|
||||||
endpoint_type="publicURL",
|
|
||||||
original_ip=None,
|
|
||||||
verify=True,
|
|
||||||
cert=None,
|
|
||||||
timeout=None,
|
|
||||||
timings=False,
|
|
||||||
keyring_saver=None,
|
|
||||||
debug=False,
|
|
||||||
user_agent=None,
|
|
||||||
http=None):
|
|
||||||
self.auth_plugin = auth_plugin
|
|
||||||
|
|
||||||
self.endpoint_type = endpoint_type
|
|
||||||
self.region_name = region_name
|
|
||||||
|
|
||||||
self.original_ip = original_ip
|
|
||||||
self.timeout = timeout
|
|
||||||
self.verify = verify
|
|
||||||
self.cert = cert
|
|
||||||
|
|
||||||
self.keyring_saver = keyring_saver
|
|
||||||
self.debug = debug
|
|
||||||
self.user_agent = user_agent or self.user_agent
|
|
||||||
|
|
||||||
self.times = [] # [("item", starttime, endtime), ...]
|
|
||||||
self.timings = timings
|
|
||||||
|
|
||||||
# requests within the same session can reuse TCP connections from pool
|
|
||||||
self.http = http or requests.Session()
|
|
||||||
|
|
||||||
self.cached_token = None
|
|
||||||
|
|
||||||
def _http_log_req(self, method, url, kwargs):
|
|
||||||
if not self.debug:
|
|
||||||
return
|
|
||||||
|
|
||||||
string_parts = [
|
|
||||||
"curl -i",
|
|
||||||
"-X '%s'" % method,
|
|
||||||
"'%s'" % url,
|
|
||||||
]
|
|
||||||
|
|
||||||
for element in kwargs['headers']:
|
|
||||||
header = "-H '%s: %s'" % (element, kwargs['headers'][element])
|
|
||||||
string_parts.append(header)
|
|
||||||
|
|
||||||
_logger.debug("REQ: %s" % " ".join(string_parts))
|
|
||||||
if 'data' in kwargs:
|
|
||||||
_logger.debug("REQ BODY: %s\n" % (kwargs['data']))
|
|
||||||
|
|
||||||
def _http_log_resp(self, resp):
|
|
||||||
if not self.debug:
|
|
||||||
return
|
|
||||||
_logger.debug(
|
|
||||||
"RESP: [%s] %s\n",
|
|
||||||
resp.status_code,
|
|
||||||
resp.headers)
|
|
||||||
if resp._content_consumed:
|
|
||||||
_logger.debug(
|
|
||||||
"RESP BODY: %s\n",
|
|
||||||
resp.text)
|
|
||||||
|
|
||||||
def serialize(self, kwargs):
|
|
||||||
if kwargs.get('json') is not None:
|
|
||||||
kwargs['headers']['Content-Type'] = 'application/json'
|
|
||||||
kwargs['data'] = json.dumps(kwargs['json'])
|
|
||||||
try:
|
|
||||||
del kwargs['json']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_timings(self):
|
|
||||||
return self.times
|
|
||||||
|
|
||||||
def reset_timings(self):
|
|
||||||
self.times = []
|
|
||||||
|
|
||||||
def request(self, method, url, **kwargs):
|
|
||||||
"""Send an http request with the specified characteristics.
|
|
||||||
|
|
||||||
Wrapper around `requests.Session.request` to handle tasks such as
|
|
||||||
setting headers, JSON encoding/decoding, and error handling.
|
|
||||||
|
|
||||||
:param method: method of HTTP request
|
|
||||||
:param url: URL of HTTP request
|
|
||||||
:param kwargs: any other parameter that can be passed to
|
|
||||||
requests.Session.request (such as `headers`) or `json`
|
|
||||||
that will be encoded as JSON and used as `data` argument
|
|
||||||
"""
|
|
||||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
|
||||||
kwargs["headers"]["User-Agent"] = self.user_agent
|
|
||||||
if self.original_ip:
|
|
||||||
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
|
||||||
self.original_ip, self.user_agent)
|
|
||||||
if self.timeout is not None:
|
|
||||||
kwargs.setdefault("timeout", self.timeout)
|
|
||||||
kwargs.setdefault("verify", self.verify)
|
|
||||||
if self.cert is not None:
|
|
||||||
kwargs.setdefault("cert", self.cert)
|
|
||||||
self.serialize(kwargs)
|
|
||||||
|
|
||||||
self._http_log_req(method, url, kwargs)
|
|
||||||
if self.timings:
|
|
||||||
start_time = time.time()
|
|
||||||
resp = self.http.request(method, url, **kwargs)
|
|
||||||
if self.timings:
|
|
||||||
self.times.append(("%s %s" % (method, url),
|
|
||||||
start_time, time.time()))
|
|
||||||
self._http_log_resp(resp)
|
|
||||||
|
|
||||||
if resp.status_code >= 400:
|
|
||||||
_logger.debug(
|
|
||||||
"Request returned failure status: %s",
|
|
||||||
resp.status_code)
|
|
||||||
raise exceptions.from_response(resp, method, url)
|
|
||||||
|
|
||||||
return resp
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def concat_url(endpoint, url):
|
|
||||||
"""Concatenate endpoint and final URL.
|
|
||||||
|
|
||||||
E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to
|
|
||||||
"http://keystone/v2.0/tokens".
|
|
||||||
|
|
||||||
:param endpoint: the base URL
|
|
||||||
:param url: the final URL
|
|
||||||
"""
|
|
||||||
return "%s/%s" % (endpoint.rstrip("/"), url.strip("/"))
|
|
||||||
|
|
||||||
def client_request(self, client, method, url, **kwargs):
|
|
||||||
"""Send an http request using `client`'s endpoint and specified `url`.
|
|
||||||
|
|
||||||
If request was rejected as unauthorized (possibly because the token is
|
|
||||||
expired), issue one authorization attempt and send the request once
|
|
||||||
again.
|
|
||||||
|
|
||||||
:param client: instance of BaseClient descendant
|
|
||||||
:param method: method of HTTP request
|
|
||||||
:param url: URL of HTTP request
|
|
||||||
:param kwargs: any other parameter that can be passed to
|
|
||||||
`HTTPClient.request`
|
|
||||||
"""
|
|
||||||
|
|
||||||
filter_args = {
|
|
||||||
"endpoint_type": client.endpoint_type or self.endpoint_type,
|
|
||||||
"service_type": client.service_type,
|
|
||||||
}
|
|
||||||
token, endpoint = (self.cached_token, client.cached_endpoint)
|
|
||||||
just_authenticated = False
|
|
||||||
if not (token and endpoint):
|
|
||||||
try:
|
|
||||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
||||||
**filter_args)
|
|
||||||
except exceptions.EndpointException:
|
|
||||||
pass
|
|
||||||
if not (token and endpoint):
|
|
||||||
self.authenticate()
|
|
||||||
just_authenticated = True
|
|
||||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
||||||
**filter_args)
|
|
||||||
if not (token and endpoint):
|
|
||||||
raise exceptions.AuthorizationFailure(
|
|
||||||
_("Cannot find endpoint or token for request"))
|
|
||||||
|
|
||||||
old_token_endpoint = (token, endpoint)
|
|
||||||
kwargs.setdefault("headers", {})["X-Auth-Token"] = token
|
|
||||||
self.cached_token = token
|
|
||||||
client.cached_endpoint = endpoint
|
|
||||||
# Perform the request once. If we get Unauthorized, then it
|
|
||||||
# might be because the auth token expired, so try to
|
|
||||||
# re-authenticate and try again. If it still fails, bail.
|
|
||||||
try:
|
|
||||||
return self.request(
|
|
||||||
method, self.concat_url(endpoint, url), **kwargs)
|
|
||||||
except exceptions.ConnectionRefused:
|
|
||||||
print('ERROR: Cannot connect to %s. Make sure it ' +
|
|
||||||
'is valid: %s.' % endpoint)
|
|
||||||
raise
|
|
||||||
except exceptions.EndpointException:
|
|
||||||
print('ERROR: Service catalog endpoint is invalid. ' +
|
|
||||||
'Please check your ' +
|
|
||||||
'endpoint is valid: %s.' % endpoint)
|
|
||||||
raise
|
|
||||||
except requests.ConnectionError:
|
|
||||||
print('ERROR: Unable to connect to endpoint. ' +
|
|
||||||
'Make sure that ' +
|
|
||||||
'your endpoint (%s) is valid.' % endpoint)
|
|
||||||
raise
|
|
||||||
except exceptions.Unauthorized as unauth_ex:
|
|
||||||
if just_authenticated:
|
|
||||||
raise
|
|
||||||
self.cached_token = None
|
|
||||||
client.cached_endpoint = None
|
|
||||||
self.authenticate()
|
|
||||||
try:
|
|
||||||
token, endpoint = self.auth_plugin.token_and_endpoint(
|
|
||||||
**filter_args)
|
|
||||||
except exceptions.EndpointException:
|
|
||||||
raise unauth_ex
|
|
||||||
if (not (token and endpoint) or
|
|
||||||
old_token_endpoint == (token, endpoint)):
|
|
||||||
raise unauth_ex
|
|
||||||
self.cached_token = token
|
|
||||||
client.cached_endpoint = endpoint
|
|
||||||
kwargs["headers"]["X-Auth-Token"] = token
|
|
||||||
return self.request(
|
|
||||||
method, self.concat_url(endpoint, url), **kwargs)
|
|
||||||
|
|
||||||
def add_client(self, base_client_instance):
|
|
||||||
"""Add a new instance of :class:`BaseClient` descendant.
|
|
||||||
|
|
||||||
`self` will store a reference to `base_client_instance`.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> def test_clients():
|
|
||||||
... from keystoneclient.auth import keystone
|
|
||||||
... from openstack.apiclient import client
|
|
||||||
... auth = keystone.KeystoneAuthPlugin(
|
|
||||||
... username="user", password="pass", tenant_name="tenant",
|
|
||||||
... auth_url="http://auth:5000/v2.0")
|
|
||||||
... openstack_client = client.HTTPClient(auth)
|
|
||||||
... # create nova client
|
|
||||||
... from novaclient.v1_1 import client
|
|
||||||
... client.Client(openstack_client)
|
|
||||||
... # create keystone client
|
|
||||||
... from keystoneclient.v2_0 import client
|
|
||||||
... client.Client(openstack_client)
|
|
||||||
... # use them
|
|
||||||
... openstack_client.identity.tenants.list()
|
|
||||||
... openstack_client.compute.servers.list()
|
|
||||||
"""
|
|
||||||
service_type = base_client_instance.service_type
|
|
||||||
if service_type and not hasattr(self, service_type):
|
|
||||||
setattr(self, service_type, base_client_instance)
|
|
||||||
|
|
||||||
def authenticate(self):
|
|
||||||
self.auth_plugin.authenticate(self)
|
|
||||||
# Store the authentication results in the keyring for later requests
|
|
||||||
if self.keyring_saver:
|
|
||||||
self.keyring_saver.save(self)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseClient(object):
|
|
||||||
"""Top-level object to access the OpenStack API.
|
|
||||||
|
|
||||||
This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient`
|
|
||||||
will handle a bunch of issues such as authentication.
|
|
||||||
"""
|
|
||||||
|
|
||||||
service_type = None
|
|
||||||
endpoint_type = None # "publicURL" will be used
|
|
||||||
cached_endpoint = None
|
|
||||||
|
|
||||||
def __init__(self, http_client, extensions=None):
|
|
||||||
self.http_client = http_client
|
|
||||||
http_client.add_client(self)
|
|
||||||
|
|
||||||
# Add in any extensions...
|
|
||||||
if extensions:
|
|
||||||
for extension in extensions:
|
|
||||||
if extension.manager_class:
|
|
||||||
setattr(self, extension.name,
|
|
||||||
extension.manager_class(self))
|
|
||||||
|
|
||||||
def client_request(self, method, url, **kwargs):
|
|
||||||
return self.http_client.client_request(
|
|
||||||
self, method, url, **kwargs)
|
|
||||||
|
|
||||||
def head(self, url, **kwargs):
|
|
||||||
return self.client_request("HEAD", url, **kwargs)
|
|
||||||
|
|
||||||
def get(self, url, **kwargs):
|
|
||||||
return self.client_request("GET", url, **kwargs)
|
|
||||||
|
|
||||||
def post(self, url, **kwargs):
|
|
||||||
return self.client_request("POST", url, **kwargs)
|
|
||||||
|
|
||||||
def put(self, url, **kwargs):
|
|
||||||
return self.client_request("PUT", url, **kwargs)
|
|
||||||
|
|
||||||
def delete(self, url, **kwargs):
|
|
||||||
return self.client_request("DELETE", url, **kwargs)
|
|
||||||
|
|
||||||
def patch(self, url, **kwargs):
|
|
||||||
return self.client_request("PATCH", url, **kwargs)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_class(api_name, version, version_map):
|
|
||||||
"""Returns the client class for the requested API version
|
|
||||||
|
|
||||||
:param api_name: the name of the API, e.g. 'compute', 'image', etc
|
|
||||||
:param version: the requested API version
|
|
||||||
:param version_map: a dict of client classes keyed by version
|
|
||||||
:rtype: a client class for the requested API version
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
client_path = version_map[str(version)]
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
msg = _("Invalid %(api_name)s client version '%(version)s'. "
|
|
||||||
"Must be one of: %(version_map)s") % {
|
|
||||||
'api_name': api_name,
|
|
||||||
'version': version,
|
|
||||||
'version_map': ', '.join(version_map.keys())}
|
|
||||||
raise exceptions.UnsupportedVersion(msg)
|
|
||||||
|
|
||||||
return importutils.import_class(client_path)
|
|
@@ -1,463 +0,0 @@
|
|||||||
# Copyright 2010 Jacob Kaplan-Moss
|
|
||||||
# Copyright 2011 Nebula, Inc.
|
|
||||||
# Copyright 2013 Alessio Ababilov
|
|
||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
Exception definitions.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import inspect
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
from solumclient.i18n import _
|
|
||||||
|
|
||||||
|
|
||||||
class ClientException(Exception):
|
|
||||||
"""The base exception class for all exceptions this library raises."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MissingArgs(ClientException):
|
|
||||||
"""Supplied arguments are not sufficient for calling a function."""
|
|
||||||
def __init__(self, missing):
|
|
||||||
self.missing = missing
|
|
||||||
msg = _("Missing arguments: %s") % ", ".join(missing)
|
|
||||||
super(MissingArgs, self).__init__(msg)
|
|
||||||
|
|
||||||
|
|
||||||
class ValidationError(ClientException):
|
|
||||||
"""Error in validation on API client side."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedVersion(ClientException):
|
|
||||||
"""User is trying to use an unsupported version of the API."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CommandError(ClientException):
|
|
||||||
"""Error in CLI tool."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthorizationFailure(ClientException):
|
|
||||||
"""Cannot authorize API client."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectionRefused(ClientException):
|
|
||||||
"""Cannot connect to API service."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AuthPluginOptionsMissing(AuthorizationFailure):
|
|
||||||
"""Auth plugin misses some options."""
|
|
||||||
def __init__(self, opt_names):
|
|
||||||
super(AuthPluginOptionsMissing, self).__init__(
|
|
||||||
_("Authentication failed. Missing options: %s") %
|
|
||||||
", ".join(opt_names))
|
|
||||||
self.opt_names = opt_names
|
|
||||||
|
|
||||||
|
|
||||||
class AuthSystemNotFound(AuthorizationFailure):
|
|
||||||
"""User has specified an AuthSystem that is not installed."""
|
|
||||||
def __init__(self, auth_system):
|
|
||||||
super(AuthSystemNotFound, self).__init__(
|
|
||||||
_("AuthSystemNotFound: %s") % repr(auth_system))
|
|
||||||
self.auth_system = auth_system
|
|
||||||
|
|
||||||
|
|
||||||
class NoUniqueMatch(ClientException):
|
|
||||||
"""Multiple entities found instead of one."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointException(ClientException):
|
|
||||||
"""Something is rotten in Service Catalog."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class EndpointNotFound(EndpointException):
|
|
||||||
"""Could not find requested endpoint in Service Catalog."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AmbiguousEndpoints(EndpointException):
|
|
||||||
"""Found more than one matching endpoint in Service Catalog."""
|
|
||||||
def __init__(self, endpoints=None):
|
|
||||||
super(AmbiguousEndpoints, self).__init__(
|
|
||||||
_("AmbiguousEndpoints: %s") % repr(endpoints))
|
|
||||||
self.endpoints = endpoints
|
|
||||||
|
|
||||||
|
|
||||||
class HttpError(ClientException):
|
|
||||||
"""The base exception class for all HTTP exceptions."""
|
|
||||||
http_status = 0
|
|
||||||
message = _("HTTP Error")
|
|
||||||
|
|
||||||
def __init__(self, message=None, details=None,
|
|
||||||
response=None, request_id=None,
|
|
||||||
url=None, method=None, http_status=None):
|
|
||||||
self.http_status = http_status or self.http_status
|
|
||||||
self.message = message or self.message
|
|
||||||
self.details = details
|
|
||||||
self.request_id = request_id
|
|
||||||
self.response = response
|
|
||||||
self.url = url
|
|
||||||
self.method = method
|
|
||||||
formatted_string = "%s (HTTP %s)" % (self.message, self.http_status)
|
|
||||||
if request_id:
|
|
||||||
formatted_string += " (Request-ID: %s)" % request_id
|
|
||||||
super(HttpError, self).__init__(formatted_string)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPRedirection(HttpError):
|
|
||||||
"""HTTP Redirection."""
|
|
||||||
message = _("HTTP Redirection")
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPClientError(HttpError):
|
|
||||||
"""Client-side HTTP error.
|
|
||||||
|
|
||||||
Exception for cases in which the client seems to have erred.
|
|
||||||
"""
|
|
||||||
message = _("HTTP Client Error")
|
|
||||||
|
|
||||||
|
|
||||||
class HttpServerError(HttpError):
|
|
||||||
"""Server-side HTTP error.
|
|
||||||
|
|
||||||
Exception for cases in which the server is aware that it has
|
|
||||||
erred or is incapable of performing the request.
|
|
||||||
"""
|
|
||||||
message = _("HTTP Server Error")
|
|
||||||
|
|
||||||
|
|
||||||
class MultipleChoices(HTTPRedirection):
|
|
||||||
"""HTTP 300 - Multiple Choices.
|
|
||||||
|
|
||||||
Indicates multiple options for the resource that the client may follow.
|
|
||||||
"""
|
|
||||||
|
|
||||||
http_status = 300
|
|
||||||
message = _("Multiple Choices")
|
|
||||||
|
|
||||||
|
|
||||||
class BadRequest(HTTPClientError):
|
|
||||||
"""HTTP 400 - Bad Request.
|
|
||||||
|
|
||||||
The request cannot be fulfilled due to bad syntax.
|
|
||||||
"""
|
|
||||||
http_status = 400
|
|
||||||
message = _("Bad Request")
|
|
||||||
|
|
||||||
|
|
||||||
class Unauthorized(HTTPClientError):
|
|
||||||
"""HTTP 401 - Unauthorized.
|
|
||||||
|
|
||||||
Similar to 403 Forbidden, but specifically for use when authentication
|
|
||||||
is required and has failed or has not yet been provided.
|
|
||||||
"""
|
|
||||||
http_status = 401
|
|
||||||
message = _("Unauthorized")
|
|
||||||
|
|
||||||
|
|
||||||
class PaymentRequired(HTTPClientError):
|
|
||||||
"""HTTP 402 - Payment Required.
|
|
||||||
|
|
||||||
Reserved for future use.
|
|
||||||
"""
|
|
||||||
http_status = 402
|
|
||||||
message = _("Payment Required")
|
|
||||||
|
|
||||||
|
|
||||||
class Forbidden(HTTPClientError):
|
|
||||||
"""HTTP 403 - Forbidden.
|
|
||||||
|
|
||||||
The request was a valid request, but the server is refusing to respond
|
|
||||||
to it.
|
|
||||||
"""
|
|
||||||
http_status = 403
|
|
||||||
message = _("Forbidden")
|
|
||||||
|
|
||||||
|
|
||||||
class NotFound(HTTPClientError):
|
|
||||||
"""HTTP 404 - Not Found.
|
|
||||||
|
|
||||||
The requested resource could not be found but may be available again
|
|
||||||
in the future.
|
|
||||||
"""
|
|
||||||
http_status = 404
|
|
||||||
message = _("Not Found")
|
|
||||||
|
|
||||||
|
|
||||||
class MethodNotAllowed(HTTPClientError):
|
|
||||||
"""HTTP 405 - Method Not Allowed.
|
|
||||||
|
|
||||||
A request was made of a resource using a request method not supported
|
|
||||||
by that resource.
|
|
||||||
"""
|
|
||||||
http_status = 405
|
|
||||||
message = _("Method Not Allowed")
|
|
||||||
|
|
||||||
|
|
||||||
class NotAcceptable(HTTPClientError):
|
|
||||||
"""HTTP 406 - Not Acceptable.
|
|
||||||
|
|
||||||
The requested resource is only capable of generating content not
|
|
||||||
acceptable according to the Accept headers sent in the request.
|
|
||||||
"""
|
|
||||||
http_status = 406
|
|
||||||
message = _("Not Acceptable")
|
|
||||||
|
|
||||||
|
|
||||||
class ProxyAuthenticationRequired(HTTPClientError):
|
|
||||||
"""HTTP 407 - Proxy Authentication Required.
|
|
||||||
|
|
||||||
The client must first authenticate itself with the proxy.
|
|
||||||
"""
|
|
||||||
http_status = 407
|
|
||||||
message = _("Proxy Authentication Required")
|
|
||||||
|
|
||||||
|
|
||||||
class RequestTimeout(HTTPClientError):
|
|
||||||
"""HTTP 408 - Request Timeout.
|
|
||||||
|
|
||||||
The server timed out waiting for the request.
|
|
||||||
"""
|
|
||||||
http_status = 408
|
|
||||||
message = _("Request Timeout")
|
|
||||||
|
|
||||||
|
|
||||||
class Conflict(HTTPClientError):
|
|
||||||
"""HTTP 409 - Conflict.
|
|
||||||
|
|
||||||
Indicates that the request could not be processed because of conflict
|
|
||||||
in the request, such as an edit conflict.
|
|
||||||
"""
|
|
||||||
http_status = 409
|
|
||||||
message = _("Conflict")
|
|
||||||
|
|
||||||
|
|
||||||
class Gone(HTTPClientError):
|
|
||||||
"""HTTP 410 - Gone.
|
|
||||||
|
|
||||||
Indicates that the resource requested is no longer available and will
|
|
||||||
not be available again.
|
|
||||||
"""
|
|
||||||
http_status = 410
|
|
||||||
message = _("Gone")
|
|
||||||
|
|
||||||
|
|
||||||
class LengthRequired(HTTPClientError):
|
|
||||||
"""HTTP 411 - Length Required.
|
|
||||||
|
|
||||||
The request did not specify the length of its content, which is
|
|
||||||
required by the requested resource.
|
|
||||||
"""
|
|
||||||
http_status = 411
|
|
||||||
message = _("Length Required")
|
|
||||||
|
|
||||||
|
|
||||||
class PreconditionFailed(HTTPClientError):
|
|
||||||
"""HTTP 412 - Precondition Failed.
|
|
||||||
|
|
||||||
The server does not meet one of the preconditions that the requester
|
|
||||||
put on the request.
|
|
||||||
"""
|
|
||||||
http_status = 412
|
|
||||||
message = _("Precondition Failed")
|
|
||||||
|
|
||||||
|
|
||||||
class RequestEntityTooLarge(HTTPClientError):
|
|
||||||
"""HTTP 413 - Request Entity Too Large.
|
|
||||||
|
|
||||||
The request is larger than the server is willing or able to process.
|
|
||||||
"""
|
|
||||||
http_status = 413
|
|
||||||
message = _("Request Entity Too Large")
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
self.retry_after = int(kwargs.pop('retry_after'))
|
|
||||||
except (KeyError, ValueError):
|
|
||||||
self.retry_after = 0
|
|
||||||
|
|
||||||
super(RequestEntityTooLarge, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
class RequestUriTooLong(HTTPClientError):
|
|
||||||
"""HTTP 414 - Request-URI Too Long.
|
|
||||||
|
|
||||||
The URI provided was too long for the server to process.
|
|
||||||
"""
|
|
||||||
http_status = 414
|
|
||||||
message = _("Request-URI Too Long")
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedMediaType(HTTPClientError):
|
|
||||||
"""HTTP 415 - Unsupported Media Type.
|
|
||||||
|
|
||||||
The request entity has a media type which the server or resource does
|
|
||||||
not support.
|
|
||||||
"""
|
|
||||||
http_status = 415
|
|
||||||
message = _("Unsupported Media Type")
|
|
||||||
|
|
||||||
|
|
||||||
class RequestedRangeNotSatisfiable(HTTPClientError):
|
|
||||||
"""HTTP 416 - Requested Range Not Satisfiable.
|
|
||||||
|
|
||||||
The client has asked for a portion of the file, but the server cannot
|
|
||||||
supply that portion.
|
|
||||||
"""
|
|
||||||
http_status = 416
|
|
||||||
message = _("Requested Range Not Satisfiable")
|
|
||||||
|
|
||||||
|
|
||||||
class ExpectationFailed(HTTPClientError):
|
|
||||||
"""HTTP 417 - Expectation Failed.
|
|
||||||
|
|
||||||
The server cannot meet the requirements of the Expect request-header field.
|
|
||||||
"""
|
|
||||||
http_status = 417
|
|
||||||
message = _("Expectation Failed")
|
|
||||||
|
|
||||||
|
|
||||||
class UnprocessableEntity(HTTPClientError):
|
|
||||||
"""HTTP 422 - Unprocessable Entity.
|
|
||||||
|
|
||||||
The request was well-formed but was unable to be followed due to semantic
|
|
||||||
errors.
|
|
||||||
"""
|
|
||||||
http_status = 422
|
|
||||||
message = _("Unprocessable Entity")
|
|
||||||
|
|
||||||
|
|
||||||
class InternalServerError(HttpServerError):
|
|
||||||
"""HTTP 500 - Internal Server Error.
|
|
||||||
|
|
||||||
A generic error message, given when no more specific message is suitable.
|
|
||||||
"""
|
|
||||||
http_status = 500
|
|
||||||
message = _("Internal Server Error")
|
|
||||||
|
|
||||||
|
|
||||||
# NotImplemented is a python keyword.
|
|
||||||
class HttpNotImplemented(HttpServerError):
|
|
||||||
"""HTTP 501 - Not Implemented.
|
|
||||||
|
|
||||||
The server either does not recognize the request method, or it lacks
|
|
||||||
the ability to fulfill the request.
|
|
||||||
"""
|
|
||||||
http_status = 501
|
|
||||||
message = _("Not Implemented")
|
|
||||||
|
|
||||||
|
|
||||||
class BadGateway(HttpServerError):
|
|
||||||
"""HTTP 502 - Bad Gateway.
|
|
||||||
|
|
||||||
The server was acting as a gateway or proxy and received an invalid
|
|
||||||
response from the upstream server.
|
|
||||||
"""
|
|
||||||
http_status = 502
|
|
||||||
message = _("Bad Gateway")
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceUnavailable(HttpServerError):
|
|
||||||
"""HTTP 503 - Service Unavailable.
|
|
||||||
|
|
||||||
The server is currently unavailable.
|
|
||||||
"""
|
|
||||||
http_status = 503
|
|
||||||
message = _("Service Unavailable")
|
|
||||||
|
|
||||||
|
|
||||||
class GatewayTimeout(HttpServerError):
|
|
||||||
"""HTTP 504 - Gateway Timeout.
|
|
||||||
|
|
||||||
The server was acting as a gateway or proxy and did not receive a timely
|
|
||||||
response from the upstream server.
|
|
||||||
"""
|
|
||||||
http_status = 504
|
|
||||||
message = _("Gateway Timeout")
|
|
||||||
|
|
||||||
|
|
||||||
class HttpVersionNotSupported(HttpServerError):
|
|
||||||
"""HTTP 505 - HttpVersion Not Supported.
|
|
||||||
|
|
||||||
The server does not support the HTTP protocol version used in the request.
|
|
||||||
"""
|
|
||||||
http_status = 505
|
|
||||||
message = _("HTTP Version Not Supported")
|
|
||||||
|
|
||||||
|
|
||||||
# _code_map contains all the classes that have http_status attribute.
|
|
||||||
_code_map = dict(
|
|
||||||
(getattr(obj, 'http_status', None), obj)
|
|
||||||
for name, obj in vars(sys.modules[__name__]).items()
|
|
||||||
if inspect.isclass(obj) and getattr(obj, 'http_status', False)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def from_response(response, method, url):
|
|
||||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
|
||||||
|
|
||||||
:param response: instance of `requests.Response` class
|
|
||||||
:param method: HTTP method used for request
|
|
||||||
:param url: URL used for request
|
|
||||||
"""
|
|
||||||
|
|
||||||
req_id = response.headers.get("x-openstack-request-id")
|
|
||||||
# NOTE(hdd) true for older versions of nova and cinder
|
|
||||||
if not req_id:
|
|
||||||
req_id = response.headers.get("x-compute-request-id")
|
|
||||||
kwargs = {
|
|
||||||
"http_status": response.status_code,
|
|
||||||
"response": response,
|
|
||||||
"method": method,
|
|
||||||
"url": url,
|
|
||||||
"request_id": req_id,
|
|
||||||
}
|
|
||||||
if "retry-after" in response.headers:
|
|
||||||
kwargs["retry_after"] = response.headers["retry-after"]
|
|
||||||
|
|
||||||
content_type = response.headers.get("Content-Type", "")
|
|
||||||
if content_type.startswith("application/json"):
|
|
||||||
try:
|
|
||||||
body = response.json()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if isinstance(body, dict) and isinstance(body.get("error"), dict):
|
|
||||||
error = body["error"]
|
|
||||||
kwargs["message"] = error.get("message")
|
|
||||||
kwargs["details"] = error.get("details")
|
|
||||||
elif content_type.startswith("text/"):
|
|
||||||
kwargs["details"] = response.text
|
|
||||||
|
|
||||||
try:
|
|
||||||
cls = _code_map[response.status_code]
|
|
||||||
except KeyError:
|
|
||||||
if 500 <= response.status_code < 600:
|
|
||||||
cls = HttpServerError
|
|
||||||
elif 400 <= response.status_code < 500:
|
|
||||||
cls = HTTPClientError
|
|
||||||
else:
|
|
||||||
cls = HttpError
|
|
||||||
return cls(**kwargs)
|
|
@@ -1,173 +0,0 @@
|
|||||||
# Copyright 2013 OpenStack Foundation
|
|
||||||
# All Rights Reserved.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""
|
|
||||||
A fake server that "responds" to API methods with pre-canned responses.
|
|
||||||
|
|
||||||
All of these responses come from the spec, so if for some reason the spec's
|
|
||||||
wrong the tests might raise AssertionError. I've indicated in comments the
|
|
||||||
places where actual behavior differs from the spec.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# W0102: Dangerous default value %s as argument
|
|
||||||
# pylint: disable=W0102
|
|
||||||
|
|
||||||
import json
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from urllib import parse
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import client
|
|
||||||
|
|
||||||
|
|
||||||
def assert_has_keys(dct, required=None, optional=None):
|
|
||||||
required = required or []
|
|
||||||
optional = optional or []
|
|
||||||
for k in required:
|
|
||||||
try:
|
|
||||||
assert k in dct
|
|
||||||
except AssertionError:
|
|
||||||
extra_keys = set(dct.keys()).difference(set(required + optional))
|
|
||||||
raise AssertionError("found unexpected keys: %s" %
|
|
||||||
list(extra_keys))
|
|
||||||
|
|
||||||
|
|
||||||
class TestResponse(requests.Response):
|
|
||||||
"""Wrap requests.Response and provide a convenient initialization."""
|
|
||||||
|
|
||||||
def __init__(self, data):
|
|
||||||
super(TestResponse, self).__init__()
|
|
||||||
self._content_consumed = True
|
|
||||||
if isinstance(data, dict):
|
|
||||||
self.status_code = data.get('status_code', 200)
|
|
||||||
# Fake the text attribute to streamline Response creation
|
|
||||||
text = data.get('text', "")
|
|
||||||
if isinstance(text, (dict, list)):
|
|
||||||
self._content = json.dumps(text)
|
|
||||||
default_headers = {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
self._content = text
|
|
||||||
default_headers = {}
|
|
||||||
if isinstance(self._content, str):
|
|
||||||
self._content = self._content.encode('utf-8', 'strict')
|
|
||||||
self.headers = data.get('headers') or default_headers
|
|
||||||
else:
|
|
||||||
self.status_code = data
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
return (self.status_code == other.status_code and
|
|
||||||
self.headers == other.headers and
|
|
||||||
self._content == other._content)
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
return not self.__eq__(other)
|
|
||||||
|
|
||||||
|
|
||||||
class FakeHTTPClient(client.HTTPClient):
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
self.callstack = []
|
|
||||||
self.fixtures = kwargs.pop("fixtures", None) or {}
|
|
||||||
if not args and "auth_plugin" not in kwargs:
|
|
||||||
args = (None, )
|
|
||||||
super(FakeHTTPClient, self).__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
def assert_called(self, method, url, body=None, pos=-1):
|
|
||||||
"""Assert than an API method was just called."""
|
|
||||||
expected = (method, url)
|
|
||||||
called = self.callstack[pos][0:2]
|
|
||||||
assert self.callstack, \
|
|
||||||
"Expected %s %s but no calls were made." % expected
|
|
||||||
|
|
||||||
assert expected == called, 'Expected %s %s; got %s %s' % (
|
|
||||||
expected + called)
|
|
||||||
|
|
||||||
if body is not None:
|
|
||||||
if self.callstack[pos][3] != body:
|
|
||||||
raise AssertionError('%r != %r' %
|
|
||||||
(self.callstack[pos][3], body))
|
|
||||||
|
|
||||||
def assert_called_anytime(self, method, url, body=None):
|
|
||||||
"""Assert than an API method was called anytime in the test."""
|
|
||||||
expected = (method, url)
|
|
||||||
|
|
||||||
assert self.callstack, \
|
|
||||||
"Expected %s %s but no calls were made." % expected
|
|
||||||
|
|
||||||
found = False
|
|
||||||
entry = None
|
|
||||||
for entry in self.callstack:
|
|
||||||
if expected == entry[0:2]:
|
|
||||||
found = True
|
|
||||||
break
|
|
||||||
|
|
||||||
assert found, 'Expected %s %s; got %s' % (method, url, self.callstack)
|
|
||||||
if body is not None:
|
|
||||||
assert entry[3] == body, "%s != %s" % (entry[3], body)
|
|
||||||
|
|
||||||
self.callstack = []
|
|
||||||
|
|
||||||
def clear_callstack(self):
|
|
||||||
self.callstack = []
|
|
||||||
|
|
||||||
def authenticate(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def client_request(self, client, method, url, **kwargs):
|
|
||||||
# Check that certain things are called correctly
|
|
||||||
if method in ["GET", "DELETE"]:
|
|
||||||
assert "json" not in kwargs
|
|
||||||
|
|
||||||
# Note the call
|
|
||||||
self.callstack.append(
|
|
||||||
(method,
|
|
||||||
url,
|
|
||||||
kwargs.get("headers") or {},
|
|
||||||
kwargs.get("json") or kwargs.get("data")))
|
|
||||||
try:
|
|
||||||
fixture = self.fixtures[url][method]
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
return TestResponse({"headers": fixture[0],
|
|
||||||
"text": fixture[1]})
|
|
||||||
|
|
||||||
# Call the method
|
|
||||||
args = parse.parse_qsl(parse.urlparse(url)[4])
|
|
||||||
kwargs.update(args)
|
|
||||||
munged_url = url.rsplit('?', 1)[0]
|
|
||||||
munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_')
|
|
||||||
munged_url = munged_url.replace('-', '_')
|
|
||||||
|
|
||||||
callback = "%s_%s" % (method.lower(), munged_url)
|
|
||||||
|
|
||||||
if not hasattr(self, callback):
|
|
||||||
raise AssertionError('Called unknown API method: %s %s, '
|
|
||||||
'expected fakes method name: %s' %
|
|
||||||
(method, url, callback))
|
|
||||||
|
|
||||||
resp = getattr(self, callback)(**kwargs)
|
|
||||||
if len(resp) == 3:
|
|
||||||
status, headers, body = resp
|
|
||||||
else:
|
|
||||||
status, body = resp
|
|
||||||
headers = {}
|
|
||||||
return TestResponse({
|
|
||||||
"status_code": status,
|
|
||||||
"text": body,
|
|
||||||
"headers": headers,
|
|
||||||
})
|
|
@@ -1,225 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from keystoneclient.auth.identity import v2 as v2_auth
|
|
||||||
from keystoneclient.auth.identity import v3 as v3_auth
|
|
||||||
from keystoneclient import discover
|
|
||||||
from keystoneclient import exceptions as ks_exc
|
|
||||||
from keystoneclient import session
|
|
||||||
from oslo_utils import strutils
|
|
||||||
from urllib import parse as urlparse
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import auth
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import exc
|
|
||||||
|
|
||||||
|
|
||||||
def _discover_auth_versions(session, auth_url):
|
|
||||||
# discover the API versions the server is supporting based on the
|
|
||||||
# given URL
|
|
||||||
v2_auth_url = None
|
|
||||||
v3_auth_url = None
|
|
||||||
try:
|
|
||||||
ks_discover = discover.Discover(session=session, auth_url=auth_url)
|
|
||||||
v2_auth_url = ks_discover.url_for('2.0')
|
|
||||||
v3_auth_url = ks_discover.url_for('3.0')
|
|
||||||
except ks_exc.DiscoveryFailure:
|
|
||||||
raise
|
|
||||||
except exceptions.ClientException:
|
|
||||||
# Identity service may not support discovery. In that case,
|
|
||||||
# try to determine version from auth_url
|
|
||||||
url_parts = urlparse.urlparse(auth_url)
|
|
||||||
(scheme, netloc, path, params, query, fragment) = url_parts
|
|
||||||
path = path.lower()
|
|
||||||
if path.startswith('/v3'):
|
|
||||||
v3_auth_url = auth_url
|
|
||||||
elif path.startswith('/v2'):
|
|
||||||
v2_auth_url = auth_url
|
|
||||||
else:
|
|
||||||
raise exc.CommandError('Unable to determine the Keystone '
|
|
||||||
'version to authenticate with '
|
|
||||||
'using the given auth_url.')
|
|
||||||
return v2_auth_url, v3_auth_url
|
|
||||||
|
|
||||||
|
|
||||||
def _get_keystone_session(**kwargs):
|
|
||||||
# TODO(fabgia): the heavy lifting here should be really done by Keystone.
|
|
||||||
# Unfortunately Keystone does not support a richer method to perform
|
|
||||||
# discovery and return a single viable URL. A bug against Keystone has
|
|
||||||
# been filed: https://bugs.launchpad.net/python-keystoneclient/+bug/1330677
|
|
||||||
|
|
||||||
# first create a Keystone session
|
|
||||||
cacert = kwargs.pop('cacert', None)
|
|
||||||
cert = kwargs.pop('cert', None)
|
|
||||||
key = kwargs.pop('key', None)
|
|
||||||
insecure = kwargs.pop('insecure', False)
|
|
||||||
auth_url = kwargs.pop('auth_url', None)
|
|
||||||
project_id = kwargs.pop('project_id', None)
|
|
||||||
project_name = kwargs.pop('project_name', None)
|
|
||||||
|
|
||||||
if insecure:
|
|
||||||
verify = False
|
|
||||||
else:
|
|
||||||
verify = cacert or True
|
|
||||||
|
|
||||||
if cert and key:
|
|
||||||
# passing cert and key together is deprecated in favour of the
|
|
||||||
# requests lib form of having the cert and key as a tuple
|
|
||||||
cert = (cert, key)
|
|
||||||
|
|
||||||
# create the keystone client session
|
|
||||||
ks_session = session.Session(verify=verify, cert=cert)
|
|
||||||
v2_auth_url, v3_auth_url = _discover_auth_versions(ks_session, auth_url)
|
|
||||||
|
|
||||||
username = kwargs.pop('username', None)
|
|
||||||
user_id = kwargs.pop('user_id', None)
|
|
||||||
user_domain_name = kwargs.pop('user_domain_name', None)
|
|
||||||
user_domain_id = kwargs.pop('user_domain_id', None)
|
|
||||||
project_domain_name = kwargs.pop('project_domain_name', None)
|
|
||||||
project_domain_id = kwargs.pop('project_domain_id', None)
|
|
||||||
auth = None
|
|
||||||
|
|
||||||
use_domain = (user_domain_id or user_domain_name or
|
|
||||||
project_domain_id or project_domain_name)
|
|
||||||
use_v3 = v3_auth_url and (use_domain or (not v2_auth_url))
|
|
||||||
use_v2 = v2_auth_url and not use_domain
|
|
||||||
|
|
||||||
if use_v3:
|
|
||||||
# the auth_url as v3 specified
|
|
||||||
# e.g. http://no.where:5000/v3
|
|
||||||
# Keystone will return only v3 as viable option
|
|
||||||
auth = v3_auth.Password(
|
|
||||||
v3_auth_url,
|
|
||||||
username=username,
|
|
||||||
password=kwargs.pop('password', None),
|
|
||||||
user_id=user_id,
|
|
||||||
user_domain_name=user_domain_name,
|
|
||||||
user_domain_id=user_domain_id,
|
|
||||||
project_name=project_name,
|
|
||||||
project_id=project_id,
|
|
||||||
project_domain_name=project_domain_name,
|
|
||||||
project_domain_id=project_domain_id)
|
|
||||||
elif use_v2:
|
|
||||||
# the auth_url as v2 specified
|
|
||||||
# e.g. http://no.where:5000/v2.0
|
|
||||||
# Keystone will return only v2 as viable option
|
|
||||||
auth = v2_auth.Password(
|
|
||||||
v2_auth_url,
|
|
||||||
username,
|
|
||||||
kwargs.pop('password', None),
|
|
||||||
tenant_id=project_id,
|
|
||||||
tenant_name=project_name)
|
|
||||||
else:
|
|
||||||
raise exc.CommandError('Unable to determine the Keystone version '
|
|
||||||
'to authenticate with using the given '
|
|
||||||
'auth_url.')
|
|
||||||
|
|
||||||
ks_session.auth = auth
|
|
||||||
return ks_session
|
|
||||||
|
|
||||||
|
|
||||||
def _get_endpoint(ks_session, **kwargs):
|
|
||||||
"""Get an endpoint using the provided keystone session."""
|
|
||||||
|
|
||||||
# set service specific endpoint types
|
|
||||||
endpoint_type = kwargs.get('endpoint_type') or 'publicURL'
|
|
||||||
service_type = kwargs.get('service_type') or 'application_deployment'
|
|
||||||
|
|
||||||
endpoint = ks_session.get_endpoint(service_type=service_type,
|
|
||||||
interface=endpoint_type,
|
|
||||||
region_name=kwargs.get('region_name'))
|
|
||||||
|
|
||||||
return endpoint
|
|
||||||
|
|
||||||
|
|
||||||
class KeystoneAuthPlugin(auth.BaseAuthPlugin):
|
|
||||||
|
|
||||||
opt_names = ['tenant_id', 'region_name', 'auth_token',
|
|
||||||
'service_type', 'endpoint_type', 'cacert',
|
|
||||||
'auth_url', 'insecure', 'cert_file', 'key_file',
|
|
||||||
'cert', 'key', 'tenant_name', 'project_name',
|
|
||||||
'project_id', 'project_domain_id', 'project_domain_name',
|
|
||||||
'user_id', 'user_domain_id', 'user_domain_name',
|
|
||||||
'password', 'username', 'endpoint']
|
|
||||||
|
|
||||||
def __init__(self, auth_system=None, **kwargs):
|
|
||||||
self.opt_names.extend(self.common_opt_names)
|
|
||||||
super(KeystoneAuthPlugin, self).__init__(auth_system, **kwargs)
|
|
||||||
|
|
||||||
def _do_authenticate(self, http_client):
|
|
||||||
token = self.opts.get('token') or self.opts.get('auth_token')
|
|
||||||
endpoint = self.opts.get('endpoint')
|
|
||||||
if not (token and endpoint):
|
|
||||||
project_id = (self.opts.get('project_id') or
|
|
||||||
self.opts.get('tenant_id'))
|
|
||||||
project_name = (self.opts.get('project_name') or
|
|
||||||
self.opts.get('tenant_name'))
|
|
||||||
ks_kwargs = {
|
|
||||||
'username': self.opts.get('username'),
|
|
||||||
'password': self.opts.get('password'),
|
|
||||||
'user_id': self.opts.get('user_id'),
|
|
||||||
'user_domain_id': self.opts.get('user_domain_id'),
|
|
||||||
'user_domain_name': self.opts.get('user_domain_name'),
|
|
||||||
'project_id': project_id,
|
|
||||||
'project_name': project_name,
|
|
||||||
'project_domain_name': self.opts.get('project_domain_name'),
|
|
||||||
'project_domain_id': self.opts.get('project_domain_id'),
|
|
||||||
'auth_url': self.opts.get('auth_url'),
|
|
||||||
'cacert': self.opts.get('cacert'),
|
|
||||||
'cert': self.opts.get('cert'),
|
|
||||||
'key': self.opts.get('key'),
|
|
||||||
'insecure': strutils.bool_from_string(
|
|
||||||
self.opts.get('insecure')),
|
|
||||||
'endpoint_type': self.opts.get('endpoint_type'),
|
|
||||||
}
|
|
||||||
|
|
||||||
# retrieve session
|
|
||||||
ks_session = _get_keystone_session(**ks_kwargs)
|
|
||||||
token = ks_session.get_token()
|
|
||||||
endpoint = (self.opts.get('endpoint') or
|
|
||||||
_get_endpoint(ks_session, **ks_kwargs))
|
|
||||||
self.opts['token'] = token
|
|
||||||
self.opts['endpoint'] = endpoint
|
|
||||||
|
|
||||||
def token_and_endpoint(self, endpoint_type, service_type):
|
|
||||||
token = self.opts.get('token')
|
|
||||||
if callable(token):
|
|
||||||
token = token()
|
|
||||||
return token, self.opts.get('endpoint')
|
|
||||||
|
|
||||||
def sufficient_options(self):
|
|
||||||
"""Check if all required options are present.
|
|
||||||
|
|
||||||
:raises: AuthPluginOptionsMissing
|
|
||||||
"""
|
|
||||||
has_token = self.opts.get('token') or self.opts.get('auth_token')
|
|
||||||
no_auth = has_token and self.opts.get('endpoint')
|
|
||||||
has_project = (self.opts.get('project_id')
|
|
||||||
or (self.opts.get('project_name')
|
|
||||||
and (self.opts.get('user_domain_name')
|
|
||||||
or self.opts.get('user_domain_id'))))
|
|
||||||
has_tenant = self.opts.get('tenant_id') or self.opts.get('tenant_name')
|
|
||||||
has_credential = (self.opts.get('username')
|
|
||||||
and (has_project or has_tenant)
|
|
||||||
and self.opts.get('password')
|
|
||||||
and self.opts.get('auth_url'))
|
|
||||||
missing = not (no_auth or has_credential)
|
|
||||||
if missing:
|
|
||||||
missing_opts = []
|
|
||||||
opts = ['token', 'endpoint', 'username', 'password', 'auth_url',
|
|
||||||
'tenant_id', 'tenant_name']
|
|
||||||
for opt in opts:
|
|
||||||
if not self.opts.get(opt):
|
|
||||||
missing_opts.append(opt)
|
|
||||||
raise exceptions.AuthPluginOptionsMissing(missing_opts)
|
|
@@ -1,104 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 urllib import parse as urlparse
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import base
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
|
|
||||||
|
|
||||||
class FindMixin(object):
|
|
||||||
"""Just `findone()`/`findall()` methods.
|
|
||||||
|
|
||||||
Note: this is largely a copy of apiclient.base.ManagerWithFind
|
|
||||||
but without the inheritance and the find() method which
|
|
||||||
does not clash with the Manager find() - now called findone().
|
|
||||||
"""
|
|
||||||
|
|
||||||
def findone(self, **kwargs):
|
|
||||||
"""Find a single item with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
This isn't very efficient: it loads the entire list then filters on
|
|
||||||
the Python side.
|
|
||||||
"""
|
|
||||||
matches = self.findall(**kwargs)
|
|
||||||
num_matches = len(matches)
|
|
||||||
if num_matches == 0:
|
|
||||||
msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
|
|
||||||
raise exceptions.NotFound(msg)
|
|
||||||
elif num_matches > 1:
|
|
||||||
raise exceptions.NoUniqueMatch()
|
|
||||||
else:
|
|
||||||
return matches[0]
|
|
||||||
|
|
||||||
def findall(self, **kwargs):
|
|
||||||
"""Find all items with attributes matching ``**kwargs``.
|
|
||||||
|
|
||||||
This isn't very efficient: it loads the entire list then filters on
|
|
||||||
the Python side.
|
|
||||||
"""
|
|
||||||
found = []
|
|
||||||
searches = kwargs.items()
|
|
||||||
|
|
||||||
for obj in self.list(app_id=kwargs.get('app_id')):
|
|
||||||
try:
|
|
||||||
if all(getattr(obj, attr) == value
|
|
||||||
for (attr, value) in searches):
|
|
||||||
found.append(obj)
|
|
||||||
except AttributeError:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return found
|
|
||||||
|
|
||||||
|
|
||||||
class CrudManager(base.CrudManager):
|
|
||||||
def list(self, base_url=None, **kwargs):
|
|
||||||
"""List the collection.
|
|
||||||
|
|
||||||
:param base_url: if provided, the generated URL will be appended to it
|
|
||||||
"""
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
|
|
||||||
return self._list(
|
|
||||||
'%(base_url)s%(query)s' % {
|
|
||||||
'base_url': self.build_url(base_url=base_url, **kwargs),
|
|
||||||
'query': '?%s' % urlparse.urlencode(kwargs) if kwargs else '',
|
|
||||||
})
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._get(
|
|
||||||
self.build_url(**kwargs))
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
return self._post(
|
|
||||||
self.build_url(**kwargs), kwargs)
|
|
||||||
|
|
||||||
def update(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
params = kwargs.copy()
|
|
||||||
params.pop('%s_id' % self.key)
|
|
||||||
|
|
||||||
return self._put(
|
|
||||||
self.build_url(**kwargs), params)
|
|
||||||
|
|
||||||
def patch(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
params = kwargs.copy()
|
|
||||||
params.pop('%s_id' % self.key)
|
|
||||||
params.pop('base_url')
|
|
||||||
|
|
||||||
return self._patch(
|
|
||||||
self.build_url(**kwargs), params)
|
|
@@ -1,298 +0,0 @@
|
|||||||
# Copyright (c) 2014 Rackspace
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from solumclient import client as solum_client
|
|
||||||
from solumclient.common import cliutils
|
|
||||||
from solumclient.common import exc
|
|
||||||
|
|
||||||
|
|
||||||
class CommandsBase(object):
|
|
||||||
"""Base command parsing class."""
|
|
||||||
parser = None
|
|
||||||
solum = None
|
|
||||||
json_output = False
|
|
||||||
verify = True
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
self.parser = parser
|
|
||||||
|
|
||||||
self._get_global_flags()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._get_auth_flags()
|
|
||||||
except exc.CommandError as ce:
|
|
||||||
print(self.__doc__)
|
|
||||||
print("ERROR: %s" % ce.message)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.parser.add_argument('action',
|
|
||||||
default='help',
|
|
||||||
help='Action to perform on resource')
|
|
||||||
|
|
||||||
parsed, _ = self.parser.parse_known_args()
|
|
||||||
action = vars(parsed).get('action')
|
|
||||||
|
|
||||||
client_args = vars(parsed)
|
|
||||||
client_args.pop('insecure', None)
|
|
||||||
client_args['verify'] = self.verify
|
|
||||||
if 'os_auth_token' in client_args:
|
|
||||||
del client_args['os_auth_token']
|
|
||||||
|
|
||||||
self.client = solum_client.get_client(parsed.solum_api_version,
|
|
||||||
**client_args)
|
|
||||||
|
|
||||||
if action in self._actions:
|
|
||||||
try:
|
|
||||||
return self._actions[action]()
|
|
||||||
except exc.CommandError as ce:
|
|
||||||
print("ERROR: %s" % ce.message)
|
|
||||||
else:
|
|
||||||
print(self.__doc__)
|
|
||||||
|
|
||||||
def _get_auth_flags(self):
|
|
||||||
self.parser.add_argument('--os-username',
|
|
||||||
default=env('OS_USERNAME'),
|
|
||||||
help='Defaults to env[OS_USERNAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-password',
|
|
||||||
default=env('OS_PASSWORD'),
|
|
||||||
help='Defaults to env[OS_PASSWORD]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-tenant-name',
|
|
||||||
default=env('OS_TENANT_NAME'),
|
|
||||||
help='Defaults to env[OS_TENANT_NAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-tenant-id',
|
|
||||||
default=env('OS_TENANT_ID'),
|
|
||||||
help='Defaults to env[OS_TENANT_ID]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-project-name',
|
|
||||||
default=env('OS_PROJECT_NAME'),
|
|
||||||
help='Defaults to env[OS_PROJECT_NAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-project-id',
|
|
||||||
default=env('OS_PROJECT_ID'),
|
|
||||||
help='Defaults to env[OS_PROJECT_ID]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-project-domain-name',
|
|
||||||
default=env('OS_PROJECT_DOMAIN_NAME'),
|
|
||||||
help='Defaults to '
|
|
||||||
'env[OS_PROJECT_DOMAIN_NAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-project-domain-id',
|
|
||||||
default=env('OS_PROJECT_DOMAIN_ID'),
|
|
||||||
help='Defaults to '
|
|
||||||
'env[OS_PROJECT_DOMAIN_NAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-user-domain-name',
|
|
||||||
default=env('OS_USER_DOMAIN_NAME'),
|
|
||||||
help='Defaults to env[OS_USER_DOMAIN_NAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-user-domain-id',
|
|
||||||
default=env('OS_USER_DOMAIN_ID'),
|
|
||||||
help='Defaults to env[OS_USER_DOMAIN_NAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-region-name',
|
|
||||||
default=env('OS_REGION_NAME'),
|
|
||||||
help='Defaults to env[OS_REGION_NAME]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-auth-url',
|
|
||||||
default=env('OS_AUTH_URL'),
|
|
||||||
help='Defaults to env[OS_AUTH_URL]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--os-auth-token',
|
|
||||||
default=env('OS_AUTH_TOKEN'),
|
|
||||||
help='Defaults to env[OS_AUTH_TOKEN]')
|
|
||||||
|
|
||||||
self.parser.add_argument('--solum-url',
|
|
||||||
default=env('SOLUM_URL'),
|
|
||||||
help='Defaults to env[SOLUM_URL]')
|
|
||||||
|
|
||||||
api_version = env('SOLUM_API_VERSION', default='1')
|
|
||||||
self.parser.add_argument('--solum-api-version',
|
|
||||||
default=api_version,
|
|
||||||
help='Defaults to env[SOLUM_API_VERSION] '
|
|
||||||
'or 1')
|
|
||||||
|
|
||||||
parsed, _ = self.parser.parse_known_args()
|
|
||||||
|
|
||||||
client_args = vars(parsed)
|
|
||||||
|
|
||||||
if not parsed.os_auth_token:
|
|
||||||
# Remove arguments that are not to be passed to the client in this
|
|
||||||
# case.
|
|
||||||
del client_args['os_auth_token']
|
|
||||||
|
|
||||||
if not parsed.os_username:
|
|
||||||
raise exc.CommandError("You must provide a username via "
|
|
||||||
"either --os-username or via "
|
|
||||||
"env[OS_USERNAME]")
|
|
||||||
|
|
||||||
if not parsed.os_password:
|
|
||||||
raise exc.CommandError("You must provide a password via "
|
|
||||||
"either --os-password or via "
|
|
||||||
"env[OS_PASSWORD]")
|
|
||||||
|
|
||||||
if not (parsed.os_tenant_name or parsed.os_project_name):
|
|
||||||
raise exc.CommandError("You must provide a tenant_name via "
|
|
||||||
"either --os-tenant-name or via "
|
|
||||||
"env[OS_TENANT_NAME]")
|
|
||||||
|
|
||||||
if not parsed.os_auth_url:
|
|
||||||
raise exc.CommandError("You must provide an auth url via "
|
|
||||||
"either --os-auth-url or via "
|
|
||||||
"env[OS_AUTH_URL]")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def _actions(self):
|
|
||||||
"""Action handler."""
|
|
||||||
return dict((attr, getattr(self, attr))
|
|
||||||
for attr in dir(self)
|
|
||||||
if not attr.startswith('_')
|
|
||||||
and callable(getattr(self, attr)))
|
|
||||||
|
|
||||||
def _get_global_flags(self):
|
|
||||||
"""Get global flags."""
|
|
||||||
# Good location to add_argument() global options like --verbose
|
|
||||||
self.parser.add_argument('--json',
|
|
||||||
action='store_true',
|
|
||||||
help='JSON formatted output')
|
|
||||||
self.parser.add_argument('-k', '--insecure',
|
|
||||||
action='store_true',
|
|
||||||
help='Explicitly allow the client 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.')
|
|
||||||
self.parser.add_argument('-d', '--debug',
|
|
||||||
action='store_true',
|
|
||||||
help='Print out request and response '
|
|
||||||
'details.')
|
|
||||||
args, _ = self.parser.parse_known_args()
|
|
||||||
if args.json:
|
|
||||||
self.json_output = True
|
|
||||||
if args.insecure:
|
|
||||||
self.verify = False
|
|
||||||
if args.debug:
|
|
||||||
logging.basicConfig(
|
|
||||||
format="%(levelname)s (%(module)s) %(message)s",
|
|
||||||
level=logging.DEBUG)
|
|
||||||
logging.getLogger('iso8601').setLevel(logging.WARNING)
|
|
||||||
urllibpool = 'urllib3.connectionpool'
|
|
||||||
logging.getLogger(urllibpool).setLevel(logging.WARNING)
|
|
||||||
|
|
||||||
def _sanitized_fields(self, fields):
|
|
||||||
def allowed(field):
|
|
||||||
if field.startswith('_'):
|
|
||||||
return False
|
|
||||||
if field == 'manager':
|
|
||||||
return False
|
|
||||||
if field == 'artifacts':
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
return [f for f in fields
|
|
||||||
if allowed(f)]
|
|
||||||
|
|
||||||
def _print_dict(self, obj, fields, dict_property="Property", wrap=0):
|
|
||||||
fields = self._sanitized_fields(fields)
|
|
||||||
try:
|
|
||||||
# datsun180b: I have no idea why, but following a PATCH
|
|
||||||
# app resources need to have this evaluated once or else
|
|
||||||
# the subset assignment below fails.
|
|
||||||
obj.attrs
|
|
||||||
except TypeError:
|
|
||||||
pass
|
|
||||||
except AttributeError:
|
|
||||||
pass
|
|
||||||
subset = dict([(f, getattr(obj, f, '')) for f in fields])
|
|
||||||
if self.json_output:
|
|
||||||
print(json.dumps(subset, indent=2, sort_keys=True))
|
|
||||||
else:
|
|
||||||
cliutils.print_dict(subset, dict_property, wrap)
|
|
||||||
|
|
||||||
def _print_list(self, objs, fields, formatters=None, sortby_index=0,
|
|
||||||
mixed_case_fields=None, field_labels=None):
|
|
||||||
fields = self._sanitized_fields(fields)
|
|
||||||
if self.json_output:
|
|
||||||
subsets = [dict([(f, getattr(obj, f, '')) for f in fields])
|
|
||||||
for obj in objs]
|
|
||||||
print(json.dumps(subsets, indent=2, sort_keys=True))
|
|
||||||
else:
|
|
||||||
cliutils.print_list(objs, fields, formatters, sortby_index,
|
|
||||||
mixed_case_fields, field_labels)
|
|
||||||
|
|
||||||
|
|
||||||
class NoSubCommands(CommandsBase):
|
|
||||||
"""Command parsing class that lacks an 'action'."""
|
|
||||||
parser = None
|
|
||||||
solum = None
|
|
||||||
json_output = False
|
|
||||||
verify = True
|
|
||||||
|
|
||||||
def __init__(self, parser):
|
|
||||||
self.parser = parser
|
|
||||||
|
|
||||||
self._get_global_flags()
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._get_auth_flags()
|
|
||||||
except exc.CommandError as ce:
|
|
||||||
print(self.__doc__)
|
|
||||||
print("ERROR: %s" % ce.message)
|
|
||||||
return
|
|
||||||
|
|
||||||
parsed, _ = self.parser.parse_known_args()
|
|
||||||
|
|
||||||
client_args = vars(parsed)
|
|
||||||
client_args.pop('insecure', None)
|
|
||||||
client_args['verify'] = self.verify
|
|
||||||
if 'os_auth_token' in client_args:
|
|
||||||
del client_args['os_auth_token']
|
|
||||||
|
|
||||||
self.client = solum_client.get_client(parsed.solum_api_version,
|
|
||||||
**client_args)
|
|
||||||
|
|
||||||
return self.info()
|
|
||||||
|
|
||||||
def info(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
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 filter_ready_lps(lp_list):
|
|
||||||
filtered_list = []
|
|
||||||
for lp in lp_list:
|
|
||||||
if lp.status == 'READY':
|
|
||||||
filtered_list.append(lp)
|
|
||||||
|
|
||||||
return filtered_list
|
|
@@ -1,71 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 time
|
|
||||||
|
|
||||||
from oslo_log import log as logging
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import client as api_client
|
|
||||||
from solumclient.common import exc
|
|
||||||
from solumclient import config
|
|
||||||
|
|
||||||
|
|
||||||
_logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class HTTPClient(api_client.HTTPClient):
|
|
||||||
def request(self, method, url, **kwargs):
|
|
||||||
"""Send an http request with the specified characteristics.
|
|
||||||
|
|
||||||
Wrapper around `requests.Session.request` to handle tasks such as
|
|
||||||
setting headers, JSON encoding/decoding, and error handling.
|
|
||||||
|
|
||||||
:param method: method of HTTP request
|
|
||||||
:param url: URL of HTTP request
|
|
||||||
:param kwargs: any other parameter that can be passed to
|
|
||||||
' requests.Session.request (such as `headers`) or `json`
|
|
||||||
that will be encoded as JSON and used as `data` argument
|
|
||||||
"""
|
|
||||||
|
|
||||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
|
||||||
kwargs["headers"]["User-Agent"] = self.user_agent
|
|
||||||
kwargs["headers"]["X-User-ID"] = config.username
|
|
||||||
kwargs["headers"]["X-Password"] = config.password
|
|
||||||
kwargs["headers"]["X-Project"] = config.tenant
|
|
||||||
if self.original_ip:
|
|
||||||
kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % (
|
|
||||||
self.original_ip, self.user_agent)
|
|
||||||
if self.timeout is not None:
|
|
||||||
kwargs.setdefault("timeout", self.timeout)
|
|
||||||
kwargs.setdefault("verify", self.verify)
|
|
||||||
if self.cert is not None:
|
|
||||||
kwargs.setdefault("cert", self.cert)
|
|
||||||
self.serialize(kwargs)
|
|
||||||
|
|
||||||
self._http_log_req(method, url, kwargs)
|
|
||||||
if self.timings:
|
|
||||||
start_time = time.time()
|
|
||||||
resp = self.http.request(method, url, **kwargs)
|
|
||||||
if self.timings:
|
|
||||||
self.times.append(("%s %s" % (method, url),
|
|
||||||
start_time, time.time()))
|
|
||||||
self._http_log_resp(resp)
|
|
||||||
|
|
||||||
if resp.status_code >= 400:
|
|
||||||
_logger.debug(
|
|
||||||
"Request returned failure status: %s",
|
|
||||||
resp.status_code)
|
|
||||||
raise exc.from_response(resp, method, url)
|
|
||||||
|
|
||||||
return resp
|
|
@@ -1,323 +0,0 @@
|
|||||||
# Copyright 2012 Red Hat, Inc.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
# W0603: Using the global statement
|
|
||||||
# W0621: Redefining name %s from outer scope
|
|
||||||
# pylint: disable=W0603,W0621
|
|
||||||
|
|
||||||
import getpass
|
|
||||||
import inspect
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import textwrap
|
|
||||||
|
|
||||||
import prettytable
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.i18n import _
|
|
||||||
|
|
||||||
from oslo_utils import encodeutils
|
|
||||||
from oslo_utils import strutils
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
|
|
||||||
def validate_args(fn, *args, **kwargs):
|
|
||||||
"""Check that the supplied args are sufficient for calling a function.
|
|
||||||
|
|
||||||
>>> validate_args(lambda a: None)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
MissingArgs: Missing argument(s): a
|
|
||||||
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
|
||||||
Traceback (most recent call last):
|
|
||||||
...
|
|
||||||
MissingArgs: Missing argument(s): b, d
|
|
||||||
|
|
||||||
:param fn: the function to check
|
|
||||||
:param arg: the positional arguments supplied
|
|
||||||
:param kwargs: the keyword arguments supplied
|
|
||||||
"""
|
|
||||||
argspec = inspect.getargspec(fn)
|
|
||||||
|
|
||||||
num_defaults = len(argspec.defaults or [])
|
|
||||||
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
|
||||||
|
|
||||||
def isbound(method):
|
|
||||||
return getattr(method, '__self__', None) is not None
|
|
||||||
|
|
||||||
if isbound(fn):
|
|
||||||
required_args.pop(0)
|
|
||||||
|
|
||||||
missing = [arg for arg in required_args if arg not in kwargs]
|
|
||||||
missing = missing[len(args):]
|
|
||||||
if missing:
|
|
||||||
raise exceptions.MissingArgs(missing)
|
|
||||||
|
|
||||||
|
|
||||||
def arg(*args, **kwargs):
|
|
||||||
"""Decorator for CLI args.
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
>>> @arg("name", help="Name of the new entity")
|
|
||||||
... def entity_create(args):
|
|
||||||
... pass
|
|
||||||
"""
|
|
||||||
def _decorator(func):
|
|
||||||
add_arg(func, *args, **kwargs)
|
|
||||||
return func
|
|
||||||
return _decorator
|
|
||||||
|
|
||||||
|
|
||||||
def env(*args, **kwargs):
|
|
||||||
"""Returns the first environment variable set.
|
|
||||||
|
|
||||||
If all are empty, defaults to '' or keyword arg `default`.
|
|
||||||
"""
|
|
||||||
for arg in args:
|
|
||||||
value = os.environ.get(arg)
|
|
||||||
if value:
|
|
||||||
return value
|
|
||||||
return kwargs.get('default', '')
|
|
||||||
|
|
||||||
|
|
||||||
def add_arg(func, *args, **kwargs):
|
|
||||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
|
||||||
|
|
||||||
if not hasattr(func, 'arguments'):
|
|
||||||
func.arguments = []
|
|
||||||
|
|
||||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
|
||||||
# tests.
|
|
||||||
if (args, kwargs) not in func.arguments:
|
|
||||||
# Because of the semantics of decorator composition if we just append
|
|
||||||
# to the options list positional options will appear to be backwards.
|
|
||||||
func.arguments.insert(0, (args, kwargs))
|
|
||||||
|
|
||||||
|
|
||||||
def unauthenticated(func):
|
|
||||||
"""Adds 'unauthenticated' attribute to decorated function.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
>>> @unauthenticated
|
|
||||||
... def mymethod(f):
|
|
||||||
... pass
|
|
||||||
"""
|
|
||||||
func.unauthenticated = True
|
|
||||||
return func
|
|
||||||
|
|
||||||
|
|
||||||
def isunauthenticated(func):
|
|
||||||
"""Checks if the function does not require authentication.
|
|
||||||
|
|
||||||
Mark such functions with the `@unauthenticated` decorator.
|
|
||||||
|
|
||||||
:returns: bool
|
|
||||||
"""
|
|
||||||
return getattr(func, 'unauthenticated', False)
|
|
||||||
|
|
||||||
|
|
||||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
|
||||||
mixed_case_fields=None, field_labels=None):
|
|
||||||
"""Print a list or objects as a table, one row per object.
|
|
||||||
|
|
||||||
:param objs: iterable of :class:`Resource`
|
|
||||||
:param fields: attributes that correspond to columns, in order
|
|
||||||
:param formatters: `dict` of callables for field formatting
|
|
||||||
:param sortby_index: index of the field for sorting table rows
|
|
||||||
:param mixed_case_fields: fields corresponding to object attributes that
|
|
||||||
have mixed case names (e.g., 'serverId')
|
|
||||||
:param field_labels: Labels to use in the heading of the table, default to
|
|
||||||
fields.
|
|
||||||
"""
|
|
||||||
formatters = formatters or {}
|
|
||||||
mixed_case_fields = mixed_case_fields or []
|
|
||||||
field_labels = field_labels or fields
|
|
||||||
if len(field_labels) != len(fields):
|
|
||||||
raise ValueError(_("Field labels list %(labels)s has different number "
|
|
||||||
"of elements than fields list %(fields)s"),
|
|
||||||
{'labels': field_labels, 'fields': fields})
|
|
||||||
|
|
||||||
if sortby_index is None:
|
|
||||||
kwargs = {}
|
|
||||||
else:
|
|
||||||
kwargs = {'sortby': field_labels[sortby_index]}
|
|
||||||
pt = prettytable.PrettyTable(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:
|
|
||||||
if field in mixed_case_fields:
|
|
||||||
field_name = field.replace(' ', '_')
|
|
||||||
else:
|
|
||||||
field_name = field.lower().replace(' ', '_')
|
|
||||||
data = getattr(o, field_name, '')
|
|
||||||
row.append(data)
|
|
||||||
pt.add_row(row)
|
|
||||||
|
|
||||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
|
|
||||||
|
|
||||||
|
|
||||||
def print_dict(dct, dict_property="Property", wrap=0):
|
|
||||||
"""Print a `dict` as a table of two columns.
|
|
||||||
|
|
||||||
:param dct: `dict` to print
|
|
||||||
:param dict_property: name of the first column
|
|
||||||
:param wrap: wrapping for the second column
|
|
||||||
"""
|
|
||||||
pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False)
|
|
||||||
pt.align = 'l'
|
|
||||||
for k, v in dct.items():
|
|
||||||
# convert dict to str to check length
|
|
||||||
if isinstance(v, dict):
|
|
||||||
v = str(v)
|
|
||||||
if wrap > 0:
|
|
||||||
v = textwrap.fill(str(v), wrap)
|
|
||||||
elif wrap < 0:
|
|
||||||
raise ValueError(_("Wrap argument should be a positive integer"))
|
|
||||||
# if value has a newline, add in multiple rows
|
|
||||||
# e.g. fault with stacktrace
|
|
||||||
if v and isinstance(v, str) and r'\n' in v:
|
|
||||||
lines = v.strip().split(r'\n')
|
|
||||||
col1 = k
|
|
||||||
for line in lines:
|
|
||||||
pt.add_row([col1, line])
|
|
||||||
col1 = ''
|
|
||||||
else:
|
|
||||||
pt.add_row([k, v])
|
|
||||||
print(encodeutils.safe_encode(pt.get_string()))
|
|
||||||
|
|
||||||
|
|
||||||
def get_password(max_password_prompts=3):
|
|
||||||
"""Read password from TTY."""
|
|
||||||
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
|
||||||
pw = None
|
|
||||||
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
|
||||||
# Check for Ctrl-D
|
|
||||||
try:
|
|
||||||
for __ in range(max_password_prompts):
|
|
||||||
pw1 = getpass.getpass("OS Password: ")
|
|
||||||
if verify:
|
|
||||||
pw2 = getpass.getpass("Please verify: ")
|
|
||||||
else:
|
|
||||||
pw2 = pw1
|
|
||||||
if pw1 == pw2 and pw1:
|
|
||||||
pw = pw1
|
|
||||||
break
|
|
||||||
except EOFError:
|
|
||||||
pass
|
|
||||||
return pw
|
|
||||||
|
|
||||||
|
|
||||||
def find_resource(manager, name_or_id, **find_args):
|
|
||||||
"""Look for resource in a given manager.
|
|
||||||
|
|
||||||
Used as a helper for the _find_* methods.
|
|
||||||
Example:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
def _find_hypervisor(cs, hypervisor):
|
|
||||||
#Get a hypervisor by name or ID.
|
|
||||||
return cliutils.find_resource(cs.hypervisors, hypervisor)
|
|
||||||
"""
|
|
||||||
# first try to get entity as integer id
|
|
||||||
try:
|
|
||||||
return manager.get(int(name_or_id))
|
|
||||||
except (TypeError, ValueError, exceptions.NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# now try to get entity as uuid
|
|
||||||
try:
|
|
||||||
if isinstance(name_or_id, str):
|
|
||||||
tmp_id = encodeutils.safe_encode(name_or_id)
|
|
||||||
else:
|
|
||||||
tmp_id = encodeutils.safe_decode(name_or_id)
|
|
||||||
|
|
||||||
if uuidutils.is_uuid_like(tmp_id):
|
|
||||||
return manager.get(tmp_id)
|
|
||||||
except (TypeError, ValueError, exceptions.NotFound):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# for str id which is not uuid
|
|
||||||
if getattr(manager, 'is_alphanum_id_allowed', False):
|
|
||||||
try:
|
|
||||||
return manager.get(name_or_id)
|
|
||||||
except exceptions.NotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
return manager.find(human_id=name_or_id, **find_args)
|
|
||||||
except exceptions.NotFound:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# finally try to find entity by name
|
|
||||||
try:
|
|
||||||
resource = getattr(manager, 'resource_class', None)
|
|
||||||
name_attr = resource.NAME_ATTR if resource else 'name'
|
|
||||||
kwargs = {name_attr: name_or_id}
|
|
||||||
kwargs.update(find_args)
|
|
||||||
return manager.find(**kwargs)
|
|
||||||
except exceptions.NotFound:
|
|
||||||
msg = _("No %(name)s with a name or "
|
|
||||||
"ID of '%(name_or_id)s' exists.") % {
|
|
||||||
"name": manager.resource_class.__name__.lower(),
|
|
||||||
"name_or_id": name_or_id
|
|
||||||
}
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
msg = _("Multiple %(name)s matches found for "
|
|
||||||
"'%(name_or_id)s', use an ID to be more specific.") % {
|
|
||||||
"name": manager.resource_class.__name__.lower(),
|
|
||||||
"name_or_id": name_or_id
|
|
||||||
}
|
|
||||||
raise exceptions.CommandError(msg)
|
|
||||||
|
|
||||||
|
|
||||||
def service_type(stype):
|
|
||||||
"""Adds 'service_type' attribute to decorated function.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
@service_type('volume')
|
|
||||||
def mymethod(f):
|
|
||||||
...
|
|
||||||
"""
|
|
||||||
def inner(f):
|
|
||||||
f.service_type = stype
|
|
||||||
return f
|
|
||||||
return inner
|
|
||||||
|
|
||||||
|
|
||||||
def get_service_type(f):
|
|
||||||
"""Retrieves service type from function."""
|
|
||||||
return getattr(f, 'service_type', None)
|
|
||||||
|
|
||||||
|
|
||||||
def pretty_choice_list(value):
|
|
||||||
return ', '.join("'%s'" % i for i in value)
|
|
||||||
|
|
||||||
|
|
||||||
def exit(msg=''):
|
|
||||||
if msg:
|
|
||||||
print(msg, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
@@ -1,77 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import exceptions
|
|
||||||
|
|
||||||
|
|
||||||
class CommandException(Exception):
|
|
||||||
"""An error occurred."""
|
|
||||||
def __init__(self, message=None):
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.message or self.__class__.__doc__
|
|
||||||
|
|
||||||
|
|
||||||
class CommandError(CommandException):
|
|
||||||
"""Invalid usage of CLI."""
|
|
||||||
|
|
||||||
|
|
||||||
class NotUnique(CommandException):
|
|
||||||
"""Name refers to more than one of a given resource."""
|
|
||||||
def __init__(self, resource='resource'):
|
|
||||||
message = "More than one %s by that name. Retry with the UUID."
|
|
||||||
self.message = message % resource
|
|
||||||
|
|
||||||
|
|
||||||
def from_response(response, method, url):
|
|
||||||
"""Returns an instance of :class:`HttpError` or subclass based on response.
|
|
||||||
|
|
||||||
:param response: instance of `requests.Response` class
|
|
||||||
:param method: HTTP method used for request
|
|
||||||
:param url: URL used for request
|
|
||||||
"""
|
|
||||||
kwargs = {
|
|
||||||
"http_status": response.status_code,
|
|
||||||
"response": response,
|
|
||||||
"method": method,
|
|
||||||
"url": url,
|
|
||||||
"request_id": response.headers.get("x-compute-request-id"),
|
|
||||||
}
|
|
||||||
if "retry-after" in response.headers:
|
|
||||||
kwargs["retry_after"] = response.headers["retry-after"]
|
|
||||||
|
|
||||||
content_type = response.headers.get("Content-Type", "")
|
|
||||||
if content_type.startswith("application/json"):
|
|
||||||
try:
|
|
||||||
body = response.json()
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if isinstance(body, dict):
|
|
||||||
kwargs["message"] = body.get("faultstring")
|
|
||||||
kwargs["details"] = body.get("debuginfo")
|
|
||||||
elif content_type.startswith("text/"):
|
|
||||||
kwargs["details"] = response.text
|
|
||||||
|
|
||||||
try:
|
|
||||||
cls = exceptions._code_map[response.status_code]
|
|
||||||
except KeyError:
|
|
||||||
if 500 <= response.status_code < 600:
|
|
||||||
cls = exceptions.HttpServerError
|
|
||||||
elif 400 <= response.status_code < 500:
|
|
||||||
cls = exceptions.HTTPClientError
|
|
||||||
else:
|
|
||||||
cls = exceptions.HttpError
|
|
||||||
return cls(**kwargs)
|
|
@@ -1,197 +0,0 @@
|
|||||||
# Copyright (c) 2015 Rackspace
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
# Tools for interacting with Github.
|
|
||||||
|
|
||||||
import base64
|
|
||||||
import getpass
|
|
||||||
import json
|
|
||||||
import random
|
|
||||||
import re
|
|
||||||
import string
|
|
||||||
|
|
||||||
import httplib2
|
|
||||||
|
|
||||||
|
|
||||||
class GitHubException(Exception):
|
|
||||||
def __init__(self, status_code, message):
|
|
||||||
self.status_code = status_code
|
|
||||||
self.message = message
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "GitHub Exception: %s: %s" % (self.status_code, self.message)
|
|
||||||
|
|
||||||
|
|
||||||
class GitHubAuth(object):
|
|
||||||
_github_auth_url = 'https://api.github.com/authorizations'
|
|
||||||
_github_repo_hook_url = 'https://api.github.com/repos/%s/hooks'
|
|
||||||
_github_user_key_url = 'https://api.github.com/user/keys'
|
|
||||||
_github_repo_regex = r'github\.com[:/](.+?)/(.+?)($|/$|\.git$|\.git/$)'
|
|
||||||
|
|
||||||
def __init__(self, git_url, username=None, password=None, repo_token=None):
|
|
||||||
self.git_url = git_url
|
|
||||||
|
|
||||||
user_org_name, repo = '', ''
|
|
||||||
|
|
||||||
repo_pat = re.compile(self._github_repo_regex)
|
|
||||||
match = repo_pat.search(self.git_url)
|
|
||||||
if match:
|
|
||||||
user_org_name, repo = match.group(1), match.group(2)
|
|
||||||
else:
|
|
||||||
raise ValueError("Failed to parse %s." % git_url)
|
|
||||||
|
|
||||||
self.user_org_name = user_org_name
|
|
||||||
|
|
||||||
self.full_repo_name = '/'.join([user_org_name, repo])
|
|
||||||
|
|
||||||
self._repo_token = repo_token
|
|
||||||
self._username = username
|
|
||||||
self._password = password
|
|
||||||
self._otp_required = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def username(self):
|
|
||||||
if self._username is None:
|
|
||||||
prompt = ("Username for repo '%s' [%s]:" %
|
|
||||||
(self.full_repo_name, self.user_org_name))
|
|
||||||
self._username = input(prompt) or self.user_org_name
|
|
||||||
return self._username
|
|
||||||
|
|
||||||
@property
|
|
||||||
def password(self):
|
|
||||||
if self._password is None:
|
|
||||||
self._password = getpass.getpass("Password: ")
|
|
||||||
return self._password
|
|
||||||
|
|
||||||
@property
|
|
||||||
def onetime_password(self):
|
|
||||||
# This is prompted for every time it's needed.
|
|
||||||
print("Two-Factor Authentication required.")
|
|
||||||
return getpass.getpass("2FA Token: ")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def repo_token(self):
|
|
||||||
if self._repo_token is None:
|
|
||||||
self.create_repo_token()
|
|
||||||
return self._repo_token
|
|
||||||
|
|
||||||
@property
|
|
||||||
def auth_header(self):
|
|
||||||
header = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
}
|
|
||||||
|
|
||||||
# The token on its own should suffice
|
|
||||||
if self._repo_token:
|
|
||||||
header['Authorization'] = 'token %s' % self._repo_token
|
|
||||||
return header
|
|
||||||
|
|
||||||
# This will prompt the user if either name or pass is missing.
|
|
||||||
authstring = '%s:%s' % (self.username, self.password)
|
|
||||||
|
|
||||||
basic_auth = base64.encodebytes(bytes(authstring, 'utf-8'))
|
|
||||||
basic_auth = basic_auth.decode('utf-8')
|
|
||||||
basic_auth = basic_auth.strip()
|
|
||||||
header['Authorization'] = 'Basic %s' % basic_auth
|
|
||||||
|
|
||||||
# This will prompt for the OTP.
|
|
||||||
if self._otp_required:
|
|
||||||
header['x-github-otp'] = self.onetime_password
|
|
||||||
|
|
||||||
return header
|
|
||||||
|
|
||||||
def _send_authed_request(self, url, body_dict):
|
|
||||||
body_text = json.dumps(body_dict)
|
|
||||||
|
|
||||||
resp, content = httplib2.Http().request(
|
|
||||||
url, 'POST',
|
|
||||||
headers=self.auth_header,
|
|
||||||
body=body_text)
|
|
||||||
|
|
||||||
if resp.get('status') in ['401']:
|
|
||||||
if resp.get('x-github-otp', '').startswith('required'):
|
|
||||||
self._otp_required = True
|
|
||||||
resp, content = httplib2.Http().request(
|
|
||||||
url, 'POST',
|
|
||||||
headers=self.auth_header,
|
|
||||||
body=body_text)
|
|
||||||
|
|
||||||
return resp, content
|
|
||||||
|
|
||||||
def create_repo_token(self):
|
|
||||||
print("Creating repo token")
|
|
||||||
note = ''.join(random.sample(string.lowercase, 5))
|
|
||||||
auth_info = {
|
|
||||||
'scopes': ['repo', 'write:public_key', 'write:repo_hook'],
|
|
||||||
'note': 'Solum-status-%s' % note,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, content = self._send_authed_request(
|
|
||||||
self._github_auth_url,
|
|
||||||
auth_info)
|
|
||||||
|
|
||||||
status_code = int(resp.get('status', '500'))
|
|
||||||
response_body = json.loads(content)
|
|
||||||
if status_code in [200, 201]:
|
|
||||||
self._repo_token = response_body.get('token')
|
|
||||||
print("Successfully created repo token %s." % auth_info['note'])
|
|
||||||
return self._repo_token
|
|
||||||
elif status_code >= 400 and status_code < 600:
|
|
||||||
message = response_body.get('message',
|
|
||||||
'No error message provided.')
|
|
||||||
raise GitHubException(status_code, message)
|
|
||||||
|
|
||||||
def create_webhook(self, trigger_uri, workflow=None):
|
|
||||||
print("Creating webhook for repo.")
|
|
||||||
hook_url = self._github_repo_hook_url % self.full_repo_name
|
|
||||||
if workflow is not None:
|
|
||||||
# workflow is a list of strings, likely
|
|
||||||
# ['unittest', 'build', 'deploy'].
|
|
||||||
# They're joined with + and appended to the
|
|
||||||
# trigger_uri here.
|
|
||||||
wf_query = "?workflow=%s" % '+'.join(workflow)
|
|
||||||
trigger_uri += wf_query
|
|
||||||
|
|
||||||
hook_info = {
|
|
||||||
'name': 'web',
|
|
||||||
'events': ['pull_request', 'commit_comment'],
|
|
||||||
'config': {
|
|
||||||
'content_type': 'json',
|
|
||||||
'url': trigger_uri,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp, content = self._send_authed_request(hook_url, hook_info)
|
|
||||||
if resp.get('status') in ['200', '201']:
|
|
||||||
print("Successfully created webhook.")
|
|
||||||
else:
|
|
||||||
print("Error creating webhook.")
|
|
||||||
|
|
||||||
def add_ssh_key(self, public_key=None):
|
|
||||||
if not public_key:
|
|
||||||
print("No public key to upload.")
|
|
||||||
return
|
|
||||||
print("Uploading public key to user account.")
|
|
||||||
api_url = self._github_user_key_url
|
|
||||||
|
|
||||||
key_info = {
|
|
||||||
'title': 'devops@Solum',
|
|
||||||
'key': public_key,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, content = self._send_authed_request(api_url, key_info)
|
|
||||||
if resp.get('status') in ['200', '201']:
|
|
||||||
print("Successfully uploaded public key.")
|
|
||||||
else:
|
|
||||||
print("Error uploading public key.")
|
|
@@ -1,50 +0,0 @@
|
|||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from debtcollector import removals
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
removals.removed_module(
|
|
||||||
'solumclient.common.yamlutils', version='3.0.0', removal_version='4.0.0',
|
|
||||||
message='The solumclient.common.yamlutils will be removed')
|
|
||||||
|
|
||||||
|
|
||||||
if hasattr(yaml, 'CSafeLoader'):
|
|
||||||
yaml_loader = yaml.CSafeLoader
|
|
||||||
else:
|
|
||||||
yaml_loader = yaml.SafeLoader
|
|
||||||
|
|
||||||
|
|
||||||
if hasattr(yaml, 'CSafeDumper'):
|
|
||||||
yaml_dumper = yaml.CSafeDumper
|
|
||||||
else:
|
|
||||||
yaml_dumper = yaml.SafeDumper
|
|
||||||
|
|
||||||
|
|
||||||
def load(s):
|
|
||||||
try:
|
|
||||||
yml_dict = yaml.load(s, yaml_loader)
|
|
||||||
except yaml.YAMLError as exc:
|
|
||||||
msg = 'An error occurred during YAML parsing.'
|
|
||||||
if hasattr(exc, 'problem_mark'):
|
|
||||||
msg += ' Error position: (%s:%s)' % (exc.problem_mark.line + 1,
|
|
||||||
exc.problem_mark.column + 1)
|
|
||||||
raise ValueError(msg)
|
|
||||||
if not isinstance(yml_dict, dict) and not isinstance(yml_dict, list):
|
|
||||||
raise ValueError('The source is not a YAML mapping or list.')
|
|
||||||
if isinstance(yml_dict, dict) and len(yml_dict) < 1:
|
|
||||||
raise ValueError('Could not find any element in your YAML mapping.')
|
|
||||||
return yml_dict
|
|
||||||
|
|
||||||
|
|
||||||
def dump(s):
|
|
||||||
return yaml.dump(s, Dumper=yaml_dumper)
|
|
@@ -1,21 +0,0 @@
|
|||||||
# Copyright (c) 2016 Rackspace
|
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
|
|
||||||
username = ''
|
|
||||||
password = ''
|
|
||||||
tenant = ''
|
|
||||||
auth_url = ''
|
|
||||||
tenant_name = ''
|
|
@@ -1,24 +0,0 @@
|
|||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
"""oslo.i18n integration module.
|
|
||||||
|
|
||||||
See https://docs.openstack.org/oslo.i18n/latest/user/usage.html
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import oslo_i18n
|
|
||||||
|
|
||||||
_translators = oslo_i18n.TranslatorFactory(domain='solumclient')
|
|
||||||
|
|
||||||
# The primary translation function using the well-known name "_"
|
|
||||||
_ = _translators.primary
|
|
@@ -1,159 +0,0 @@
|
|||||||
# Andi Chandler <andi@gowling.com>, 2017. #zanata
|
|
||||||
msgid ""
|
|
||||||
msgstr ""
|
|
||||||
"Project-Id-Version: python-solumclient VERSION\n"
|
|
||||||
"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n"
|
|
||||||
"POT-Creation-Date: 2018-01-24 07:29+0000\n"
|
|
||||||
"MIME-Version: 1.0\n"
|
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
|
||||||
"PO-Revision-Date: 2017-10-06 07:48+0000\n"
|
|
||||||
"Last-Translator: Andi Chandler <andi@gowling.com>\n"
|
|
||||||
"Language-Team: English (United Kingdom)\n"
|
|
||||||
"Language: en_GB\n"
|
|
||||||
"X-Generator: Zanata 4.3.3\n"
|
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "AmbiguousEndpoints: %s"
|
|
||||||
msgstr "AmbiguousEndpoints: %s"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "AuthSystemNotFound: %s"
|
|
||||||
msgstr "AuthSystemNotFound: %s"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Authentication failed. Missing options: %s"
|
|
||||||
msgstr "Authentication failed. Missing options: %s"
|
|
||||||
|
|
||||||
msgid "Bad Gateway"
|
|
||||||
msgstr "Bad Gateway"
|
|
||||||
|
|
||||||
msgid "Bad Request"
|
|
||||||
msgstr "Bad Request"
|
|
||||||
|
|
||||||
msgid "Cannot find endpoint or token for request"
|
|
||||||
msgstr "Cannot find endpoint or token for request"
|
|
||||||
|
|
||||||
msgid "Conflict"
|
|
||||||
msgstr "Conflict"
|
|
||||||
|
|
||||||
msgid "Expectation Failed"
|
|
||||||
msgstr "Expectation Failed"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Field labels list %(labels)s has different number of elements than fields "
|
|
||||||
"list %(fields)s"
|
|
||||||
msgstr ""
|
|
||||||
"Field labels list %(labels)s has different number of elements than fields "
|
|
||||||
"list %(fields)s"
|
|
||||||
|
|
||||||
msgid "Forbidden"
|
|
||||||
msgstr "Forbidden"
|
|
||||||
|
|
||||||
msgid "Gateway Timeout"
|
|
||||||
msgstr "Gateway Timeout"
|
|
||||||
|
|
||||||
msgid "Gone"
|
|
||||||
msgstr "Gone"
|
|
||||||
|
|
||||||
msgid "HTTP Client Error"
|
|
||||||
msgstr "HTTP Client Error"
|
|
||||||
|
|
||||||
msgid "HTTP Error"
|
|
||||||
msgstr "HTTP Error"
|
|
||||||
|
|
||||||
msgid "HTTP Redirection"
|
|
||||||
msgstr "HTTP Redirection"
|
|
||||||
|
|
||||||
msgid "HTTP Server Error"
|
|
||||||
msgstr "HTTP Server Error"
|
|
||||||
|
|
||||||
msgid "HTTP Version Not Supported"
|
|
||||||
msgstr "HTTP Version Not Supported"
|
|
||||||
|
|
||||||
msgid "Internal Server Error"
|
|
||||||
msgstr "Internal Server Error"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Invalid %(api_name)s client version '%(version)s'. Must be one of: "
|
|
||||||
"%(version_map)s"
|
|
||||||
msgstr ""
|
|
||||||
"Invalid %(api_name)s client version '%(version)s'. Must be one of: "
|
|
||||||
"%(version_map)s"
|
|
||||||
|
|
||||||
msgid "Length Required"
|
|
||||||
msgstr "Length Required"
|
|
||||||
|
|
||||||
msgid "Method Not Allowed"
|
|
||||||
msgstr "Method Not Allowed"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "Missing arguments: %s"
|
|
||||||
msgstr "Missing arguments: %s"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid ""
|
|
||||||
"Multiple %(name)s matches found for '%(name_or_id)s', use an ID to be more "
|
|
||||||
"specific."
|
|
||||||
msgstr ""
|
|
||||||
"Multiple %(name)s matches found for '%(name_or_id)s', use an ID to be more "
|
|
||||||
"specific."
|
|
||||||
|
|
||||||
msgid "Multiple Choices"
|
|
||||||
msgstr "Multiple Choices"
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "No %(name)s matching %(args)s."
|
|
||||||
msgstr "No %(name)s matching %(args)s."
|
|
||||||
|
|
||||||
#, python-format
|
|
||||||
msgid "No %(name)s with a name or ID of '%(name_or_id)s' exists."
|
|
||||||
msgstr "No %(name)s with a name or ID of '%(name_or_id)s' exists."
|
|
||||||
|
|
||||||
msgid "Not Acceptable"
|
|
||||||
msgstr "Not Acceptable"
|
|
||||||
|
|
||||||
msgid "Not Found"
|
|
||||||
msgstr "Not Found"
|
|
||||||
|
|
||||||
msgid "Not Implemented"
|
|
||||||
msgstr "Not Implemented"
|
|
||||||
|
|
||||||
msgid "Payment Required"
|
|
||||||
msgstr "Payment Required"
|
|
||||||
|
|
||||||
msgid "Precondition Failed"
|
|
||||||
msgstr "Precondition Failed"
|
|
||||||
|
|
||||||
msgid "Proxy Authentication Required"
|
|
||||||
msgstr "Proxy Authentication Required"
|
|
||||||
|
|
||||||
msgid "Request Entity Too Large"
|
|
||||||
msgstr "Request Entity Too Large"
|
|
||||||
|
|
||||||
msgid "Request Timeout"
|
|
||||||
msgstr "Request Timeout"
|
|
||||||
|
|
||||||
msgid "Request-URI Too Long"
|
|
||||||
msgstr "Request-URI Too Long"
|
|
||||||
|
|
||||||
msgid "Requested Range Not Satisfiable"
|
|
||||||
msgstr "Requested Range Not Satisfiable"
|
|
||||||
|
|
||||||
msgid "Service Unavailable"
|
|
||||||
msgstr "Service Unavailable"
|
|
||||||
|
|
||||||
msgid "Unauthorized"
|
|
||||||
msgstr "Unauthorised"
|
|
||||||
|
|
||||||
msgid "Unprocessable Entity"
|
|
||||||
msgstr "Unprocessable Entity"
|
|
||||||
|
|
||||||
msgid "Unsupported Media Type"
|
|
||||||
msgstr "Unsupported Media Type"
|
|
||||||
|
|
||||||
msgid "Wrap argument should be a positive integer"
|
|
||||||
msgstr "Wrap argument should be a positive integer"
|
|
1934
solumclient/solum.py
1934
solumclient/solum.py
File diff suppressed because it is too large
Load Diff
@@ -1,31 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
# Copyright 2010-2011 OpenStack Foundation
|
|
||||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
import fixtures
|
|
||||||
from oslotest import base
|
|
||||||
import testscenarios
|
|
||||||
|
|
||||||
|
|
||||||
class TestCase(testscenarios.WithScenarios, base.BaseTestCase):
|
|
||||||
|
|
||||||
"""Test case base class for all unit tests."""
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Run before each test method to initialize test environment."""
|
|
||||||
|
|
||||||
super(TestCase, self).setUp()
|
|
||||||
self.log_fixture = self.useFixture(fixtures.FakeLogger())
|
|
@@ -1,31 +0,0 @@
|
|||||||
# Copyright 2014 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 unittest import mock
|
|
||||||
|
|
||||||
from solumclient.builder import client
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import auth
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class ClientTest(base.TestCase):
|
|
||||||
|
|
||||||
def test_client_unsupported_version(self):
|
|
||||||
self.assertRaises(exceptions.UnsupportedVersion,
|
|
||||||
client.Client, '111.11', **{})
|
|
||||||
|
|
||||||
def test_client(self):
|
|
||||||
with mock.patch.object(auth, 'KeystoneAuthPlugin'):
|
|
||||||
client.Client('1', **{})
|
|
@@ -1,73 +0,0 @@
|
|||||||
# Copyright 2014 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.builder.v1 import client as builder_client
|
|
||||||
from solumclient.builder.v1 import image
|
|
||||||
from solumclient.common.apiclient import fake_client
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
image_fixture = {
|
|
||||||
'uri': 'http://example.com/v1/images/i1',
|
|
||||||
'name': 'php-web-app',
|
|
||||||
'source_uri': 'git://example.com/project/app.git',
|
|
||||||
'type': 'image',
|
|
||||||
'description': 'A php web application',
|
|
||||||
'tags': ['small'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_get = {
|
|
||||||
'/v1/images/i1': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
image_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_create = {
|
|
||||||
'/v1/images': {
|
|
||||||
'POST': (
|
|
||||||
{},
|
|
||||||
image_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ImageManagerTest(base.TestCase):
|
|
||||||
|
|
||||||
def test_create(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_create)
|
|
||||||
api_client = builder_client.Client(fake_http_client)
|
|
||||||
mgr = image.ImageManager(api_client)
|
|
||||||
image_obj = mgr.create()
|
|
||||||
self.assertIn('Image', repr(image_obj))
|
|
||||||
self.assertEqual(image_fixture['uri'], image_obj.uri)
|
|
||||||
self.assertEqual(image_fixture['type'], image_obj.type)
|
|
||||||
self.assertEqual(image_fixture['project_id'], image_obj.project_id)
|
|
||||||
self.assertEqual(image_fixture['user_id'], image_obj.user_id)
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_get)
|
|
||||||
api_client = builder_client.Client(fake_http_client)
|
|
||||||
mgr = image.ImageManager(api_client)
|
|
||||||
image_obj = mgr.get(image_id='i1')
|
|
||||||
self.assertIn('Image', repr(image_obj))
|
|
||||||
self.assertEqual(image_fixture['uri'], image_obj.uri)
|
|
||||||
self.assertEqual(image_fixture['type'], image_obj.type)
|
|
||||||
self.assertEqual(image_fixture['project_id'], image_obj.project_id)
|
|
||||||
self.assertEqual(image_fixture['user_id'], image_obj.user_id)
|
|
@@ -1,101 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 unittest import mock
|
|
||||||
|
|
||||||
from keystoneclient.v2_0 import client as ksclient
|
|
||||||
import testtools
|
|
||||||
|
|
||||||
from solumclient.common import auth
|
|
||||||
from solumclient.common import client
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(ksclient, 'Client')
|
|
||||||
class KeystoneAuthPluginTest(base.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(KeystoneAuthPluginTest, self).setUp()
|
|
||||||
plugin = auth.KeystoneAuthPlugin(
|
|
||||||
username="fake-username",
|
|
||||||
password="fake-password",
|
|
||||||
tenant_name="fake-tenant-name",
|
|
||||||
project_domain_name="default",
|
|
||||||
user_domain_name="default",
|
|
||||||
auth_url="http://auth")
|
|
||||||
self.cs = client.HTTPClient(auth_plugin=plugin)
|
|
||||||
|
|
||||||
@testtools.skip("Skip it when found solution")
|
|
||||||
def test_authenticate(self, mock_ksclient):
|
|
||||||
self.cs.authenticate()
|
|
||||||
mock_ksclient.assert_called_with(
|
|
||||||
username="fake-username",
|
|
||||||
password="fake-password",
|
|
||||||
tenant_name="fake-tenant-name",
|
|
||||||
project_domain_name="default",
|
|
||||||
user_domain_name="default",
|
|
||||||
auth_url="http://auth")
|
|
||||||
|
|
||||||
def test_token_and_endpoint(self, mock_ksclient):
|
|
||||||
plugin = auth.KeystoneAuthPlugin(
|
|
||||||
endpoint="http://solum",
|
|
||||||
token="test_token")
|
|
||||||
self.cs = client.HTTPClient(auth_plugin=plugin)
|
|
||||||
self.cs.authenticate()
|
|
||||||
(token, endpoint) = self.cs.auth_plugin.token_and_endpoint(
|
|
||||||
"fake-endpoint-type", "fake-service-type")
|
|
||||||
self.assertEqual(token, "test_token")
|
|
||||||
self.assertEqual("http://solum", endpoint)
|
|
||||||
|
|
||||||
def test_token_and_endpoint_before_auth(self, mock_ksclient):
|
|
||||||
(token, endpoint) = self.cs.auth_plugin.token_and_endpoint(
|
|
||||||
"fake-endpoint-type", "fake-service-type")
|
|
||||||
self.assertIsNone(token, None)
|
|
||||||
self.assertIsNone(endpoint, None)
|
|
||||||
|
|
||||||
@testtools.skip("Skip it when found solution")
|
|
||||||
def test_endpoint_with_no_token(self, mock_ksclient):
|
|
||||||
plugin = auth.KeystoneAuthPlugin(
|
|
||||||
username="fake-username",
|
|
||||||
password="fake-password",
|
|
||||||
tenant_name="fake-tenant-name",
|
|
||||||
project_domain_name="default",
|
|
||||||
user_domain_name="default",
|
|
||||||
auth_url="http://auth",
|
|
||||||
endpoint="http://solum")
|
|
||||||
self.cs = client.HTTPClient(auth_plugin=plugin)
|
|
||||||
self.cs.authenticate()
|
|
||||||
mock_ksclient.assert_called_with(
|
|
||||||
username="fake-username",
|
|
||||||
password="fake-password",
|
|
||||||
tenant_name="fake-tenant-name",
|
|
||||||
auth_url="http://auth")
|
|
||||||
(token, endpoint) = self.cs.auth_plugin.token_and_endpoint(
|
|
||||||
"fake-endpoint-type", "fake-service-type")
|
|
||||||
self.assertIsInstance(token, mock.MagicMock)
|
|
||||||
self.assertEqual("http://solum", endpoint)
|
|
||||||
|
|
||||||
|
|
||||||
@mock.patch.object(ksclient, 'Client')
|
|
||||||
class KeystoneAuthPluginTokenTest(base.TestCase):
|
|
||||||
def test_token_and_endpoint(self, mock_ksclient):
|
|
||||||
plugin = auth.KeystoneAuthPlugin(
|
|
||||||
token="fake-token",
|
|
||||||
endpoint="http://solum")
|
|
||||||
cs = client.HTTPClient(auth_plugin=plugin)
|
|
||||||
|
|
||||||
cs.authenticate()
|
|
||||||
(token, endpoint) = cs.auth_plugin.token_and_endpoint(
|
|
||||||
"fake-endpoint-type", "fake-service-type")
|
|
||||||
self.assertEqual('fake-token', token)
|
|
||||||
self.assertEqual('http://solum', endpoint)
|
|
@@ -1,115 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base
|
|
||||||
from solumclient.common.apiclient import client
|
|
||||||
from solumclient.common.apiclient import fake_client
|
|
||||||
from solumclient.tests import base as test_base
|
|
||||||
|
|
||||||
|
|
||||||
fixture1 = {
|
|
||||||
'/foo_resource': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
{'id': 1, 'name': 'foo'}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixture2 = {
|
|
||||||
'/foo_resource': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
{'foo_resource': {'id': 1, 'name': 'foo'}}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixture3 = {
|
|
||||||
'/foo_resources': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
[
|
|
||||||
{'id': 1, 'name': 'foo'},
|
|
||||||
{'id': 2, 'name': 'bar'}
|
|
||||||
]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixture4 = {
|
|
||||||
'/foo_resources': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
{'foo_resources': [
|
|
||||||
{'id': 1, 'name': 'foo'},
|
|
||||||
{'id': 2, 'name': 'bar'}
|
|
||||||
]}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class FooResource(base.Resource):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class FooResourceManager(base.BaseManager):
|
|
||||||
resource_class = FooResource
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
return self._get("/foo_resource")
|
|
||||||
|
|
||||||
def get_with_response_key(self):
|
|
||||||
return self._get("/foo_resource", "foo_resource")
|
|
||||||
|
|
||||||
def list(self):
|
|
||||||
return self._list("/foo_resources")
|
|
||||||
|
|
||||||
def list_with_response_key(self):
|
|
||||||
return self._list("/foo_resources", "foo_resources")
|
|
||||||
|
|
||||||
|
|
||||||
class TestClient(client.BaseClient):
|
|
||||||
|
|
||||||
service_type = "test"
|
|
||||||
|
|
||||||
def __init__(self, http_client, extensions=None):
|
|
||||||
super(TestClient, self).__init__(
|
|
||||||
http_client, extensions=extensions)
|
|
||||||
|
|
||||||
self.foo_resource = FooResourceManager(self)
|
|
||||||
|
|
||||||
|
|
||||||
class BaseManagerTest(test_base.TestCase):
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
http_client = fake_client.FakeHTTPClient(fixtures=fixture1)
|
|
||||||
tc = TestClient(http_client)
|
|
||||||
tc.foo_resource.get()
|
|
||||||
|
|
||||||
def test_get_with_response_key(self):
|
|
||||||
http_client = fake_client.FakeHTTPClient(fixtures=fixture2)
|
|
||||||
tc = TestClient(http_client)
|
|
||||||
tc.foo_resource.get_with_response_key()
|
|
||||||
|
|
||||||
def test_list(self):
|
|
||||||
http_client = fake_client.FakeHTTPClient(fixtures=fixture3)
|
|
||||||
tc = TestClient(http_client)
|
|
||||||
tc.foo_resource.list()
|
|
||||||
|
|
||||||
def test_list_with_response_key(self):
|
|
||||||
http_client = fake_client.FakeHTTPClient(fixtures=fixture4)
|
|
||||||
tc = TestClient(http_client)
|
|
||||||
tc.foo_resource.list_with_response_key()
|
|
@@ -1,99 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 requests
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import auth
|
|
||||||
from solumclient.common.apiclient import client as api_client
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import client
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestClient(api_client.BaseClient):
|
|
||||||
service_type = "test"
|
|
||||||
|
|
||||||
|
|
||||||
class FakeAuthPlugin(auth.BaseAuthPlugin):
|
|
||||||
auth_system = "fake"
|
|
||||||
attempt = -1
|
|
||||||
|
|
||||||
def _do_authenticate(self, http_client):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def token_and_endpoint(self, endpoint_type, service_type):
|
|
||||||
self.attempt = self.attempt + 1
|
|
||||||
return ("token-%s" % self.attempt, "/endpoint-%s" % self.attempt)
|
|
||||||
|
|
||||||
|
|
||||||
class ClientTest(base.TestCase):
|
|
||||||
def test_client_request(self):
|
|
||||||
http_client = client.HTTPClient(FakeAuthPlugin())
|
|
||||||
mock_request = mock.Mock()
|
|
||||||
mock_request.return_value = requests.Response()
|
|
||||||
mock_request.return_value.status_code = 200
|
|
||||||
with mock.patch("requests.Session.request", mock_request):
|
|
||||||
http_client.client_request(
|
|
||||||
TestClient(http_client), "GET", "/resource", json={"1": "2"})
|
|
||||||
requests.Session.request.assert_called_with(
|
|
||||||
"GET",
|
|
||||||
"/endpoint-0/resource",
|
|
||||||
headers={
|
|
||||||
"User-Agent": http_client.user_agent,
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-Auth-Token": "token-0",
|
|
||||||
"X-Password": "",
|
|
||||||
"X-User-ID": "",
|
|
||||||
"X-Project": ""
|
|
||||||
},
|
|
||||||
data='{"1": "2"}',
|
|
||||||
verify=True)
|
|
||||||
|
|
||||||
def test_client_with_response_404_status_code(self):
|
|
||||||
http_client = client.HTTPClient(FakeAuthPlugin())
|
|
||||||
mock_request = mock.Mock()
|
|
||||||
mock_request.return_value = requests.Response()
|
|
||||||
mock_request.return_value.status_code = 404
|
|
||||||
with mock.patch("requests.Session.request", mock_request):
|
|
||||||
self.assertRaises(
|
|
||||||
exceptions.HttpError, http_client.client_request,
|
|
||||||
TestClient(http_client), "GET", "/resource")
|
|
||||||
|
|
||||||
def test_client_with_invalid_endpoint(self):
|
|
||||||
http_client = client.HTTPClient(FakeAuthPlugin())
|
|
||||||
mock_request = mock.Mock()
|
|
||||||
mock_request.side_effect = requests.ConnectionError
|
|
||||||
with mock.patch("requests.Session.request", mock_request):
|
|
||||||
self.assertRaises(
|
|
||||||
requests.ConnectionError, http_client.client_request,
|
|
||||||
TestClient(http_client), "GET", "/resource")
|
|
||||||
|
|
||||||
def test_client_with_invalid_service_catalog(self):
|
|
||||||
http_client = client.HTTPClient(FakeAuthPlugin())
|
|
||||||
mock_request = mock.Mock()
|
|
||||||
mock_request.side_effect = exceptions.EndpointException
|
|
||||||
with mock.patch("requests.Session.request", mock_request):
|
|
||||||
self.assertRaises(
|
|
||||||
exceptions.EndpointException, http_client.client_request,
|
|
||||||
TestClient(http_client), "GET", "/resource")
|
|
||||||
|
|
||||||
def test_client_with_connection_refused(self):
|
|
||||||
http_client = client.HTTPClient(FakeAuthPlugin())
|
|
||||||
mock_request = mock.Mock()
|
|
||||||
mock_request.side_effect = exceptions.ConnectionRefused
|
|
||||||
with mock.patch("requests.Session.request", mock_request):
|
|
||||||
self.assertRaises(
|
|
||||||
exceptions.ConnectionRefused, http_client.client_request,
|
|
||||||
TestClient(http_client), "GET", "/resource")
|
|
@@ -1,143 +0,0 @@
|
|||||||
# Copyright (c) 2014 Rackspace
|
|
||||||
#
|
|
||||||
# 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 unittest import mock
|
|
||||||
|
|
||||||
import fixtures
|
|
||||||
|
|
||||||
from solumclient import client as solum_client
|
|
||||||
from solumclient.common import cli_utils
|
|
||||||
import solumclient.solum
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestCli_Utils(base.TestCase):
|
|
||||||
"Test commandbase"
|
|
||||||
scenarios = [
|
|
||||||
('username', dict(
|
|
||||||
fake_env={'OS_USERNAME': 'username',
|
|
||||||
'OS_PASSWORD': 'password',
|
|
||||||
'OS_TENANT_NAME': '',
|
|
||||||
'OS_TENANT_ID': '',
|
|
||||||
'OS_PROJECT_NAME': 'project_name',
|
|
||||||
'OS_PROJECT_ID': '',
|
|
||||||
'OS_PROJECT_DOMAIN_NAME': 'default',
|
|
||||||
'OS_PROJECT_DOMAIN_ID': '',
|
|
||||||
'OS_USER_DOMAIN_NAME': 'default',
|
|
||||||
'OS_USER_DOMAIN_ID': '',
|
|
||||||
'OS_REGION_NAME': 'RegionOne',
|
|
||||||
'OS_AUTH_URL': 'http://no.where'},
|
|
||||||
output={'solum_api_version': '1',
|
|
||||||
'os_username': 'username',
|
|
||||||
'solum_url': '',
|
|
||||||
'os_tenant_name': '',
|
|
||||||
'os_auth_url': 'http://no.where',
|
|
||||||
'os_password': 'password',
|
|
||||||
'os_tenant_id': '',
|
|
||||||
'os_project_name': 'project_name',
|
|
||||||
'os_project_id': '',
|
|
||||||
'os_project_domain_name': 'default',
|
|
||||||
'os_project_domain_id': '',
|
|
||||||
'os_user_domain_name': 'default',
|
|
||||||
'os_user_domain_id': '',
|
|
||||||
'os_region_name': 'RegionOne',
|
|
||||||
'action': 'create',
|
|
||||||
'json': False,
|
|
||||||
'verify': True,
|
|
||||||
'debug': False
|
|
||||||
})),
|
|
||||||
('token', dict(
|
|
||||||
fake_env={'OS_AUTH_TOKEN': '123456',
|
|
||||||
'SOLUM_URL': 'http://10.0.2.15:9777'},
|
|
||||||
output={'os_auth_url': '',
|
|
||||||
'solum_url': 'http://10.0.2.15:9777',
|
|
||||||
'solum_api_version': '1',
|
|
||||||
'os_username': '',
|
|
||||||
'os_tenant_name': '',
|
|
||||||
'os_password': '',
|
|
||||||
'os_tenant_id': '',
|
|
||||||
'os_project_name': '',
|
|
||||||
'os_project_id': '',
|
|
||||||
'os_project_domain_name': '',
|
|
||||||
'os_project_domain_id': '',
|
|
||||||
'os_user_domain_name': '',
|
|
||||||
'os_user_domain_id': '',
|
|
||||||
'os_region_name': '',
|
|
||||||
'action': 'create',
|
|
||||||
'json': False,
|
|
||||||
'verify': True,
|
|
||||||
'debug': False
|
|
||||||
})),
|
|
||||||
('solum_url_with_no_token', dict(
|
|
||||||
fake_env={'OS_USERNAME': 'username',
|
|
||||||
'OS_PASSWORD': 'password',
|
|
||||||
'OS_TENANT_NAME': '',
|
|
||||||
'OS_TENANT_ID': '',
|
|
||||||
'OS_PROJECT_NAME': 'project_name',
|
|
||||||
'OS_PROJECT_ID': '',
|
|
||||||
'OS_PROJECT_DOMAIN_NAME': 'default',
|
|
||||||
'OS_PROJECT_DOMAIN_ID': '',
|
|
||||||
'OS_USER_DOMAIN_NAME': 'default',
|
|
||||||
'OS_USER_DOMAIN_ID': '',
|
|
||||||
'OS_REGION_NAME': 'RegionOne',
|
|
||||||
'OS_AUTH_URL': 'http://no.where',
|
|
||||||
'SOLUM_URL': 'http://10.0.2.15:9777'},
|
|
||||||
output={'os_auth_url': 'http://no.where',
|
|
||||||
'solum_url': 'http://10.0.2.15:9777',
|
|
||||||
'solum_api_version': '1',
|
|
||||||
'os_username': 'username',
|
|
||||||
'os_tenant_name': '',
|
|
||||||
'os_password': 'password',
|
|
||||||
'os_tenant_id': '',
|
|
||||||
'os_project_name': 'project_name',
|
|
||||||
'os_project_id': '',
|
|
||||||
'os_project_domain_name': 'default',
|
|
||||||
'os_project_domain_id': '',
|
|
||||||
'os_user_domain_name': 'default',
|
|
||||||
'os_user_domain_id': '',
|
|
||||||
'os_region_name': 'RegionOne',
|
|
||||||
'action': 'create',
|
|
||||||
'json': False,
|
|
||||||
'verify': True,
|
|
||||||
'debug': False
|
|
||||||
})),
|
|
||||||
]
|
|
||||||
|
|
||||||
# Patch os.environ to avoid reading auth info
|
|
||||||
# from environment or command line.
|
|
||||||
def make_env(self, exclude=None):
|
|
||||||
env = dict((k, v) for k, v in self.fake_env.items() if k != exclude)
|
|
||||||
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
|
|
||||||
|
|
||||||
def fake_argv(self):
|
|
||||||
self.useFixture(fixtures.MonkeyPatch('sys.argv', ['foo', 'create']))
|
|
||||||
|
|
||||||
@mock.patch.object(solum_client, "get_client")
|
|
||||||
def test_env_parsing(self, mock_get_client):
|
|
||||||
parser = solumclient.solum.PermissiveParser()
|
|
||||||
|
|
||||||
self.make_env()
|
|
||||||
self.fake_argv()
|
|
||||||
FakeCommands(parser)
|
|
||||||
mock_get_client.assert_called_once_with(
|
|
||||||
self.output['solum_api_version'], **self.output)
|
|
||||||
|
|
||||||
|
|
||||||
class FakeCommands(cli_utils.CommandsBase):
|
|
||||||
"""Fake command class."""
|
|
||||||
|
|
||||||
def create(self):
|
|
||||||
"""Fake Create Method."""
|
|
||||||
return
|
|
@@ -1,51 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import exc
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class FakeResponse(object):
|
|
||||||
json_data = {}
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
|
||||||
for key, value in kwargs.items():
|
|
||||||
setattr(self, key, value)
|
|
||||||
|
|
||||||
def json(self):
|
|
||||||
return self.json_data
|
|
||||||
|
|
||||||
|
|
||||||
class ExceptionTest(base.TestCase):
|
|
||||||
|
|
||||||
def test_from_response_with_status_code_404(self):
|
|
||||||
json_data = {"faultstring": "fake message",
|
|
||||||
"debuginfo": "fake details"}
|
|
||||||
method = 'GET'
|
|
||||||
status_code = 404
|
|
||||||
url = 'http://example.com:9777/v1/assemblies/fake-id'
|
|
||||||
ex = exc.from_response(
|
|
||||||
FakeResponse(status_code=status_code,
|
|
||||||
headers={"Content-Type": "application/json"},
|
|
||||||
json_data=json_data),
|
|
||||||
method,
|
|
||||||
url
|
|
||||||
)
|
|
||||||
self.assertIsInstance(ex, exceptions.HttpError)
|
|
||||||
self.assertEqual(json_data["faultstring"], ex.message)
|
|
||||||
self.assertEqual(json_data["debuginfo"], ex.details)
|
|
||||||
self.assertEqual(method, ex.method)
|
|
||||||
self.assertEqual(url, ex.url)
|
|
||||||
self.assertEqual(status_code, ex.http_status)
|
|
@@ -1,164 +0,0 @@
|
|||||||
# Copyright (c) 2015 Rackspace
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
# implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
import json
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
|
|
||||||
from solumclient.common import github
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestGitHubAuth(base.TestCase):
|
|
||||||
fake_repo = "http://github.com/fakeuser/fakerepo.git"
|
|
||||||
fake_trigger = "http://example.com/trigger/1"
|
|
||||||
fake_username = 'fakeuser'
|
|
||||||
fake_password = 'fakepassword'
|
|
||||||
fake_token = 'faketoken'
|
|
||||||
|
|
||||||
def test_invalid_repo(self):
|
|
||||||
self.assertRaises(ValueError,
|
|
||||||
github.GitHubAuth,
|
|
||||||
"http://example.com")
|
|
||||||
|
|
||||||
def test_auth_header_username_password(self):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
username=self.fake_username,
|
|
||||||
password=self.fake_password)
|
|
||||||
# base64.b64encode('fakeuser:fakepassword') yields 'ZmFrZX...'
|
|
||||||
expected_auth_header = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Basic ZmFrZXVzZXI6ZmFrZXBhc3N3b3Jk',
|
|
||||||
}
|
|
||||||
self.assertEqual(expected_auth_header, gha.auth_header)
|
|
||||||
|
|
||||||
@mock.patch('getpass.getpass')
|
|
||||||
def test_auth_header_username_password_2fa(self, fake_getpass):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
username=self.fake_username,
|
|
||||||
password=self.fake_password)
|
|
||||||
gha._otp_required = True
|
|
||||||
fake_getpass.return_value = 'fakeonetime'
|
|
||||||
expected_auth_header = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'Basic ZmFrZXVzZXI6ZmFrZXBhc3N3b3Jk',
|
|
||||||
'x-github-otp': 'fakeonetime',
|
|
||||||
}
|
|
||||||
self.assertEqual(expected_auth_header, gha.auth_header)
|
|
||||||
|
|
||||||
def test_auth_header_repo_token(self):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
repo_token=self.fake_token)
|
|
||||||
expected_auth_header = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': 'token %s' % self.fake_token,
|
|
||||||
}
|
|
||||||
self.assertEqual(expected_auth_header, gha.auth_header)
|
|
||||||
|
|
||||||
@mock.patch('httplib2.Http.request')
|
|
||||||
def test_create_webhook(self, fake_request):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
repo_token=self.fake_token)
|
|
||||||
fake_request.return_value = ({'status': '200'},
|
|
||||||
'{"token": "%s"}' % self.fake_token)
|
|
||||||
gha.create_repo_token = mock.MagicMock()
|
|
||||||
gha.create_repo_token.return_value = 'token123'
|
|
||||||
|
|
||||||
gha.create_webhook(self.fake_trigger)
|
|
||||||
fake_request.assert_called_once_with(
|
|
||||||
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
|
||||||
'POST',
|
|
||||||
headers=mock.ANY,
|
|
||||||
body=mock.ANY)
|
|
||||||
|
|
||||||
expected_body = {
|
|
||||||
"config": {
|
|
||||||
"url": self.fake_trigger,
|
|
||||||
"content_type": "json"},
|
|
||||||
"name": "web",
|
|
||||||
"events": ["pull_request", "commit_comment"]}
|
|
||||||
|
|
||||||
actual_body = json.loads(fake_request.call_args[1]['body'])
|
|
||||||
self.assertEqual(expected_body, actual_body)
|
|
||||||
|
|
||||||
@mock.patch('httplib2.Http.request')
|
|
||||||
def test_create_webhook_unittest_only(self, fake_request):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
username=self.fake_username,
|
|
||||||
password=self.fake_password)
|
|
||||||
fake_request.return_value = ({'status': '200'},
|
|
||||||
'{"token": "foo"}')
|
|
||||||
|
|
||||||
gha.create_repo_token = mock.MagicMock()
|
|
||||||
gha.create_repo_token.return_value = 'token123'
|
|
||||||
|
|
||||||
gha.create_webhook(self.fake_trigger, workflow=['unittest'])
|
|
||||||
fake_request.assert_called_once_with(
|
|
||||||
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
|
||||||
'POST',
|
|
||||||
headers=mock.ANY,
|
|
||||||
body=mock.ANY)
|
|
||||||
expected_body = {
|
|
||||||
"config": {
|
|
||||||
"url": self.fake_trigger + "?workflow=unittest",
|
|
||||||
"content_type": "json"},
|
|
||||||
"name": "web",
|
|
||||||
"events": ["pull_request", "commit_comment"]}
|
|
||||||
actual_body = json.loads(fake_request.call_args[1]['body'])
|
|
||||||
self.assertEqual(expected_body, actual_body)
|
|
||||||
|
|
||||||
@mock.patch('httplib2.Http.request')
|
|
||||||
def test_create_webhook_unittest_build(self, fake_request):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
username=self.fake_username,
|
|
||||||
password=self.fake_password)
|
|
||||||
fake_request.return_value = ({'status': '200'},
|
|
||||||
'{"token": "foo"}')
|
|
||||||
|
|
||||||
gha.create_repo_token = mock.MagicMock()
|
|
||||||
gha.create_repo_token.return_value = 'token123'
|
|
||||||
|
|
||||||
gha.create_webhook(self.fake_trigger, workflow=['unittest', 'build'])
|
|
||||||
fake_request.assert_called_once_with(
|
|
||||||
'https://api.github.com/repos/fakeuser/fakerepo/hooks',
|
|
||||||
'POST',
|
|
||||||
headers=mock.ANY,
|
|
||||||
body=mock.ANY)
|
|
||||||
expected_body = {
|
|
||||||
"config": {
|
|
||||||
"url": self.fake_trigger + "?workflow=unittest+build",
|
|
||||||
"content_type": "json"},
|
|
||||||
"name": "web",
|
|
||||||
"events": ["pull_request", "commit_comment"]}
|
|
||||||
actual_body = json.loads(fake_request.call_args[1]['body'])
|
|
||||||
self.assertEqual(expected_body, actual_body)
|
|
||||||
|
|
||||||
@mock.patch('httplib2.Http.request')
|
|
||||||
def test_add_ssh_key(self, fake_request):
|
|
||||||
gha = github.GitHubAuth(self.fake_repo,
|
|
||||||
username=self.fake_username,
|
|
||||||
password=self.fake_password)
|
|
||||||
fake_request.return_value = ({'status': '200'},
|
|
||||||
'{"token": "foo"}')
|
|
||||||
fake_pub_key = 'foo'
|
|
||||||
gha.add_ssh_key(public_key=fake_pub_key)
|
|
||||||
fake_request.assert_called_once_with(
|
|
||||||
'https://api.github.com/user/keys',
|
|
||||||
'POST',
|
|
||||||
headers=mock.ANY,
|
|
||||||
body=mock.ANY)
|
|
||||||
expected_body = {"key": "foo", "title": "devops@Solum"}
|
|
||||||
actual_body = json.loads(fake_request.call_args[1]['body'])
|
|
||||||
self.assertEqual(expected_body, actual_body)
|
|
@@ -1,48 +0,0 @@
|
|||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
# not use this file except in compliance with the License. You may obtain
|
|
||||||
# a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
||||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
||||||
# License for the specific language governing permissions and limitations
|
|
||||||
# under the License.
|
|
||||||
|
|
||||||
from unittest import mock
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from solumclient.common import yamlutils
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class TestYamlUtils(base.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
super(TestYamlUtils, self).setUp()
|
|
||||||
|
|
||||||
def test_load_yaml(self):
|
|
||||||
yml_dict = yamlutils.load('a: x\nb: y\n')
|
|
||||||
self.assertEqual({'a': 'x', 'b': 'y'}, yml_dict)
|
|
||||||
|
|
||||||
def test_load_empty_yaml(self):
|
|
||||||
self.assertRaises(ValueError, yamlutils.load, '{}')
|
|
||||||
|
|
||||||
def test_load_empty_list(self):
|
|
||||||
yml_dict = yamlutils.load('[]')
|
|
||||||
self.assertEqual([], yml_dict)
|
|
||||||
|
|
||||||
def test_load_invalid_yaml_syntax(self):
|
|
||||||
self.assertRaises(ValueError, yamlutils.load, "}invalid: y'm'l3!")
|
|
||||||
|
|
||||||
def test_load_invalid_yaml_type(self):
|
|
||||||
self.assertRaises(ValueError, yamlutils.load, 'invalid yaml type')
|
|
||||||
|
|
||||||
@mock.patch('solumclient.common.yamlutils.yaml.dump')
|
|
||||||
def test_dump_yaml(self, dump):
|
|
||||||
if hasattr(yaml, 'CSafeDumper'):
|
|
||||||
yaml_dumper = yaml.CSafeDumper
|
|
||||||
else:
|
|
||||||
yaml_dumper = yaml.SafeDumper
|
|
||||||
yamlutils.dump('version: 1')
|
|
||||||
dump.assert_called_with('version: 1', Dumper=yaml_dumper)
|
|
@@ -1,31 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 unittest import mock
|
|
||||||
|
|
||||||
from solumclient import client
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import auth
|
|
||||||
from solumclient.tests import base
|
|
||||||
|
|
||||||
|
|
||||||
class ClientTest(base.TestCase):
|
|
||||||
|
|
||||||
def test_client_unsupported_version(self):
|
|
||||||
self.assertRaises(exceptions.UnsupportedVersion,
|
|
||||||
client.Client, '111.11', **{})
|
|
||||||
|
|
||||||
def test_client(self):
|
|
||||||
with mock.patch.object(auth, 'KeystoneAuthPlugin'):
|
|
||||||
client.Client('1', **{})
|
|
@@ -1,458 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 io import StringIO
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
import collections
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
|
|
||||||
import fixtures
|
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
from stevedore import extension
|
|
||||||
import testtools
|
|
||||||
from testtools import matchers
|
|
||||||
|
|
||||||
from solumclient import client
|
|
||||||
from solumclient.common.apiclient import auth
|
|
||||||
from solumclient.common import yamlutils
|
|
||||||
from solumclient import solum
|
|
||||||
from solumclient.tests import base
|
|
||||||
from solumclient.v1 import component
|
|
||||||
from solumclient.v1 import languagepack
|
|
||||||
from solumclient.v1 import pipeline
|
|
||||||
from solumclient.v1 import plan
|
|
||||||
|
|
||||||
FAKE_ENV = {'OS_USERNAME': 'username',
|
|
||||||
'OS_PASSWORD': 'password',
|
|
||||||
'OS_TENANT_NAME': 'tenant_name',
|
|
||||||
'OS_AUTH_URL': 'http://no.where'}
|
|
||||||
|
|
||||||
languagepack_file_data = (
|
|
||||||
'{"language-pack-type":"Java", "language-pack-name":"Java version 1.4."}')
|
|
||||||
|
|
||||||
|
|
||||||
class MockEntrypoint(object):
|
|
||||||
def __init__(self, name, plugin):
|
|
||||||
self.name = name
|
|
||||||
self.plugin = plugin
|
|
||||||
|
|
||||||
|
|
||||||
class BaseFakePlugin(auth.BaseAuthPlugin):
|
|
||||||
def _do_authenticate(self, http_client):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def token_and_endpoint(self, endpoint_type, service_type):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TestSolum(base.TestCase):
|
|
||||||
"""Test the Solum CLI."""
|
|
||||||
|
|
||||||
re_options = re.DOTALL | re.MULTILINE
|
|
||||||
|
|
||||||
# Patch os.environ to avoid required auth info.
|
|
||||||
def make_env(self, exclude=None):
|
|
||||||
env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude)
|
|
||||||
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
|
|
||||||
|
|
||||||
@mock.patch.object(extension.ExtensionManager, "map")
|
|
||||||
def shell(self, argstr, mock_mgr_map, exit_code=0):
|
|
||||||
class FakePlugin(BaseFakePlugin):
|
|
||||||
def authenticate(self, cls):
|
|
||||||
cls.request(
|
|
||||||
"POST", "http://auth/tokens",
|
|
||||||
json={"fake": "me"}, allow_redirects=True)
|
|
||||||
|
|
||||||
mock_mgr_map.side_effect = (
|
|
||||||
lambda func: func(MockEntrypoint("fake", FakePlugin)))
|
|
||||||
|
|
||||||
orig = sys.stdout
|
|
||||||
try:
|
|
||||||
sys.stdout = StringIO()
|
|
||||||
argv = [__file__, ]
|
|
||||||
argv.extend(argstr.split())
|
|
||||||
self.useFixture(
|
|
||||||
fixtures.MonkeyPatch('sys.argv', argv))
|
|
||||||
|
|
||||||
solum.main()
|
|
||||||
except SystemExit:
|
|
||||||
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
||||||
self.assertEqual(exit_code, exc_value.code)
|
|
||||||
finally:
|
|
||||||
out = sys.stdout.getvalue()
|
|
||||||
sys.stdout.close()
|
|
||||||
sys.stdout = orig
|
|
||||||
|
|
||||||
return out
|
|
||||||
|
|
||||||
@testtools.skip("Skip it when found solution")
|
|
||||||
def test_get_client_debug(self):
|
|
||||||
env = dict((k, v) for k, v in FAKE_ENV.items() if k is not None)
|
|
||||||
test_client = client.get_client('1', **env)
|
|
||||||
self.assertFalse(test_client.http_client.debug)
|
|
||||||
test_client = client.get_client('1', debug=True)
|
|
||||||
self.assertTrue(test_client.http_client.debug)
|
|
||||||
|
|
||||||
@testtools.skip("Skip it when found solution")
|
|
||||||
def test_get_client_insecure(self):
|
|
||||||
env = dict((k, v) for k, v in FAKE_ENV.items() if k is not None)
|
|
||||||
test_client = client.get_client('1', **env)
|
|
||||||
self.assertTrue(test_client.http_client.verify)
|
|
||||||
test_client = client.get_client('1', verify=False)
|
|
||||||
self.assertFalse(test_client.http_client.verify)
|
|
||||||
|
|
||||||
def test_help(self):
|
|
||||||
required = [
|
|
||||||
'.*?^Available commands:'
|
|
||||||
|
|
||||||
]
|
|
||||||
help_text = self.shell('--help')
|
|
||||||
for r in required:
|
|
||||||
self.assertThat(help_text,
|
|
||||||
matchers.MatchesRegex(r,
|
|
||||||
self.re_options))
|
|
||||||
|
|
||||||
# Workflow Tests #
|
|
||||||
def test_workflow_error(self):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("workflow unknown-command")
|
|
||||||
self.assertIn("Available commands", out)
|
|
||||||
|
|
||||||
# Pipeline Tests #
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "list")
|
|
||||||
def test_pipeline_list(self, mock_pipeline_list):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("pipeline list")
|
|
||||||
mock_pipeline_list.assert_called_once_with()
|
|
||||||
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "create")
|
|
||||||
def test_pipeline_create(self, mock_pipeline_create):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("pipeline create http://example.com/a.yaml workbook test")
|
|
||||||
mock_pipeline_create.assert_called_once_with(
|
|
||||||
name='test',
|
|
||||||
workbook_name='workbook',
|
|
||||||
plan_uri='http://example.com/a.yaml')
|
|
||||||
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "create")
|
|
||||||
def test_pipeline_create_without_name(self, mock_pipeline_create):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("pipeline create http://example.com/a.yaml workbook",
|
|
||||||
exit_code=2)
|
|
||||||
|
|
||||||
@mock.patch.object(plan.PlanManager, "find")
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "create")
|
|
||||||
def test_pipeline_create_with_plan_name(self, mock_pipeline_create,
|
|
||||||
mock_plan_find):
|
|
||||||
class FakePlan(object):
|
|
||||||
uri = 'http://example.com/the-plan.yaml'
|
|
||||||
|
|
||||||
self.make_env()
|
|
||||||
mock_plan_find.return_value = FakePlan()
|
|
||||||
self.shell("pipeline create the-plan-name workbook test")
|
|
||||||
mock_plan_find.assert_called_once_with(name_or_id='the-plan-name')
|
|
||||||
mock_pipeline_create.assert_called_once_with(
|
|
||||||
name='test',
|
|
||||||
workbook_name='workbook',
|
|
||||||
plan_uri='http://example.com/the-plan.yaml')
|
|
||||||
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "delete")
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "find")
|
|
||||||
def test_pipeline_delete(self, mock_pipeline_find, mock_pipeline_delete):
|
|
||||||
self.make_env()
|
|
||||||
the_id = uuidutils.generate_uuid()
|
|
||||||
self.shell("pipeline delete %s" % the_id)
|
|
||||||
mock_pipeline_find.assert_called_once_with(
|
|
||||||
name_or_id=the_id)
|
|
||||||
self.assertEqual(1, mock_pipeline_delete.call_count)
|
|
||||||
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "find")
|
|
||||||
def test_pipeline_get(self, mock_pipeline_find):
|
|
||||||
self.make_env()
|
|
||||||
the_id = uuidutils.generate_uuid()
|
|
||||||
self.shell("pipeline show %s" % the_id)
|
|
||||||
mock_pipeline_find.assert_called_once_with(name_or_id=the_id)
|
|
||||||
|
|
||||||
@mock.patch.object(pipeline.PipelineManager, "find")
|
|
||||||
def test_pipeline_get_by_name(self, mock_pipeline_find):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("pipeline show app2")
|
|
||||||
mock_pipeline_find.assert_called_once_with(name_or_id='app2')
|
|
||||||
|
|
||||||
# App Tests #
|
|
||||||
def test_app_create_with_missing_workflow_config(self):
|
|
||||||
raw_data = 'version: 1\nname: ex_app\nworkflow_config:\n'
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("app create --app-file /dev/null")
|
|
||||||
self.assertEqual("ERROR: Workflow config cannot be empty\n", out)
|
|
||||||
|
|
||||||
def test_app_create_with_missing_trigger_actions(self):
|
|
||||||
raw_data = 'version: 1\nname: ex_app\ntrigger_actions:\n'
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("app create --app-file /dev/null")
|
|
||||||
self.assertEqual("ERROR: Trigger actions cannot be empty\n", out)
|
|
||||||
|
|
||||||
def test_app_create_with_bad_name(self):
|
|
||||||
raw_data = '\n'.join([
|
|
||||||
'version: 1',
|
|
||||||
'name: ex=app1',
|
|
||||||
'description: python web app',
|
|
||||||
'workflow_config:',
|
|
||||||
' run_cmd: python app.py',
|
|
||||||
'ports: [5000]'])
|
|
||||||
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("app create --app-file /dev/null")
|
|
||||||
|
|
||||||
self.assertEqual("ERROR: Application name must be 1-100 "
|
|
||||||
"characters long, only contain a-z,0-9,-,_ and "
|
|
||||||
"start with an alphabet character.\n", out)
|
|
||||||
|
|
||||||
# OldApp Tests #
|
|
||||||
def test_oldapp_create_with_missing_artifacts(self):
|
|
||||||
raw_data = 'version: 1\nname: ex_plan1\ndescription: dsc1.'
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("oldapp create --plan-file /dev/null")
|
|
||||||
self.assertEqual("ERROR: Missing artifacts section\n", out)
|
|
||||||
|
|
||||||
def test_oldapp_create_with_bad_name(self):
|
|
||||||
raw_data = '\n'.join([
|
|
||||||
'version: 1',
|
|
||||||
'name: ex=plan1',
|
|
||||||
'description: python web app',
|
|
||||||
'artifacts:',
|
|
||||||
'- name: web',
|
|
||||||
' content:',
|
|
||||||
' href: https://github.com/user/repo.git',
|
|
||||||
' language_pack: auto',
|
|
||||||
' unittest_cmd: ./unit_tests.sh',
|
|
||||||
' run_cmd: python app.py',
|
|
||||||
' ports: 5000'])
|
|
||||||
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("oldapp create --plan-file /dev/null")
|
|
||||||
|
|
||||||
self.assertEqual("ERROR: Application name must be 1-100 "
|
|
||||||
"characters long, only contain a-z,0-9,-,_ and "
|
|
||||||
"start with an alphabet character.\n", out)
|
|
||||||
|
|
||||||
@testtools.skip("Skip it when found solution")
|
|
||||||
def test_oldapp_create_with_bad_artifact_name(self):
|
|
||||||
raw_data = '\n'.join([
|
|
||||||
'version: 1',
|
|
||||||
'name: explan1',
|
|
||||||
'description: python web app',
|
|
||||||
'artifacts:',
|
|
||||||
'- name:',
|
|
||||||
' content:',
|
|
||||||
' href: https://github.com/user/repo.git',
|
|
||||||
' language_pack: auto',
|
|
||||||
' unittest_cmd: ./unit_tests.sh',
|
|
||||||
' run_cmd: python app.py',
|
|
||||||
' ports: 5000'])
|
|
||||||
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("oldapp create --plan-file /dev/null")
|
|
||||||
|
|
||||||
# No part of the plan is in error; the next step in the test
|
|
||||||
# is authorization, which is deliberately mocked.
|
|
||||||
self.assertIn("ERROR: Authorization Failed:", out)
|
|
||||||
self.assertIn("http://no.where/tokens", out)
|
|
||||||
|
|
||||||
def test_oldapp_create_with_artifacts_empty(self):
|
|
||||||
raw_data = 'version: 1\nname: ex_plan1\ndescription: dsc1.\nartifacts:'
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("oldapp create --plan-file /dev/null")
|
|
||||||
self.assertEqual("ERROR: Artifacts cannot be empty\n", out)
|
|
||||||
|
|
||||||
def test_oldapp_create_with_artifacts_no_content(self):
|
|
||||||
raw_data = 'version: 1\nname: ex_plan1\ndescription: d1.\nartifacts:\n'
|
|
||||||
raw_data += '- name: asdfds\n'
|
|
||||||
raw_data += ' artifact_type: heroku\n'
|
|
||||||
raw_data += ' language_pack: lp'
|
|
||||||
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
out = self.shell("oldapp create --plan-file /dev/null")
|
|
||||||
self.assertEqual("ERROR: Artifact content missing\n", out)
|
|
||||||
|
|
||||||
# Plan Tests #
|
|
||||||
@mock.patch.object(plan.PlanManager, "create")
|
|
||||||
def test_plan_create(self, mock_plan_create):
|
|
||||||
FakeResource = collections.namedtuple("FakeResource",
|
|
||||||
"uuid name description uri")
|
|
||||||
|
|
||||||
mock_plan_create.return_value = FakeResource('foo', 'foo', 'foo',
|
|
||||||
'foo')
|
|
||||||
raw_data = 'version: 1\nname: ex_plan1\ndescription: dsc1.'
|
|
||||||
plan_data = yamlutils.dump(yamlutils.load(raw_data))
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("plan create /dev/null")
|
|
||||||
mock_plan_create.assert_called_once_with(plan_data)
|
|
||||||
|
|
||||||
@mock.patch.object(plan.PlanManager, "create")
|
|
||||||
def test_plan_create_with_private_github_repo(self, mock_plan_create):
|
|
||||||
FakeResource = collections.namedtuple(
|
|
||||||
"FakeResource", "uuid name description uri artifacts")
|
|
||||||
|
|
||||||
mock_plan_create.return_value = FakeResource('foo', 'foo', 'foo',
|
|
||||||
'foo', 'artifacts')
|
|
||||||
expected_printed_dict_args = mock_plan_create.return_value._asdict()
|
|
||||||
expected_printed_dict_args.pop('artifacts')
|
|
||||||
raw_data = 'version: 1\nname: ex_plan1\ndescription: dsc1.'
|
|
||||||
plan_data = yamlutils.dump(yamlutils.load(raw_data))
|
|
||||||
mopen = mock.mock_open(read_data=raw_data)
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("plan create /dev/null")
|
|
||||||
mock_plan_create.assert_called_once_with(plan_data)
|
|
||||||
|
|
||||||
@mock.patch.object(plan.PlanManager, "list")
|
|
||||||
def test_plan_list(self, mock_plan_list):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("plan list")
|
|
||||||
mock_plan_list.assert_called_once_with()
|
|
||||||
|
|
||||||
@mock.patch.object(plan.PlanManager, "delete")
|
|
||||||
@mock.patch.object(plan.PlanManager, "find")
|
|
||||||
def test_plan_delete(self, mock_plan_find, mock_plan_delete):
|
|
||||||
self.make_env()
|
|
||||||
the_id = uuidutils.generate_uuid()
|
|
||||||
self.shell("plan delete %s" % the_id)
|
|
||||||
mock_plan_find.assert_called_once_with(name_or_id=the_id)
|
|
||||||
self.assertEqual(1, mock_plan_delete.call_count)
|
|
||||||
|
|
||||||
@mock.patch.object(plan.PlanManager, "find")
|
|
||||||
def test_plan_get(self, mock_plan_find):
|
|
||||||
self.make_env()
|
|
||||||
the_id = uuidutils.generate_uuid()
|
|
||||||
self.shell("plan show %s" % the_id)
|
|
||||||
mock_plan_find.assert_called_once_with(name_or_id=the_id)
|
|
||||||
|
|
||||||
@mock.patch.object(plan.PlanManager, "find")
|
|
||||||
def test_plan_get_private_github_repo(self, mock_plan_find):
|
|
||||||
self.make_env()
|
|
||||||
the_id = uuidutils.generate_uuid()
|
|
||||||
FakeResource = collections.namedtuple(
|
|
||||||
"FakeResource", "uuid name description uri artifacts")
|
|
||||||
mock_plan_find.return_value = FakeResource('foo', 'foo', 'foo', 'foo',
|
|
||||||
'artifacts')
|
|
||||||
self.shell("plan show %s" % the_id)
|
|
||||||
mock_plan_find.assert_called_once_with(name_or_id=the_id)
|
|
||||||
|
|
||||||
# LanguagePack Tests #
|
|
||||||
@mock.patch.object(languagepack.LanguagePackManager, "list")
|
|
||||||
def test_languagepack_list(self, mock_lp_list):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("languagepack list")
|
|
||||||
self.assertEqual(1, mock_lp_list.call_count)
|
|
||||||
|
|
||||||
@mock.patch.object(languagepack.LanguagePackManager, "create")
|
|
||||||
def test_languagepack_create(self, mock_lp_create):
|
|
||||||
FakeResource = collections.namedtuple("FakeResource",
|
|
||||||
"uuid name description "
|
|
||||||
"compiler_versions os_platform")
|
|
||||||
mock_lp_create.return_value = FakeResource(
|
|
||||||
'foo', 'foo', 'foo', 'foo', 'foo')
|
|
||||||
lp_metadata = '{"OS": "Ubuntu"}'
|
|
||||||
mopen = mock.mock_open(read_data=lp_metadata)
|
|
||||||
with mock.patch('%s.open' % solum.__name__, mopen, create=True):
|
|
||||||
self.make_env()
|
|
||||||
|
|
||||||
self.shell("languagepack create lp_name github.com/test "
|
|
||||||
"--lp_metadata=/dev/null")
|
|
||||||
mock_lp_create.assert_called_once_with(
|
|
||||||
name='lp_name',
|
|
||||||
source_uri='github.com/test',
|
|
||||||
lp_metadata=lp_metadata,
|
|
||||||
lp_params={})
|
|
||||||
|
|
||||||
@mock.patch.object(languagepack.LanguagePackManager, "delete")
|
|
||||||
def test_languagepack_delete(self, mock_lp_delete):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("languagepack delete fake-lp-id")
|
|
||||||
mock_lp_delete.assert_called_once_with(lp_id='fake-lp-id')
|
|
||||||
|
|
||||||
@mock.patch.object(languagepack.LanguagePackManager, "find")
|
|
||||||
def test_languagepack_get(self, mock_lp_get):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("languagepack show fake-lp-id1")
|
|
||||||
mock_lp_get.assert_called_once_with(name_or_id='fake-lp-id1')
|
|
||||||
|
|
||||||
# Component Tests #
|
|
||||||
@mock.patch.object(component.ComponentManager, "list")
|
|
||||||
def test_component_list(self, mock_component_list):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("component list")
|
|
||||||
mock_component_list.assert_called_once_with()
|
|
||||||
|
|
||||||
@mock.patch.object(component.ComponentManager, "find")
|
|
||||||
def test_component_get(self, mock_component_find):
|
|
||||||
self.make_env()
|
|
||||||
the_id = uuidutils.generate_uuid()
|
|
||||||
self.shell("component show %s" % the_id)
|
|
||||||
mock_component_find.assert_called_once_with(name_or_id=the_id)
|
|
||||||
|
|
||||||
@mock.patch.object(component.ComponentManager, "find")
|
|
||||||
def test_component_get_by_name(self, mock_component_find):
|
|
||||||
self.make_env()
|
|
||||||
self.shell("component show comp1")
|
|
||||||
mock_component_find.assert_called_once_with(name_or_id='comp1')
|
|
||||||
|
|
||||||
def test_transform_git_url(self):
|
|
||||||
private_uri = 'git@github.com:solum/python.git'
|
|
||||||
public_uri = 'https://github.com/solum/python.git'
|
|
||||||
private = True
|
|
||||||
public = False
|
|
||||||
|
|
||||||
result = solum.transform_git_url(private_uri, private)
|
|
||||||
self.assertEqual(result, private_uri)
|
|
||||||
|
|
||||||
result = solum.transform_git_url(public_uri, private)
|
|
||||||
self.assertEqual(result, private_uri)
|
|
||||||
|
|
||||||
result = solum.transform_git_url(private_uri, public)
|
|
||||||
self.assertEqual(result, public_uri)
|
|
||||||
|
|
||||||
result = solum.transform_git_url(public_uri, public)
|
|
||||||
self.assertEqual(result, public_uri)
|
|
@@ -1,194 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import client
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common.apiclient import fake_client
|
|
||||||
from solumclient.tests import base
|
|
||||||
from solumclient.v1 import component
|
|
||||||
|
|
||||||
|
|
||||||
component_list = [
|
|
||||||
{
|
|
||||||
'uri': 'http://example.com/v1/components/c1',
|
|
||||||
'name': 'php-web-app',
|
|
||||||
'type': 'component',
|
|
||||||
'description': 'A php web application component',
|
|
||||||
'tags': ['web_app'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'assembly_link': {
|
|
||||||
' href': 'http://example.com:9777/v1/assembly/a1',
|
|
||||||
'target_name': 'a1'},
|
|
||||||
'service_links': [{
|
|
||||||
'href': 'http://example.com:9777/v1/services/s1',
|
|
||||||
'target_name': 's1'}],
|
|
||||||
'operations_uri': 'http://example.com:9777/v1/operations/o1',
|
|
||||||
'sensors_uri': 'http://example.com:9777/v1/sensors/s1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'uri': 'http://example.com/v1/components/c2',
|
|
||||||
'name': 'mysql-db',
|
|
||||||
'type': 'component',
|
|
||||||
'description': 'A mysql db component',
|
|
||||||
'tags': ['database'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'assembly_link': {
|
|
||||||
'href': 'http://example.com:9777/v1/assembly/a2',
|
|
||||||
'target_name': 'a2'},
|
|
||||||
'service_links': [{
|
|
||||||
'href': 'http://example.com:9777/v1/services/s2',
|
|
||||||
'target_name': 's2'}],
|
|
||||||
'operations_uri': 'http://example.com:9777/v1/operations/o2',
|
|
||||||
'sensors_uri': 'http://example.com:9777/v1/sensors/s2'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
component_fixture = {
|
|
||||||
'uri': 'http://example.com/v1/components/c1',
|
|
||||||
'name': 'mysql-db',
|
|
||||||
'type': 'component',
|
|
||||||
'description': 'A mysql db component',
|
|
||||||
'tags': ['database'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'assembly_link': {
|
|
||||||
'href': 'http://example.com:9777/v1/assembly/a1',
|
|
||||||
'target_name': 'a1'},
|
|
||||||
'service_links': [{
|
|
||||||
'href': 'http://example.com:9777/v1/services/s1',
|
|
||||||
'target_name': 's1'}],
|
|
||||||
'operations_uri': 'http://example.com:9777/v1/operations/o1',
|
|
||||||
'sensors_uri': 'http://example.com:9777/v1/sensors/o2'
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_list = {
|
|
||||||
'/v1/components': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
component_list
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_get = {
|
|
||||||
'/v1/components/c1': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
component_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_create = {
|
|
||||||
'/v1/components': {
|
|
||||||
'POST': (
|
|
||||||
{},
|
|
||||||
component_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_put = {
|
|
||||||
'/v1/components/c1': {
|
|
||||||
'PUT': (
|
|
||||||
{},
|
|
||||||
component_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentManagerTest(base.TestCase):
|
|
||||||
|
|
||||||
def test_list_all(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
self.mgr = component.ComponentManager(api_client)
|
|
||||||
components = self.mgr.list()
|
|
||||||
self.assertEqual(2, len(components))
|
|
||||||
self.assertIn('Component', repr(components[0]))
|
|
||||||
self.assertEqual('http://example.com/v1/components/c1',
|
|
||||||
components[0].uri)
|
|
||||||
self.assertEqual('http://example.com/v1/components/c2',
|
|
||||||
components[1].uri)
|
|
||||||
|
|
||||||
def test_create(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_create)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = component.ComponentManager(api_client)
|
|
||||||
component_obj = mgr.create()
|
|
||||||
self.assertIn('Component', repr(component_obj))
|
|
||||||
self.assertEqual('http://example.com/v1/components/c1',
|
|
||||||
component_obj.uri)
|
|
||||||
self.assertEqual('component',
|
|
||||||
component_obj.type)
|
|
||||||
self.assertEqual('1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
component_obj.project_id)
|
|
||||||
self.assertEqual('55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
component_obj.user_id)
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_get)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = component.ComponentManager(api_client)
|
|
||||||
component_obj = mgr.get(component_id='c1')
|
|
||||||
self.assertIn('Component', repr(component_obj))
|
|
||||||
self.assertEqual('http://example.com/v1/components/c1',
|
|
||||||
component_obj.uri)
|
|
||||||
self.assertEqual('component',
|
|
||||||
component_obj.type)
|
|
||||||
self.assertEqual('1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
component_obj.project_id)
|
|
||||||
self.assertEqual('55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
component_obj.user_id)
|
|
||||||
|
|
||||||
def test_put(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_put)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = component.ComponentManager(api_client)
|
|
||||||
component_obj = mgr.put(component_id='c1')
|
|
||||||
self.assertIn('Component', repr(component_obj))
|
|
||||||
self.assertEqual('http://example.com/v1/components/c1',
|
|
||||||
component_obj.uri)
|
|
||||||
self.assertEqual('component',
|
|
||||||
component_obj.type)
|
|
||||||
self.assertEqual('1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
component_obj.project_id)
|
|
||||||
self.assertEqual('55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
component_obj.user_id)
|
|
||||||
|
|
||||||
def test_find_one(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = component.ComponentManager(api_client)
|
|
||||||
components = mgr.findall(name='php-web-app')
|
|
||||||
self.assertEqual(1, len(components))
|
|
||||||
self.assertIn('Component', repr(components[0]))
|
|
||||||
self.assertEqual(component_list[0]['uri'], components[0].uri)
|
|
||||||
|
|
||||||
def test_find_one_only(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = component.ComponentManager(api_client)
|
|
||||||
result = mgr.find(name_or_id='php-web-app')
|
|
||||||
self.assertEqual(component_list[0]['uri'], result.uri)
|
|
||||||
|
|
||||||
def test_find_none(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = component.ComponentManager(api_client)
|
|
||||||
self.assertRaises(exceptions.NotFound, mgr.find, name_or_id='test')
|
|
@@ -1,161 +0,0 @@
|
|||||||
# Copyright 2014 - Rackspace
|
|
||||||
#
|
|
||||||
# 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 solumclient.builder.v1 import image
|
|
||||||
from solumclient.common.apiclient import client
|
|
||||||
from solumclient.common.apiclient import fake_client
|
|
||||||
from solumclient.tests import base
|
|
||||||
from solumclient.v1 import languagepack
|
|
||||||
|
|
||||||
languagepack_list = [
|
|
||||||
{
|
|
||||||
'uri': 'http://example.com/v1/language_packs/x1',
|
|
||||||
'name': 'database',
|
|
||||||
'language_pack_type': 'python',
|
|
||||||
'description': 'Python Language pack',
|
|
||||||
'tags': ['python'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'language_implementation': 'python',
|
|
||||||
'compiler_versions': '2.6',
|
|
||||||
'os_platform': 'ubuntu 12.04',
|
|
||||||
'attr_blob': '',
|
|
||||||
'service_id': 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'uri': 'http://example.com/v1/language_packs/x2',
|
|
||||||
'name': 'database',
|
|
||||||
'language_pack_type': 'java',
|
|
||||||
'description': 'Java Language pack',
|
|
||||||
'tags': ['java'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'language_implementation': 'java',
|
|
||||||
'compiler_versions': '7.0',
|
|
||||||
'os_platform': 'ubuntu 12.04',
|
|
||||||
'attr_blob': '',
|
|
||||||
'service_id': 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
languagepack_fixture = {
|
|
||||||
'uri': 'http://example.com/v1/language_packs/x1',
|
|
||||||
'name': 'database',
|
|
||||||
'language_pack_type': 'java',
|
|
||||||
'description': 'Java Language pack',
|
|
||||||
'tags': ['java'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'language_implementation': 'java',
|
|
||||||
'compiler_versions': '7.0',
|
|
||||||
'os_platform': 'ubuntu 12.04',
|
|
||||||
'attr_blob': '',
|
|
||||||
'service_id': 1
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_list = {
|
|
||||||
'/v1/language_packs': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
languagepack_list
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_get = {
|
|
||||||
'/v1/language_packs/x1': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
languagepack_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_create = {
|
|
||||||
'/v1/language_packs': {
|
|
||||||
'POST': (
|
|
||||||
{},
|
|
||||||
languagepack_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
image_fixture = {
|
|
||||||
'name': 'lp1',
|
|
||||||
'source_uri': 'github.com/test',
|
|
||||||
'lp_metadata': 'sample_lp_metadata'
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_build = {
|
|
||||||
'/v1/images': {
|
|
||||||
'POST': (
|
|
||||||
{},
|
|
||||||
image_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LanguagePackManagerTest(base.TestCase):
|
|
||||||
|
|
||||||
def assert_lp_object(self, lp_obj):
|
|
||||||
self.assertIn('LanguagePack', repr(lp_obj))
|
|
||||||
self.assertEqual(languagepack_fixture['uri'], lp_obj.uri)
|
|
||||||
self.assertEqual(languagepack_fixture['language_pack_type'],
|
|
||||||
lp_obj.language_pack_type)
|
|
||||||
self.assertEqual(languagepack_fixture['project_id'], lp_obj.project_id)
|
|
||||||
self.assertEqual(languagepack_fixture['user_id'], lp_obj.user_id)
|
|
||||||
|
|
||||||
def test_list_all(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = languagepack.LanguagePackManager(api_client)
|
|
||||||
languagepacks = mgr.list()
|
|
||||||
self.assertEqual(2, len(languagepacks))
|
|
||||||
self.assertIn('LanguagePack', repr(languagepacks[0]))
|
|
||||||
self.assertEqual(languagepack_list[0]['uri'],
|
|
||||||
languagepacks[0].uri)
|
|
||||||
self.assertEqual(languagepack_list[1]['uri'],
|
|
||||||
languagepacks[1].uri)
|
|
||||||
|
|
||||||
def test_create(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_create)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = languagepack.LanguagePackManager(api_client)
|
|
||||||
languagepack_obj = mgr.create()
|
|
||||||
self.assert_lp_object(languagepack_obj)
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_get)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = languagepack.LanguagePackManager(api_client)
|
|
||||||
languagepack_obj = mgr.get(lp_id='x1')
|
|
||||||
self.assert_lp_object(languagepack_obj)
|
|
||||||
|
|
||||||
def test_build(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_build)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = image.ImageManager(api_client)
|
|
||||||
image_obj = mgr.create(name='lp1',
|
|
||||||
source_uri='github.com/test',
|
|
||||||
lp_metadata='sample_lp_metadata')
|
|
||||||
self.assert_image_object(image_obj)
|
|
||||||
|
|
||||||
def assert_image_object(self, image_obj):
|
|
||||||
self.assertIn('Image', repr(image_obj))
|
|
||||||
self.assertEqual(image_fixture['source_uri'], image_obj.source_uri)
|
|
||||||
self.assertEqual(image_fixture['name'], image_obj.name)
|
|
||||||
self.assertEqual(image_fixture['lp_metadata'], image_obj.lp_metadata)
|
|
@@ -1,183 +0,0 @@
|
|||||||
# Copyright 2014 - Rackspace Hosting
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import client
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common.apiclient import fake_client
|
|
||||||
from solumclient.tests import base
|
|
||||||
from solumclient.v1 import pipeline
|
|
||||||
|
|
||||||
pipeline_list = [
|
|
||||||
{
|
|
||||||
'uri': 'http://example.com/v1/pipelines/x1',
|
|
||||||
'name': 'database',
|
|
||||||
'type': 'pipeline',
|
|
||||||
'description': 'A mysql database',
|
|
||||||
'tags': ['small'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'component_links': [{
|
|
||||||
'href': 'http://example.com:9777/v1/components/x1',
|
|
||||||
'target_name': 'x1'}],
|
|
||||||
'operations_uri': 'http://example.com:9777/v1/operations/o1',
|
|
||||||
'sensors_uri': 'http://example.com:9777/v1/sensors/s1'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'uri': 'http://example.com/v1/pipelines/x2',
|
|
||||||
'name': 'load_balancer',
|
|
||||||
'type': 'pipeline',
|
|
||||||
'description': 'A load balancer',
|
|
||||||
'tags': ['small'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'component_links': [{
|
|
||||||
'href': 'http://example.com:9777/v1/components/x2',
|
|
||||||
'target_name': 'x2'}],
|
|
||||||
'operations_uri': 'http://example.com:9777/v1/operations/o2',
|
|
||||||
'sensors_uri': 'http://example.com:9777/v1/sensors/s2'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
pipeline_fixture = {
|
|
||||||
'uri': 'http://example.com/v1/pipelines/x1',
|
|
||||||
'name': 'database',
|
|
||||||
'type': 'pipeline',
|
|
||||||
'description': 'A mysql database',
|
|
||||||
'tags': ['small'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'component_links': [{
|
|
||||||
'href': 'http://example.com:9777/v1/components/x1',
|
|
||||||
'target_name': 'x1'}],
|
|
||||||
'operations_uri': 'http://example.com:9777/v1/operations/o1',
|
|
||||||
'sensors_uri': 'http://example.com:9777/v1/sensors/s1'
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_list = {
|
|
||||||
'/v1/pipelines': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
pipeline_list
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_get = {
|
|
||||||
'/v1/pipelines/x1': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
pipeline_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_create = {
|
|
||||||
'/v1/pipelines': {
|
|
||||||
'POST': (
|
|
||||||
{},
|
|
||||||
pipeline_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_put = {
|
|
||||||
'/v1/pipelines/x1': {
|
|
||||||
'PUT': (
|
|
||||||
{},
|
|
||||||
pipeline_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_delete = {
|
|
||||||
'/v1/pipelines/x1': {
|
|
||||||
'DELETE': (
|
|
||||||
{},
|
|
||||||
{},
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineManagerTest(base.TestCase):
|
|
||||||
|
|
||||||
def assert_pipeline_object(self, pipeline_obj):
|
|
||||||
self.assertIn('Pipeline', repr(pipeline_obj))
|
|
||||||
self.assertEqual(pipeline_fixture['uri'], pipeline_obj.uri)
|
|
||||||
self.assertEqual(pipeline_fixture['type'], pipeline_obj.type)
|
|
||||||
self.assertEqual(pipeline_fixture['project_id'],
|
|
||||||
pipeline_obj.project_id)
|
|
||||||
self.assertEqual(pipeline_fixture['user_id'], pipeline_obj.user_id)
|
|
||||||
|
|
||||||
def test_list_all(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
pipelines = mgr.list()
|
|
||||||
self.assertEqual(2, len(pipelines))
|
|
||||||
self.assertIn('Pipeline', repr(pipelines[0]))
|
|
||||||
self.assertEqual(pipeline_list[0]['uri'], pipelines[0].uri)
|
|
||||||
self.assertEqual(pipeline_list[1]['uri'], pipelines[1].uri)
|
|
||||||
|
|
||||||
def test_find_one(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
pipelines = mgr.findall(name='database')
|
|
||||||
self.assertEqual(1, len(pipelines))
|
|
||||||
self.assertIn('Pipeline', repr(pipelines[0]))
|
|
||||||
self.assertEqual(pipeline_list[0]['uri'], pipelines[0].uri)
|
|
||||||
|
|
||||||
def test_find_one_only(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
result = mgr.find(name_or_id='database')
|
|
||||||
self.assertEqual(pipeline_list[0]['uri'], result.uri)
|
|
||||||
|
|
||||||
def test_find_none(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
self.assertRaises(exceptions.NotFound, mgr.find, name_or_id='what')
|
|
||||||
|
|
||||||
def test_create(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_create)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
pipeline_obj = mgr.create()
|
|
||||||
self.assert_pipeline_object(pipeline_obj)
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_get)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
pipeline_obj = mgr.get(pipeline_id='x1')
|
|
||||||
self.assert_pipeline_object(pipeline_obj)
|
|
||||||
|
|
||||||
def test_put(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_put)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
pipeline_obj = mgr.put(pipeline_id='x1')
|
|
||||||
self.assert_pipeline_object(pipeline_obj)
|
|
||||||
|
|
||||||
def test_delete(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_delete)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = pipeline.PipelineManager(api_client)
|
|
||||||
mgr.delete(pipeline_id='x1')
|
|
||||||
fake_http_client.assert_called('DELETE', '/v1/pipelines/x1')
|
|
@@ -1,201 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 unittest import mock
|
|
||||||
|
|
||||||
from solumclient.common.apiclient import client
|
|
||||||
from solumclient.common.apiclient import fake_client
|
|
||||||
from solumclient.tests import base
|
|
||||||
from solumclient.v1 import plan
|
|
||||||
|
|
||||||
|
|
||||||
plan_list = [
|
|
||||||
{
|
|
||||||
'name': 'Example plan 1',
|
|
||||||
'artifacts': (
|
|
||||||
[{'name': 'My python app',
|
|
||||||
'artifact_type': 'git_pull',
|
|
||||||
'content': {'href': 'git://example.com/project.git'},
|
|
||||||
'requirements': [{
|
|
||||||
'requirement_type': 'git_pull',
|
|
||||||
'language_pack': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'fulfillment': '1dae5a09ef2b4d8cbf3594b0eb4f6b94'}]}]),
|
|
||||||
'services': [{'name': 'Build Service',
|
|
||||||
'id': 'build',
|
|
||||||
'characteristics': ['python_build_service']}],
|
|
||||||
'description': 'A plan with no services or artifacts shown'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'name': 'Example plan 2',
|
|
||||||
'artifacts': (
|
|
||||||
[{'name': 'My java app',
|
|
||||||
'artifact_type': 'git_pull',
|
|
||||||
'content': {'href': 'git://example.com/project.git'},
|
|
||||||
'requirements': [{
|
|
||||||
'requirement_type': 'git_pull',
|
|
||||||
'language_pack': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'fulfillment': '1dae5a09ef2b4d8cbf3594b0eb4f6b94'}]}]),
|
|
||||||
'services': [{'name': 'Build Service',
|
|
||||||
'id': 'build',
|
|
||||||
'characteristics': ['python_build_service']}],
|
|
||||||
'description': 'A plan with no services or artifacts shown'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
artifacts = [{'name': 'My python app',
|
|
||||||
'artifact_type': 'git_pull',
|
|
||||||
'content': {'href': 'git://example.com/project.git'},
|
|
||||||
'requirements': [{
|
|
||||||
'requirement_type': 'git_pull',
|
|
||||||
'language_pack': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'fulfillment': '1dae5a09ef2b4d8cbf3594b0eb4f6b94'}]}]
|
|
||||||
|
|
||||||
services = [{'name': 'Build Service',
|
|
||||||
'id': 'build',
|
|
||||||
'characteristics': ['python_build_service']}]
|
|
||||||
|
|
||||||
plan_fixture = {
|
|
||||||
'uri': 'http://example.com/v1/plans/p1',
|
|
||||||
'name': 'Example plan',
|
|
||||||
'type': 'plan',
|
|
||||||
'tags': ['small'],
|
|
||||||
'artifacts': artifacts,
|
|
||||||
'services': services,
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'description': 'A plan with no services or artifacts shown'
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_list = {
|
|
||||||
'/v1/plans': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
plan_list
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_list_empty = {
|
|
||||||
'/v1/plans': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
[]
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_get = {
|
|
||||||
'/v1/plans/p1': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
plan_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fixtures_create = {
|
|
||||||
'/v1/plans': {
|
|
||||||
'POST': (
|
|
||||||
{},
|
|
||||||
plan_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fixtures_put = {
|
|
||||||
'/v1/plans/p1': {
|
|
||||||
'PUT': (
|
|
||||||
{},
|
|
||||||
plan_fixture
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PlanManagerTest(base.TestCase):
|
|
||||||
|
|
||||||
def assert_plan_obj(self, plan_obj):
|
|
||||||
self.assertIn('Plan', repr(plan_obj))
|
|
||||||
self.assertIn('Artifact', repr(plan_obj.artifacts[0]))
|
|
||||||
self.assertIn('ServiceReference', repr(plan_obj.services[0]))
|
|
||||||
self.assertEqual(plan_fixture['uri'], plan_obj.uri)
|
|
||||||
self.assertEqual(plan_fixture['type'], plan_obj.type)
|
|
||||||
self.assertEqual(plan_fixture['project_id'], plan_obj.project_id)
|
|
||||||
self.assertEqual(plan_fixture['user_id'], plan_obj.user_id)
|
|
||||||
|
|
||||||
def test_list_all(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_list)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
plan.PlanManager(api_client)
|
|
||||||
# NOTE(stannie): will re-enable this test once
|
|
||||||
# https://bugs.launchpad.net/solum/+bug/1331093 is committed.
|
|
||||||
# FakeHTTPClient doesn't manage YAML properly but since this method
|
|
||||||
# will use the json content-type once implemented in the API, this can
|
|
||||||
# stay temporary disabled.
|
|
||||||
|
|
||||||
def test_list_empty(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(
|
|
||||||
fixtures=fixtures_list_empty)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = plan.PlanManager(api_client)
|
|
||||||
self.assertEqual([], mgr.list())
|
|
||||||
|
|
||||||
def test_create(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_create)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = plan.PlanManager(api_client)
|
|
||||||
plan_obj = mgr.create('version: 1\nname: ex_plan1\ndescription: dsc1.')
|
|
||||||
self.assert_plan_obj(plan_obj)
|
|
||||||
|
|
||||||
def test_plan_create_post_failure(self):
|
|
||||||
api_client = mock.MagicMock()
|
|
||||||
api_client.post.side_effect = Exception("Bad data")
|
|
||||||
try:
|
|
||||||
mgr = plan.PlanManager(api_client)
|
|
||||||
mgr.create('version: 1\nname: ex_plan1\ndescription: dsc1.')
|
|
||||||
except Exception:
|
|
||||||
self.assertTrue(True)
|
|
||||||
|
|
||||||
def test_plan_create_post_success(self):
|
|
||||||
api_client = mock.MagicMock()
|
|
||||||
dummy_data = 'version: 1\nname: ex_plan1\ndescription: dsc1.'
|
|
||||||
response = mock.MagicMock()
|
|
||||||
setattr(response, 'content', dummy_data)
|
|
||||||
api_client.post.return_value = response
|
|
||||||
try:
|
|
||||||
mgr = plan.PlanManager(api_client)
|
|
||||||
plan_obj = mgr.create(dummy_data)
|
|
||||||
assert plan_obj is not None
|
|
||||||
assert plan_obj.name == 'ex_plan1'
|
|
||||||
assert plan_obj.description == 'dsc1.'
|
|
||||||
assert plan_obj.version == 1
|
|
||||||
except Exception:
|
|
||||||
self.assertFalse(True)
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_get)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = plan.PlanManager(api_client)
|
|
||||||
plan_obj = mgr.get(plan_id='p1')
|
|
||||||
self.assert_plan_obj(plan_obj)
|
|
||||||
|
|
||||||
def test_update(self):
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures_put)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
mgr = plan.PlanManager(api_client)
|
|
||||||
plan_obj = mgr.update('version: 1\nname: ex_plan1\ndescription: dsc1.',
|
|
||||||
plan_id='p1')
|
|
||||||
self.assert_plan_obj(plan_obj)
|
|
@@ -1,59 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import client
|
|
||||||
from solumclient.common.apiclient import fake_client
|
|
||||||
from solumclient.tests import base
|
|
||||||
from solumclient.v1 import platform
|
|
||||||
|
|
||||||
|
|
||||||
fixtures = {
|
|
||||||
'/v1': {
|
|
||||||
'GET': (
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
'uri': 'http://example.com/v1',
|
|
||||||
'name': 'solum',
|
|
||||||
'type': 'platform',
|
|
||||||
'tags': ['solid'],
|
|
||||||
'project_id': '1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
'user_id': '55f41cf46df74320b9486a35f5d28a11',
|
|
||||||
'description': 'solum native implementation',
|
|
||||||
'implementation_version': '2014.1.1',
|
|
||||||
'assemblies_uri': 'http://example.com:9777/v1/assemblies',
|
|
||||||
'services_uri': 'http://example.com:9777/v1/services',
|
|
||||||
'components_uri': 'http://example.com:9777/v1/components',
|
|
||||||
'extenstions_uri': 'http://example.com:9777/v1/extenstions'
|
|
||||||
}
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class PlatformManagerTest(base.TestCase):
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
super(PlatformManagerTest, self).setUp()
|
|
||||||
fake_http_client = fake_client.FakeHTTPClient(fixtures=fixtures)
|
|
||||||
api_client = client.BaseClient(fake_http_client)
|
|
||||||
self.mgr = platform.PlatformManager(api_client)
|
|
||||||
|
|
||||||
def test_get(self):
|
|
||||||
platform = self.mgr.get()
|
|
||||||
self.assertIn('Platform', repr(platform))
|
|
||||||
self.assertEqual('http://example.com/v1', platform.uri)
|
|
||||||
self.assertEqual('platform', platform.type)
|
|
||||||
self.assertEqual('1dae5a09ef2b4d8cbf3594b0eb4f6b94',
|
|
||||||
platform.project_id)
|
|
||||||
self.assertEqual('55f41cf46df74320b9486a35f5d28a11', platform.user_id)
|
|
@@ -1,65 +0,0 @@
|
|||||||
# Copyright 2015 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base as apiclient_base
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import base as solum_base
|
|
||||||
from solumclient.common import exc
|
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
|
|
||||||
class App(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<App %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class AppManager(solum_base.CrudManager, solum_base.FindMixin):
|
|
||||||
resource_class = App
|
|
||||||
collection_key = 'apps'
|
|
||||||
key = 'app'
|
|
||||||
|
|
||||||
def list(self, **kwargs):
|
|
||||||
return super(AppManager, self).list(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
return super(AppManager, self).create(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
return super(AppManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def put(self, **kwargs):
|
|
||||||
return super(AppManager, self).put(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def patch(self, **kwargs):
|
|
||||||
return super(AppManager, self).patch(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
|
||||||
return super(AppManager, self).delete(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
if 'app_id' in kwargs:
|
|
||||||
return super(AppManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
elif 'name_or_id' in kwargs:
|
|
||||||
name_or_uuid = kwargs['name_or_id']
|
|
||||||
try:
|
|
||||||
if uuidutils.is_uuid_like(name_or_uuid):
|
|
||||||
return super(AppManager, self).get(
|
|
||||||
base_url="/v1",
|
|
||||||
app_id=name_or_uuid)
|
|
||||||
else:
|
|
||||||
return super(AppManager, self).findone(
|
|
||||||
name=name_or_uuid)
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
raise exc.NotUnique(resource='App')
|
|
@@ -1,43 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient import client as solum_client
|
|
||||||
from solumclient.v1 import app
|
|
||||||
from solumclient.v1 import component
|
|
||||||
from solumclient.v1 import languagepack
|
|
||||||
from solumclient.v1 import pipeline
|
|
||||||
from solumclient.v1 import plan
|
|
||||||
from solumclient.v1 import platform
|
|
||||||
from solumclient.v1 import workflow
|
|
||||||
|
|
||||||
|
|
||||||
class Client(object):
|
|
||||||
"""Client for the Solum v1 API."""
|
|
||||||
|
|
||||||
service_type = "application_deployment"
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""Initialize a new client for the Solum v1 API."""
|
|
||||||
if not kwargs.get('auth_plugin'):
|
|
||||||
kwargs['auth_plugin'] = solum_client.get_auth_plugin(**kwargs)
|
|
||||||
self.auth_plugin = kwargs.get('auth_plugin')
|
|
||||||
|
|
||||||
self.http_client = solum_client.construct_http_client(**kwargs)
|
|
||||||
self.apps = app.AppManager(self.http_client)
|
|
||||||
self.components = component.ComponentManager(self.http_client)
|
|
||||||
self.pipelines = pipeline.PipelineManager(self.http_client)
|
|
||||||
self.platform = platform.PlatformManager(self.http_client)
|
|
||||||
self.plans = plan.PlanManager(self.http_client)
|
|
||||||
self.languagepacks = languagepack.LanguagePackManager(self.http_client)
|
|
||||||
self.workflows = workflow.WorkflowManager(self.http_client)
|
|
@@ -1,59 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base as apiclient_base
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import base as solum_base
|
|
||||||
from solumclient.common import exc
|
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
|
|
||||||
class Component(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Component %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class ComponentManager(solum_base.CrudManager, solum_base.FindMixin):
|
|
||||||
resource_class = Component
|
|
||||||
collection_key = 'components'
|
|
||||||
key = 'component'
|
|
||||||
|
|
||||||
def list(self, **kwargs):
|
|
||||||
return super(ComponentManager, self).list(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
return super(ComponentManager, self).create(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
return super(ComponentManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def put(self, **kwargs):
|
|
||||||
return super(ComponentManager, self).put(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
if 'component_id' in kwargs:
|
|
||||||
return super(ComponentManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
elif 'name_or_id' in kwargs:
|
|
||||||
name_or_uuid = kwargs['name_or_id']
|
|
||||||
try:
|
|
||||||
if uuidutils.is_uuid_like(name_or_uuid):
|
|
||||||
return super(ComponentManager, self).get(
|
|
||||||
base_url="/v1",
|
|
||||||
component_id=name_or_uuid)
|
|
||||||
else:
|
|
||||||
return super(ComponentManager, self).findone(
|
|
||||||
name=name_or_uuid)
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
raise exc.NotUnique(resource='Component')
|
|
@@ -1,60 +0,0 @@
|
|||||||
# Copyright 2014 - Rackspace
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base as apiclient_base
|
|
||||||
from solumclient.common import base as solum_base
|
|
||||||
|
|
||||||
|
|
||||||
class LanguagePack(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<LanguagePack %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class UserLog(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Log %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class LanguagePackManager(solum_base.CrudManager):
|
|
||||||
resource_class = LanguagePack
|
|
||||||
collection_key = 'language_packs'
|
|
||||||
key = 'lp'
|
|
||||||
|
|
||||||
def list(self, **kwargs):
|
|
||||||
return super(LanguagePackManager, self).list(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
return super(LanguagePackManager,
|
|
||||||
self).create(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
return super(LanguagePackManager,
|
|
||||||
self).get(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
|
||||||
return super(LanguagePackManager,
|
|
||||||
self).delete(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
name_or_uuid = kwargs['name_or_id']
|
|
||||||
return super(LanguagePackManager, self).get(base_url="/v1",
|
|
||||||
lp_id=name_or_uuid)
|
|
||||||
|
|
||||||
def logs(self, **kwargs):
|
|
||||||
self.resource_class = UserLog
|
|
||||||
languagepack = self.find(name_or_id=kwargs['lp_id'])
|
|
||||||
kwargs['lp_id'] = languagepack.uuid
|
|
||||||
url = self.build_url(base_url="/v1", **kwargs)
|
|
||||||
url += '/logs/'
|
|
||||||
return self._list(url)
|
|
@@ -1,62 +0,0 @@
|
|||||||
# Copyright 2014 - Rackspace Hosting
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base as apiclient_base
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import base as solum_base
|
|
||||||
from solumclient.common import exc
|
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
|
|
||||||
class Pipeline(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Pipeline %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class PipelineManager(solum_base.CrudManager, solum_base.FindMixin):
|
|
||||||
resource_class = Pipeline
|
|
||||||
collection_key = 'pipelines'
|
|
||||||
key = 'pipeline'
|
|
||||||
|
|
||||||
def list(self, **kwargs):
|
|
||||||
return super(PipelineManager, self).list(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
return super(PipelineManager, self).create(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
return super(PipelineManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def put(self, **kwargs):
|
|
||||||
return super(PipelineManager, self).put(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
|
||||||
return super(PipelineManager, self).delete(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
if 'pipeline_id' in kwargs:
|
|
||||||
return super(PipelineManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
elif 'name_or_id' in kwargs:
|
|
||||||
name_or_uuid = kwargs['name_or_id']
|
|
||||||
try:
|
|
||||||
if uuidutils.is_uuid_like(name_or_uuid):
|
|
||||||
return super(PipelineManager, self).get(
|
|
||||||
base_url="/v1",
|
|
||||||
pipeline_id=name_or_uuid)
|
|
||||||
else:
|
|
||||||
return super(PipelineManager, self).findone(
|
|
||||||
name=name_or_uuid)
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
raise exc.NotUnique(resource='Pipeline')
|
|
@@ -1,160 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base as apiclient_base
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import base as solum_base
|
|
||||||
from solumclient.common import exc
|
|
||||||
from solumclient.common import yamlutils
|
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
|
|
||||||
class Requirement(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Requirement %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class ServiceReference(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<ServiceReference %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class Artifact(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Artifact %s>" % self._info
|
|
||||||
|
|
||||||
def _add_requirements_details(self, req_list):
|
|
||||||
return [Requirement(None, res, loaded=True)
|
|
||||||
for res in req_list if req_list]
|
|
||||||
|
|
||||||
def _add_details(self, info):
|
|
||||||
for (k, v) in info.items():
|
|
||||||
try:
|
|
||||||
if k == 'requirements':
|
|
||||||
v = self._add_requirements_details(v)
|
|
||||||
setattr(self, k, v)
|
|
||||||
self._info[k] = v
|
|
||||||
except AttributeError:
|
|
||||||
# In this case we already defined the attribute on the class
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Plan(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Plan %s>" % self._info
|
|
||||||
|
|
||||||
def _add_artifact_details(self, artf_list):
|
|
||||||
return [Artifact(None, res, loaded=True)
|
|
||||||
for res in artf_list if artf_list]
|
|
||||||
|
|
||||||
def _add_services_details(self, serv_list):
|
|
||||||
return [ServiceReference(None, res, loaded=True)
|
|
||||||
for res in serv_list if serv_list]
|
|
||||||
|
|
||||||
def _add_details(self, info):
|
|
||||||
for (k, v) in info.items():
|
|
||||||
try:
|
|
||||||
if k == 'artifacts':
|
|
||||||
v = self._add_artifact_details(v)
|
|
||||||
elif k == 'services':
|
|
||||||
v = self._add_services_details(v)
|
|
||||||
setattr(self, k, v)
|
|
||||||
self._info[k] = v
|
|
||||||
except AttributeError:
|
|
||||||
# In this case we already defined the attribute on the class
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PlanManager(solum_base.CrudManager, solum_base.FindMixin):
|
|
||||||
resource_class = Plan
|
|
||||||
collection_key = 'plans'
|
|
||||||
key = 'plan'
|
|
||||||
|
|
||||||
def list(self, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
|
||||||
kwargs['headers']['Content-Type'] = 'x-application/yaml'
|
|
||||||
resp = self.client.get(
|
|
||||||
self.build_url(base_url="/v1", **kwargs), **kwargs)
|
|
||||||
try:
|
|
||||||
resp_plan = yamlutils.load(resp.content)
|
|
||||||
except ValueError as e:
|
|
||||||
raise exc.CommandException(message='Could not load Plan. '
|
|
||||||
'Reason: %s' % e.message)
|
|
||||||
return [Plan(self, res, loaded=True) for res in resp_plan if res]
|
|
||||||
|
|
||||||
def create(self, plan, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
kwargs['data'] = plan
|
|
||||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
|
||||||
kwargs['headers']['Content-Type'] = 'x-application/yaml'
|
|
||||||
try:
|
|
||||||
resp = self.client.post(
|
|
||||||
self.build_url(base_url="/v1", **kwargs), **kwargs)
|
|
||||||
except Exception as e:
|
|
||||||
message = vars(e).get('details', str(e))
|
|
||||||
raise exceptions.BadRequest(message=message)
|
|
||||||
try:
|
|
||||||
resp_plan = yamlutils.load(resp.content)
|
|
||||||
except ValueError as e:
|
|
||||||
raise exc.CommandException(message='Could not load Plan. '
|
|
||||||
'Reason: %s' % e.message)
|
|
||||||
return Plan(self, resp_plan)
|
|
||||||
|
|
||||||
def _get(self, url, response_key=None):
|
|
||||||
kwargs = {'headers': {}}
|
|
||||||
kwargs['headers']['Content-Type'] = 'x-application/yaml'
|
|
||||||
resp = self.client.get(url, **kwargs)
|
|
||||||
try:
|
|
||||||
resp_plan = yamlutils.load(resp.content)
|
|
||||||
except ValueError as e:
|
|
||||||
raise exc.CommandException(message='Could not load Plan. '
|
|
||||||
'Reason: %s' % e.message)
|
|
||||||
|
|
||||||
return Plan(self, resp_plan, loaded=True)
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
return super(PlanManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
if 'plan_id' in kwargs:
|
|
||||||
return super(PlanManager, self).get(base_url="/v1", **kwargs)
|
|
||||||
elif 'name_or_id' in kwargs:
|
|
||||||
name_or_uuid = kwargs['name_or_id']
|
|
||||||
try:
|
|
||||||
if uuidutils.is_uuid_like(name_or_uuid):
|
|
||||||
return super(PlanManager, self).get(base_url="/v1",
|
|
||||||
plan_id=name_or_uuid)
|
|
||||||
else:
|
|
||||||
return super(PlanManager, self).findone(name=name_or_uuid)
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
raise exc.NotUnique(resource='Plan')
|
|
||||||
|
|
||||||
def update(self, plan, **kwargs):
|
|
||||||
kwargs = self._filter_kwargs(kwargs)
|
|
||||||
kwargs['data'] = plan
|
|
||||||
kwargs.setdefault("headers", kwargs.get("headers", {}))
|
|
||||||
kwargs['headers']['Content-Type'] = 'x-application/yaml'
|
|
||||||
resp = self.client.put(self.build_url(base_url="/v1", **kwargs),
|
|
||||||
**kwargs)
|
|
||||||
try:
|
|
||||||
resp_plan = yamlutils.load(resp.content)
|
|
||||||
except ValueError as e:
|
|
||||||
raise exc.CommandException(message='Could not load Plan. '
|
|
||||||
'Reason: %s' % e.message)
|
|
||||||
return self.resource_class(self, resp_plan)
|
|
||||||
|
|
||||||
def delete(self, **kwargs):
|
|
||||||
return super(PlanManager, self).delete(base_url="/v1", **kwargs)
|
|
@@ -1,27 +0,0 @@
|
|||||||
# Copyright 2013 - Noorul Islam K M
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base
|
|
||||||
|
|
||||||
|
|
||||||
class Platform(base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Platform %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class PlatformManager(base.BaseManager):
|
|
||||||
resource_class = Platform
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
return self._get('/v1')
|
|
@@ -1,91 +0,0 @@
|
|||||||
# Copyright 2015 - Rackspace
|
|
||||||
#
|
|
||||||
# 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 solumclient.common.apiclient import base as apiclient_base
|
|
||||||
from solumclient.common.apiclient import exceptions
|
|
||||||
from solumclient.common import base as solum_base
|
|
||||||
from solumclient.common import exc
|
|
||||||
|
|
||||||
from oslo_utils import uuidutils
|
|
||||||
|
|
||||||
|
|
||||||
class Workflow(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Workflow %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class UserLog(apiclient_base.Resource):
|
|
||||||
def __repr__(self):
|
|
||||||
return "<Log %s>" % self._info
|
|
||||||
|
|
||||||
|
|
||||||
class WorkflowManager(solum_base.CrudManager, solum_base.FindMixin):
|
|
||||||
resource_class = Workflow
|
|
||||||
collection_key = 'workflows'
|
|
||||||
key = 'workflow'
|
|
||||||
|
|
||||||
def list(self, **kwargs):
|
|
||||||
self.app_id = kwargs.pop('app_id')
|
|
||||||
self.base_url = '/v1/apps/%s' % self.app_id
|
|
||||||
return (super(WorkflowManager, self).list(
|
|
||||||
base_url=self.base_url, **kwargs))
|
|
||||||
|
|
||||||
def create(self, **kwargs):
|
|
||||||
self.app_id = kwargs.get('app_id')
|
|
||||||
self.base_url = '/v1/apps/%s' % self.app_id
|
|
||||||
return (super(WorkflowManager, self).create(
|
|
||||||
base_url=self.base_url, **kwargs))
|
|
||||||
|
|
||||||
def get(self, **kwargs):
|
|
||||||
self.app_id = kwargs.pop('app_id')
|
|
||||||
self.base_url = '/v1/apps/%s' % self.app_id
|
|
||||||
return (super(WorkflowManager, self).get(
|
|
||||||
base_url=self.base_url, **kwargs))
|
|
||||||
|
|
||||||
def logs(self, **kwargs):
|
|
||||||
self.app_id = kwargs.get('app_id')
|
|
||||||
self.base_url = '/v1/apps/%s' % self.app_id
|
|
||||||
self.resource_class = UserLog
|
|
||||||
url = self.build_url(self.base_url, **kwargs)
|
|
||||||
rev_or_uuid = kwargs['revision_or_id']
|
|
||||||
try:
|
|
||||||
if uuidutils.is_uuid_like(rev_or_uuid):
|
|
||||||
workflow_id = rev_or_uuid
|
|
||||||
else:
|
|
||||||
wf = self.find(**kwargs)
|
|
||||||
workflow_id = wf.id
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
raise exc.NotUnique(resource='Workflow')
|
|
||||||
|
|
||||||
url += '/%s/logs/' % workflow_id
|
|
||||||
return self._list(url)
|
|
||||||
|
|
||||||
def find(self, **kwargs):
|
|
||||||
self.app_id = kwargs.get('app_id')
|
|
||||||
self.base_url = '/v1/apps/%s' % self.app_id
|
|
||||||
if 'workflow_id' in kwargs:
|
|
||||||
return (super(WorkflowManager, self).get(
|
|
||||||
base_url=self.base_url, **kwargs))
|
|
||||||
elif 'revision_or_id' in kwargs:
|
|
||||||
rev_or_uuid = kwargs['revision_or_id']
|
|
||||||
try:
|
|
||||||
if uuidutils.is_uuid_like(rev_or_uuid):
|
|
||||||
return super(WorkflowManager, self).get(
|
|
||||||
base_url=self.base_url,
|
|
||||||
workflow_id=rev_or_uuid)
|
|
||||||
else:
|
|
||||||
return super(WorkflowManager, self).findone(
|
|
||||||
app_id=self.app_id, wf_id=rev_or_uuid)
|
|
||||||
except exceptions.NoUniqueMatch:
|
|
||||||
raise exc.NotUnique(resource='Workflow')
|
|
@@ -1,10 +0,0 @@
|
|||||||
# The order of packages is significant, because pip processes them in the order
|
|
||||||
# of appearance. Changing the order has an impact on the overall integration
|
|
||||||
# process, which may cause wedges in the gate later.
|
|
||||||
hacking>=3.1.0,<3.2.0 # Apache-2.0
|
|
||||||
coverage!=4.4,>=4.0 # Apache-2.0
|
|
||||||
fixtures>=3.0.0 # Apache-2.0/BSD
|
|
||||||
oslotest>=4.4.1 # Apache-2.0
|
|
||||||
stestr>=2.0.0 # Apache-2.0
|
|
||||||
testscenarios>=0.4 # Apache-2.0/BSD
|
|
||||||
testtools>=2.2.0 # MIT
|
|
53
tox.ini
53
tox.ini
@@ -1,53 +0,0 @@
|
|||||||
[tox]
|
|
||||||
minversion = 3.2.0
|
|
||||||
envlist = py38,pep8
|
|
||||||
skipsdist = True
|
|
||||||
ignore_basepython_conflict = True
|
|
||||||
|
|
||||||
[testenv]
|
|
||||||
basepython = python3
|
|
||||||
usedevelop = True
|
|
||||||
deps =
|
|
||||||
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
|
|
||||||
-r{toxinidir}/requirements.txt
|
|
||||||
-r{toxinidir}/test-requirements.txt
|
|
||||||
allowlist_externals = find
|
|
||||||
setenv = VIRTUAL_ENV={envdir}
|
|
||||||
commands =
|
|
||||||
find . -type f -name "*.py[c|o]" -delete
|
|
||||||
find . -type d -name "__pycache__" -delete
|
|
||||||
stestr run {posargs}
|
|
||||||
|
|
||||||
[testenv:pep8]
|
|
||||||
commands = flake8
|
|
||||||
|
|
||||||
[testenv:venv]
|
|
||||||
commands = {posargs}
|
|
||||||
|
|
||||||
[testenv:cover]
|
|
||||||
setenv =
|
|
||||||
{[testenv]setenv}
|
|
||||||
PYTHON=coverage run --source solumclient --parallel-mode
|
|
||||||
commands =
|
|
||||||
stestr run {posargs}
|
|
||||||
coverage combine
|
|
||||||
coverage html -d cover
|
|
||||||
coverage xml -o cover/coverage.xml
|
|
||||||
|
|
||||||
[testenv:debug]
|
|
||||||
commands = oslo_debug_helper -t solumclient/tests {posargs}
|
|
||||||
|
|
||||||
[testenv:docs]
|
|
||||||
deps =
|
|
||||||
-c{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}
|
|
||||||
-r{toxinidir}/requirements.txt
|
|
||||||
-r{toxinidir}/doc/requirements.txt
|
|
||||||
commands = sphinx-build -b html doc/source doc/build/html
|
|
||||||
|
|
||||||
[flake8]
|
|
||||||
show-source = True
|
|
||||||
builtins = _
|
|
||||||
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build
|
|
||||||
|
|
||||||
[hacking]
|
|
||||||
import_exceptions = solumclient.i18n
|
|
Reference in New Issue
Block a user