diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b411aa1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +__pycache__ +dist +build +.venv +tests/scenario/.vagrant +.idea +.autogenerated +.coverage +cover/ +coverage.xml +*.sw? +.tox +*.egg +*.egg-info +*.py[co] +.DS_Store +*.log +.testrepository +subunit.log +.eggs +AUTHORS +ChangeLog + +# Django files that get created during the test runs +.secret_key_store +*.lock + +# Coverage data +.coverage.* diff --git a/__init__.py b/.gitreview similarity index 100% rename from __init__.py rename to .gitreview diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..e88a201 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,333 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=no + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + +# DEPRECATED +include-ids=no + +# DEPRECATED +symbols=no + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=W,C,R + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=no + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus extisting member attributes cannot be deduced by static analysis +ignored-modules=distutils + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent,BackupJob.time_stamp,BackupJob.start_time + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input,file + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Include a hint for the correct naming format with invalid-name +include-naming-hint=no + +# Regular expression matching correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for function names +function-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for variable names +variable-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct constant names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Naming hint for constant names +const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression matching correct attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for attribute names +attr-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for argument names +argument-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression matching correct class attribute names +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Naming hint for class attribute names +class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression matching correct inline iteration names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Naming hint for inline iteration names +inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$ + +# Regular expression matching correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Naming hint for class names +class-name-hint=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression matching correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Naming hint for module names +module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression matching correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Naming hint for method names +method-name-hint=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the name of dummy variables (i.e. expectedly +# not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[LOGGING] + +# Logging modules to check that the string format arguments are in logging +# function parameter format +logging-modules=logging + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..731601c --- /dev/null +++ b/.testr.conf @@ -0,0 +1,9 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_LOG_CAPTURE=${OS_LOG_CAPTURE:-1} \ + ${PYTHON:-python} -m subunit.run discover -s ${OS_TEST_PATH:-./freezerclient/tests/unit} -t . $LISTOPT $IDOPTION + +test_id_option=--load-list $IDFILE +test_list_option=--list +group_regex=([^\.]+\.)+ diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..2e7c504 --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,17 @@ +If you would like to contribute to the development of OpenStack, you must +follow the steps in this page: + + http://docs.openstack.org/infra/manual/developers.html + +If you already have a good understanding of how the system works and your +OpenStack accounts are set up, you can skip to the development workflow +section of this documentation to learn how changes to OpenStack should be +submitted for review via the Gerrit tool: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/freezer diff --git a/HACKING.rst b/HACKING.rst new file mode 100644 index 0000000..b1c90df --- /dev/null +++ b/HACKING.rst @@ -0,0 +1,4 @@ +python-freezerclient Style Commandments +=============================================== + +Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..67db858 --- /dev/null +++ b/LICENSE @@ -0,0 +1,175 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c978a52 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,6 @@ +include AUTHORS +include ChangeLog +exclude .gitignore +exclude .gitreview + +global-exclude *.pyc diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..01ce35f --- /dev/null +++ b/README.rst @@ -0,0 +1,25 @@ +=================================================== +Python bindings and CLI for OpenStack's Freezer API +=================================================== + +This is a client library for Freezer built on the OpenStack Disaster Recovery API. It provides a Python API (the freezerclient module) and a command-line tool (freezer). This library fully supports the v1 Disaster Recovery API. + +Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. The master repository is in `Git `_. + + +* License: Apache License, Version 2.0 +* `PyPi`_ - package installation +* `Online Documentation`_ +* `Launchpad project`_ - release management +* `Blueprints`_ - feature specifications +* `Bugs`_ - issue tracking +* `Source`_ +* `How to Contribute`_ + +.. _PyPi: https://pypi.python.org/pypi/python-freezerclient +.. _Online Documentation: https://wiki.openstack.org/wiki/Python-freezerclient +.. _Launchpad project: https://launchpad.net/python-freezerclient +.. _Blueprints: https://blueprints.launchpad.net/python-freezerclient +.. _Bugs: https://bugs.launchpad.net/python-freezerclient +.. _Source: https://git.openstack.org/cgit/openstack/python-freezerclient +.. _How to Contribute: http://docs.openstack.org/infra/manual/developers.html diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..efceab8 --- /dev/null +++ b/babel.cfg @@ -0,0 +1 @@ +[python: **.py] diff --git a/client.py b/client.py deleted file mode 100644 index f10644f..0000000 --- a/client.py +++ /dev/null @@ -1,261 +0,0 @@ -""" -Copyright 2015 Hewlett-Packard - -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. - -client interface to the Freezer API -""" - -import os -import socket - -from keystoneclient.auth.identity import v2 -from keystoneclient.auth.identity import v3 -from keystoneclient import session as ksc_session -from oslo_config import cfg - -from freezer.apiclient import actions -from freezer.apiclient import backups -from freezer.apiclient import jobs -from freezer.apiclient import registration -from freezer.apiclient import sessions -from freezer.utils import Namespace - -CONF = cfg.CONF - - -FREEZER_SERVICE_TYPE = 'backup' - - -def env(*vars, **kwargs): - for v in vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - -class cached_property(object): - - def __init__(self, func): - self.__doc__ = getattr(func, '__doc__') - self.func = func - - def __get__(self, obj, cls): - if obj is None: - return self - value = obj.__dict__[self.func.__name__] = self.func(obj) - return value - - -def build_os_options(): - osclient_opts = [ - cfg.StrOpt('os-username', - default=env('OS_USERNAME'), - help='Name used for authentication with the OpenStack ' - 'Identity service. Defaults to env[OS_USERNAME].', - dest='os_username'), - cfg.StrOpt('os-password', - default=env('OS_PASSWORD'), - help='Password used for authentication with the OpenStack ' - 'Identity service. Defaults to env[OS_PASSWORD].', - dest='os_password'), - cfg.StrOpt('os-project-name', - default=env('OS_PROJECT_NAME'), - help='Project name to scope to. Defaults to ' - 'env[OS_PROJECT_NAME].', - dest='os_project_name'), - cfg.StrOpt('os-project-domain-name', - default=env('OS_PROJECT_DOMAIN_NAME'), - help='Domain name containing project. Defaults to ' - 'env[OS_PROJECT_DOMAIN_NAME].', - dest='os_project_domain_name'), - cfg.StrOpt('os-user-domain-name', - default=env('OS_USER_DOMAIN_NAME'), - help='User\'s domain name. Defaults to ' - 'env[OS_USER_DOMAIN_NAME].', - dest='os_user_domain_name'), - cfg.StrOpt('os-tenant-name', - default=env('OS_TENANT_NAME'), - help='Tenant to request authorization on. Defaults to ' - 'env[OS_TENANT_NAME].', - dest='os_tenant_name'), - cfg.StrOpt('os-tenant-id', - default=env('OS_TENANT_ID'), - help='Tenant to request authorization on. Defaults to ' - 'env[OS_TENANT_ID].', - dest='os_tenant_id'), - cfg.StrOpt('os-auth-url', - default=env('OS_AUTH_URL'), - help='Specify the Identity endpoint to use for ' - 'authentication. Defaults to env[OS_AUTH_URL].', - dest='os_auth_url'), - cfg.StrOpt('os-backup-url', - default=env('OS_BACKUP_URL'), - help='Specify the Freezer backup service endpoint to use. ' - 'Defaults to env[OS_BACKUP_URL].', - dest='os_backup_url'), - cfg.StrOpt('os-region-name', - default=env('OS_REGION_NAME'), - help='Specify the region to use. Defaults to ' - 'env[OS_REGION_NAME].', - dest='os_region_name'), - cfg.StrOpt('os-token', - default=env('OS_TOKEN'), - help='Specify an existing token to use instead of retrieving' - ' one via authentication (e.g. with username & ' - 'password). Defaults to env[OS_TOKEN].', - dest='os_token'), - cfg.StrOpt('os-identity-api-version', - default=env('OS_IDENTITY_API_VERSION'), - help='Identity API version: 2.0 or 3. ' - 'Defaults to env[OS_IDENTITY_API_VERSION]', - dest='os_identity_api_version'), - cfg.StrOpt('os-endpoint-type', - choices=['public', 'publicURL', 'internal', 'internalURL', - 'admin', 'adminURL'], - default=env('OS_ENDPOINT_TYPE') or 'public', - help='Endpoint type to select. Valid endpoint types: ' - '"public" or "publicURL", "internal" or "internalURL",' - ' "admin" or "adminURL". Defaults to ' - 'env[OS_ENDPOINT_TYPE] or "public"', - dest='os_endpoint_type'), - - ] - - return osclient_opts - - -def guess_auth_version(opts): - if opts.os_identity_api_version == '3': - return '3' - elif opts.os_identity_api_version == '2.0': - return '2.0' - elif opts.os_auth_url.endswith('v3'): - return '3' - elif opts.os_auth_url.endswith('v2.0'): - return '2.0' - raise Exception('Please provide valid keystone auth url with valid' - ' keystone api version to use') - - -def get_auth_plugin(opts): - auth_version = guess_auth_version(opts) - if opts.os_username: - if auth_version == '3': - return v3.Password(auth_url=opts.os_auth_url, - username=opts.os_username, - password=opts.os_password, - project_name=opts.os_project_name, - user_domain_name=opts.os_user_domain_name, - project_domain_name=opts.os_project_domain_name) - elif auth_version == '2.0': - return v2.Password(auth_url=opts.os_auth_url, - username=opts.os_username, - password=opts.os_password, - tenant_name=opts.os_tenant_name) - elif opts.os_token: - if auth_version == '3': - return v3.Token(auth_url=opts.os_auth_url, - token=opts.os_token, - project_name=opts.os_project_name, - project_domain_name=opts.os_project_domain_name) - elif auth_version == '2.0': - return v2.Token(auth_url=opts.os_auth_url, - token=opts.os_token, - tenant_name=opts.os_tenant_name) - raise Exception('Unable to determine correct auth method, please provide' - ' either username or token') - - -class Client(object): - def __init__(self, - version='1', - token=None, - username=None, - password=None, - tenant_name=None, - auth_url=None, - session=None, - endpoint=None, - opts=None, - project_name=None, - user_domain_name=None, - project_domain_name=None, - verify=True, - cacert=False): - - self.opts = opts - # this creates a namespace for self.opts when the client is - # created from other method rather than command line arguments. - if self.opts is None: - self.opts = Namespace({}) - if token: - self.opts.os_token = token - if username: - self.opts.os_username = username - if password: - self.opts.os_password = password - if tenant_name: - self.opts.os_tenant_name = tenant_name - if auth_url: - self.opts.os_auth_url = auth_url - if endpoint: - self.opts.os_backup_url = endpoint - if project_name: - self.opts.os_project_name = project_name - if user_domain_name: - self.opts.os_user_domain_name = user_domain_name - if project_domain_name: - self.opts.os_project_domain_name = project_domain_name - - # flag to initialize freezer-scheduler with insecure mode - self.verify = verify - - self._session = session - self.version = version - - self.backups = backups.BackupsManager(self, verify=verify) - self.registration = registration.RegistrationManager(self, verify=verify) - self.jobs = jobs.JobManager(self, verify=verify) - self.actions = actions.ActionManager(self, verify=verify) - self.sessions = sessions.SessionManager(self, verify=verify) - - - @cached_property - def session(self): - if self._session: - return self._session - auth_plugin = get_auth_plugin(self.opts) - return ksc_session.Session(auth=auth_plugin, verify=self.verify) - - @cached_property - def endpoint(self): - if self.opts.os_backup_url: - return self.opts.os_backup_url - else: - auth_ref = self.session.auth.get_auth_ref(self.session) - endpoint = auth_ref.service_catalog.url_for( - service_type=FREEZER_SERVICE_TYPE, - endpoint_type=self.opts.os_endpoint_type, - ) - return endpoint - - @property - def auth_token(self): - return self.session.get_token() - - @cached_property - def client_id(self): - return '{0}_{1}'.format(self.session.get_project_id(), - socket.gethostname()) diff --git a/devstack/README.rst b/devstack/README.rst new file mode 100644 index 0000000..e69de29 diff --git a/devstack/gate_hook.sh b/devstack/gate_hook.sh new file mode 100644 index 0000000..94ef2fc --- /dev/null +++ b/devstack/gate_hook.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# (c) Copyright 2014-2016 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. + +set -ex + +# Install python-freezerclient devstack integration +export DEVSTACK_LOCAL_CONFIG="enable_plugin python-freezerclient https://git.openstack.org/openstack/python-freezerclient" + +$BASE/new/devstack-gate/devstack-vm-gate.sh \ No newline at end of file diff --git a/devstack/lib/freezerclient b/devstack/lib/freezerclient new file mode 100644 index 0000000..36228dd --- /dev/null +++ b/devstack/lib/freezerclient @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# (c) Copyright 2014-2016 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. + +# Install python-freezerclient + +# add the following to localrc: +# enable_service python-freezerclient +# +# Dependencies: +# - functions +# - OS_AUTH_URL for auth in api +# - DEST set to the destination directory +# - SERVICE_PASSWORD, SERVICE_TENANT_NAME for auth in api +# - STACK_USER service user + +# Save trace setting +XTRACE=$(set +o | grep xtrace) +set +o xtrace + +# Functions +# --------- + +function install_python-freezerclient { + + git_clone $FREEZERCLIENT_REPO $FREEZERCLIENT_DIR $FREEZERCLIENT_BRANCH + setup_develop $FREEZER_DIR +} + +# Restore xtrace +$XTRACE \ No newline at end of file diff --git a/devstack/local.conf.example b/devstack/local.conf.example new file mode 100644 index 0000000..fd691b7 --- /dev/null +++ b/devstack/local.conf.example @@ -0,0 +1,31 @@ +# (c) Copyright 2014-2016 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. + +[[local|localrc]] +disable_all_services + +enable_plugin python-freezerclient https://git.openstack.org/openstack/python-freezerclient master + +enable_service rabbit mysql key + +# This is to keep the token small for testing +KEYSTONE_TOKEN_FORMAT=UUID + +# Modify passwords as needed +DATABASE_PASSWORD=secretdatabase +RABBIT_PASSWORD=secretrabbit +ADMIN_PASSWORD=secretadmin +SERVICE_PASSWORD=secretservice +SERVICE_TOKEN=111222333444 + diff --git a/devstack/plugin.sh b/devstack/plugin.sh new file mode 100644 index 0000000..bf68048 --- /dev/null +++ b/devstack/plugin.sh @@ -0,0 +1,27 @@ +# (c) Copyright 2014-2016 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. + +# check for service enabled +if is_service_enabled python-freezerclient; then + if [[ "$1" == "source" || "`type -t install_freezerclient`" != 'function' ]]; then + # Initial source + source $FREEZER_DIR/devstack/lib/python-freezerclient + fi + + if [[ "$1" == "stack" && "$2" == "install" ]]; then + echo_summary "Installing python-freezerclient" + install_freezerclient + fi +fi + diff --git a/devstack/settings b/devstack/settings new file mode 100644 index 0000000..5713c63 --- /dev/null +++ b/devstack/settings @@ -0,0 +1,26 @@ +# (c) Copyright 2014-2016 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. + +# Defaults +# -------- + +# Set up default directories +FREEZERCLIENT_DIR=$DEST/python-freezerclient +FREEZERCLIENT_LOG_DIR=$DEST/logs + +# Python freezerclient repository +FREEZERCLIENT_REPO=${FREEZER_REPO:-${GIT_BASE}/openstack/python-freezerclient.git} +FREEZERCLIENT_BRANCH=${FREEZER_BRANCH:-master} + +enable_service python-freezerclient \ No newline at end of file diff --git a/doc/.gitignore b/doc/.gitignore new file mode 100644 index 0000000..4c2e299 --- /dev/null +++ b/doc/.gitignore @@ -0,0 +1,3 @@ +build/ +source/ref/ +source/api/ diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 0000000..1e0a1c7 --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,265 @@ +# -*- coding: utf-8 -*- +# +# Freezer documentation build configuration file, created by +# sphinx-quickstart on Thu Feb 4 22:27:35 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys +import os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.autodoc', + 'sphinx.ext.viewcode', + 'oslosphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Python Freezer Client' +copyright = u'2016, OpenStack' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '2.0' +# The full version, including alpha/beta/rc tags. +release = '2.0.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +#keep_warnings = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +#html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Freezerdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + ('index', 'Freezer.tex', u'Freezer Documentation', + u'OpenStack', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'freezer', u'Freezer Documentation', + [u'OpenStack'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'Freezer', u'Freezer Documentation', + u'OpenStack', 'Freezer', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +#texinfo_no_detailmenu = False + + +# Example configuration for intersphinx: refer to the Python standard library. +#intersphinx_mapping = {'http://docs.python.org/': None} + diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 0000000..ae0dc36 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,23 @@ +.. Python Freezer Client documentation master file, created by + sphinx-quickstart on Thu Feb 4 22:27:35 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Freezer's documentation! +=================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + + diff --git a/freezerclient/__init__.py b/freezerclient/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/exceptions.py b/freezerclient/exceptions.py similarity index 72% rename from exceptions.py rename to freezerclient/exceptions.py index 5e3dd08..9028f19 100644 --- a/exceptions.py +++ b/freezerclient/exceptions.py @@ -1,19 +1,16 @@ -""" -(c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -""" +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json diff --git a/freezerclient/shell.py b/freezerclient/shell.py new file mode 100644 index 0000000..6702ccd --- /dev/null +++ b/freezerclient/shell.py @@ -0,0 +1,234 @@ +# (c) Copyright 2014-2016 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 logging + +import os +import sys + +from cliff.app import App +from cliff.commandmanager import CommandManager + +from freezerclient.v1 import actions +from freezerclient.v1 import backups +from freezerclient.v1.client import Client +from freezerclient.v1 import clients +from freezerclient.v1 import jobs +from freezerclient.v1 import sessions + + +log = logging.getLogger(__name__) + + +class FreezerCommandManager(CommandManager): + """ All commands available for the shell are registered here """ + SHELL_COMMANDS = { + 'job-show': jobs.JobShow, + 'job-list': jobs.JobList, + 'job-create': jobs.JobCreate, + 'job-get': jobs.JobGet, + 'job-delete': jobs.JobDelete, + 'job-start': jobs.JobStart, + 'job-stop': jobs.JobStop, + 'job-abort': jobs.JobAbort, + 'job-update': jobs.JobUpdate, + 'client-list': clients.ClientList, + 'client-show': clients.ClientShow, + 'client-register': clients.ClientRegister, + 'client-delete': clients.ClientDelete, + 'backup-list': backups.BackupList, + 'backup-show': backups.BackupShow, + 'session-list': sessions.SessionList, + 'session-show': sessions.SessionShow, + 'session-create': sessions.SessionCreate, + 'session-add-job': sessions.SessionAddJob, + 'session-remove-job': sessions.SessionRemoveJob, + 'session-start': sessions.SessionStart, + 'session-end': sessions.SessionEnd, + 'session-update': sessions.SessionUpdate, + 'action-show': actions.ActionShow, + 'action-list': actions.ActionList, + 'action-delete': actions.ActionDelete, + 'action-create': actions.ActionCreate, + 'action-update': actions.ActionUpdate + } + + def load_commands(self, namespace): + for name, command_class in self.SHELL_COMMANDS.items(): + self.add_command(name, command_class) + + +class FreezerShell(App): + def __init__(self): + super(FreezerShell, self).__init__( + description='Python Freezer Client', + version='0.1', + deferred_help=True, + command_manager=FreezerCommandManager(None), + ) + + def build_option_parser(self, description, version): + parser = super(FreezerShell, self).build_option_parser(description, version) + parser.add_argument( + '--os-auth-url', + dest='os_auth_url', + default=os.environ.get('OS_AUTH_URL'), + help='Specify identity endpoint', + ) + + parser.add_argument( + '--os-backup-url', + dest='os_backup_url', + default=os.environ.get('OS_BACKUP_URL'), + help='Specify the Freezer backup service endpoint to use' + ) + + parser.add_argument( + '--os-endpoint-type', + dest='os_endpoint_type', + default=os.environ.get('OS_ENDPOINT_TYPE'), + help='''Endpoint type to select. Valid endpoint types: + "public" or "publicURL", "internal" or "internalURL", + "admin" or "adminURL"''' + ) + + parser.add_argument( + '--os-identity-api-version', + dest='os_identity_api_version', + default=os.environ.get('OS_IDENTITY_API_VERSION'), + help='Identity API version: 2.0 or 3' + ) + + parser.add_argument( + '--os-password', + dest='os_password', + default=os.environ.get('OS_PASSWORD'), + help='''Password used for authentication with the OpenStack + Identity service''' + ) + + parser.add_argument( + '--os-username', + dest='os_username', + default=os.environ.get('OS_USERNAME'), + help='''Name used for authentication with the OpenStack + Identity service''' + ) + + parser.add_argument( + '--os-token', + dest='os_token', + default=os.environ.get('OS_TOKEN'), + help='''Specify an existing token to use instead of retrieving + one via authentication''' + ) + + parser.add_argument( + '--os-project-domain-name', + dest='os_project_domain_name', + default=os.environ.get('OS_PROJECT_DOMAIN_NAME'), + help='Domain name containing project' + ) + + parser.add_argument( + '--os-project-name', + dest='os_project_name', + default=os.environ.get('OS_PROJECT_NAME'), + help='Project name to scope to' + ) + + parser.add_argument( + '--os-region-name', + dest='os_region_name', + default=os.environ.get('OS_REGION_NAME'), + help='Specify the region to use' + ) + + parser.add_argument( + '--os-tenant-id', + dest='os_tenant_id', + default=os.environ.get('OS_TENANT_ID'), + help='Tenant to request authorization on' + ) + + parser.add_argument( + '--os-tenant-name', + dest='os_tenant_name', + default=os.environ.get('OS_TENANT_NAME'), + help='Tenant to request authorization on' + ) + + parser.add_argument( + '--os-user-domain-name', + dest='os_user_domain_name', + default=os.environ.get('OS_USER_DOMAIN_NAME'), + help='User domain name' + ) + + parser.add_argument( + '-k', '--insecure', + dest='insecure', + action='store_true', + default=os.environ.get('OS_INSECURE'), + help='use python-freezerclient with insecure connections' + ) + + parser.add_argument( + '--os-cacert', + dest='os_cacert', + default=os.environ.get('OS_CACERT'), + help='''Path of CA TLS certificate(s) used to verify the + remote server's certificate. Without this option + freezer looks for the default system CA certificates.''' + ) + + parser.add_argument( + '--os-cert', + dest='os_cert', + default=os.environ.get('OS_CERT'), + help='''Path of CERT TLS certificate(s) used to verify the + remote server's certificate.1''' + ) + + return parser + + @property + def client(self): + """ Build a client object to communicate with the API + :return: freezerclient object + """ + opts = { + 'token': self.options.os_token, + 'version': self.options.os_identity_api_version, + 'username': self.options.os_username, + 'password': self.options.os_password, + 'tenant_name': self.options.os_tenant_name, + 'auth_url': self.options.os_auth_url, + 'endpoint': self.options.os_backup_url, + 'project_name': self.options.os_project_name, + 'user_domain_name': self.options.os_user_domain_name, + 'project_domain_name': self.options.os_project_domain_name, + 'verify': True or self.options.os_cacert, + 'cert': self.options.os_cert + } + return Client(**opts) + + +def main(argv=sys.argv[1:]): + print('hola') + return FreezerShell().run(argv) + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/freezerclient/tests/__init__.py b/freezerclient/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezerclient/tests/unit/__init__.py b/freezerclient/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezerclient/tests/unit/test_exceptions.py b/freezerclient/tests/unit/test_exceptions.py new file mode 100644 index 0000000..c050f51 --- /dev/null +++ b/freezerclient/tests/unit/test_exceptions.py @@ -0,0 +1,25 @@ +# (c) Copyright 2014-2016 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 unittest + +from freezerclient import exceptions + + +class TestApiClientException(unittest.TestCase): + + def test_get_message_from_response_string(self): + e = exceptions.ApiClientException('some error message') + self.assertEquals(str(e), 'some error message') diff --git a/freezerclient/tests/unit/test_utils.py b/freezerclient/tests/unit/test_utils.py new file mode 100644 index 0000000..6728e57 --- /dev/null +++ b/freezerclient/tests/unit/test_utils.py @@ -0,0 +1,13 @@ +# (c) Copyright 2014-2016 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. diff --git a/freezerclient/tests/unit/v1/__init__.py b/freezerclient/tests/unit/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezerclient/tests/unit/v1/test_client.py b/freezerclient/tests/unit/v1/test_client.py new file mode 100644 index 0000000..87b79ce --- /dev/null +++ b/freezerclient/tests/unit/v1/test_client.py @@ -0,0 +1,150 @@ +# (c) Copyright 2014,2015,2016 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 unittest + +from freezerclient.v1 import client + +from mock import Mock, patch + + +class TestSupportFunctions(unittest.TestCase): + + def test_guess_auth_version_returns_none(self): + mock_opts = Mock() + mock_opts.os_identity_api_version = '' + mock_opts.os_auth_url = '' + self.assertRaises(Exception, client.guess_auth_version, mock_opts) + + def test_guess_auth_version_explicit_3(self): + mock_opts = Mock() + mock_opts.os_identity_api_version = '3' + self.assertEquals(client.guess_auth_version(mock_opts), '3') + + def test_guess_auth_version_explicit_2(self): + mock_opts = Mock() + mock_opts.os_identity_api_version = '2.0' + self.assertEquals(client.guess_auth_version(mock_opts), '2.0') + + def test_guess_auth_version_implicit_3(self): + mock_opts = Mock() + mock_opts.os_auth_url = 'http://whatever/v3' + self.assertEquals(client.guess_auth_version(mock_opts), '3') + + def test_guess_auth_version_implicit_2(self): + mock_opts = Mock() + mock_opts.os_auth_url = 'http://whatever/v2.0' + self.assertEquals(client.guess_auth_version(mock_opts), '2.0') + + @patch('freezerclient.v1.client.v3') + @patch('freezerclient.v1.client.v2') + def test_get_auth_plugin_v3_Password(self, mock_v2, mock_v3): + mock_opts = Mock() + mock_opts.os_identity_api_version = '3' + mock_opts.os_user_name = 'myuser' + mock_opts.os_token = '' + client.get_auth_plugin(mock_opts) + self.assertTrue(mock_v3.Password.called) + + @patch('freezerclient.v1.client.v3') + @patch('freezerclient.v1.client.v2') + def test_get_auth_plugin_v3_Token(self, mock_v2, mock_v3): + mock_opts = Mock() + mock_opts.os_identity_api_version = '3' + mock_opts.os_username = '' + mock_opts.os_token = 'mytoken' + client.get_auth_plugin(mock_opts) + self.assertTrue(mock_v3.Token.called) + + @patch('freezerclient.v1.client.v3') + @patch('freezerclient.v1.client.v2') + def test_get_auth_plugin_v2_Password(self, mock_v2, mock_v3): + mock_opts = Mock() + mock_opts.os_identity_api_version = '2.0' + mock_opts.os_user_name = 'myuser' + mock_opts.os_token = '' + client.get_auth_plugin(mock_opts) + self.assertTrue(mock_v2.Password.called) + + @patch('freezerclient.v1.client.v3') + @patch('freezerclient.v1.client.v2') + def test_get_auth_plugin_v2_Token(self, mock_v2, mock_v3): + mock_opts = Mock() + mock_opts.os_identity_api_version = '2.0' + mock_opts.os_username = '' + mock_opts.os_token = 'mytoken' + client.get_auth_plugin(mock_opts) + self.assertTrue(mock_v2.Token.called) + + @patch('freezerclient.v1.client.v3') + @patch('freezerclient.v1.client.v2') + def test_get_auth_plugin_raises_when_no_username_token(self, mock_v2, mock_v3): + mock_opts = Mock() + mock_opts.os_identity_api_version = '2.0' + mock_opts.os_username = '' + mock_opts.os_token = '' + self.assertRaises(Exception, client.get_auth_plugin, mock_opts) + + +class TestClientMock(unittest.TestCase): + + @patch('freezerclient.v1.client.ksc_session') + @patch('freezerclient.v1.client.get_auth_plugin') + def test_client_new(self, mock_get_auth_plugin, mock_ksc_session): + c = client.Client(opts=Mock(), endpoint='blabla') + self.assertIsInstance(c, client.Client) + + @patch('freezerclient.v1.client.ksc_session') + @patch('freezerclient.v1.client.get_auth_plugin') + def test_client_new_with_kwargs(self, mock_get_auth_plugin, mock_ksc_session): + kwargs = {'token': 'alpha', + 'username': 'bravo', + 'password': 'charlie', + 'tenant_name': 'delta', + 'auth_url': 'echo', + 'session': 'foxtrot', + 'endpoint': 'golf', + 'version': 'hotel', + 'opts': Mock()} + c = client.Client(**kwargs) + self.assertIsInstance(c, client.Client) + self.assertEqual('alpha', c.opts.os_token) + self.assertEqual('bravo', c.opts.os_username) + self.assertEqual('charlie', c.opts.os_password) + self.assertEqual('delta', c.opts.os_tenant_name) + self.assertEqual('echo', c.opts.os_auth_url) + self.assertEqual('foxtrot', c._session) + self.assertEqual('foxtrot', c.session) + self.assertEqual('golf', c.endpoint) + + @patch('freezerclient.v1.client.ksc_session') + @patch('freezerclient.v1.client.get_auth_plugin') + def test_get_token(self, mock_get_auth_plugin, mock_ksc_session): + mock_session = Mock() + mock_session.get_token.return_value = 'antaniX2' + c = client.Client(session=mock_session, endpoint='justtest', opts=Mock()) + self.assertIsInstance(c, client.Client) + self.assertEquals(c.auth_token, 'antaniX2') + + @patch('freezerclient.v1.client.socket') + @patch('freezerclient.v1.client.ksc_session') + @patch('freezerclient.v1.client.get_auth_plugin') + def test_get_client_id(self, mock_get_auth_plugin, mock_ksc_session, mock_socket): + mock_socket.gethostname.return_value = 'parmenide' + mock_session = Mock() + mock_session.get_project_id.return_value = 'H2O' + c = client.Client(session=mock_session, endpoint='justtest', opts=Mock()) + self.assertIsInstance(c, client.Client) + self.assertEquals(c.client_id, 'H2O_parmenide') diff --git a/freezerclient/tests/unit/v1/test_client_actions.py b/freezerclient/tests/unit/v1/test_client_actions.py new file mode 100644 index 0000000..8700c50 --- /dev/null +++ b/freezerclient/tests/unit/v1/test_client_actions.py @@ -0,0 +1,128 @@ +# (c) Copyright 2014,2015,2016 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 unittest + +from freezerclient import exceptions +from freezerclient.v1.managers import actions + +from mock import Mock, patch + + +class TestActionManager(unittest.TestCase): + + def setUp(self): + self.mock_client = Mock() + self.mock_response = Mock() + self.mock_client.endpoint = 'http://testendpoint:9999' + self.mock_client.auth_token = 'testtoken' + self.mock_client.client_id = 'test_client_id_78900987' + self.action_manager = actions.ActionManager(self.mock_client) + + @patch('freezerclient.v1.managers.actions.requests') + def test_create(self, mock_requests): + self.assertEqual('http://testendpoint:9999/v1/actions/', self.action_manager.endpoint) + self.assertEqual({'X-Auth-Token': 'testtoken'}, self.action_manager.headers) + + @patch('freezerclient.v1.managers.actions.requests') + def test_create_ok(self, mock_requests): + self.mock_response.status_code = 201 + self.mock_response.json.return_value = {'action_id': 'qwerqwer'} + mock_requests.post.return_value = self.mock_response + retval = self.action_manager.create({'action': 'metadata'}) + self.assertEqual('qwerqwer', retval) + + @patch('freezerclient.v1.managers.actions.requests') + def test_create_fail_when_api_return_error_code(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.action_manager.create, {'action': 'metadata'}) + + @patch('freezerclient.v1.managers.actions.requests') + def test_delete_ok(self, mock_requests): + self.mock_response.status_code = 204 + mock_requests.delete.return_value = self.mock_response + retval = self.action_manager.delete('test_action_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.actions.requests') + def test_delete_fail(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.delete.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.action_manager.delete, 'test_action_id') + + @patch('freezerclient.v1.managers.actions.requests') + def test_get_ok(self, mock_requests): + self.mock_response.status_code = 200 + self.mock_response.json.return_value = {'action_id': 'qwerqwer'} + mock_requests.get.return_value = self.mock_response + retval = self.action_manager.get('test_action_id') + self.assertEqual({'action_id': 'qwerqwer'}, retval) + + @patch('freezerclient.v1.managers.actions.requests') + def test_get_fails_on_error_different_from_404(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.get.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.action_manager.get, 'test_action_id') + + @patch('freezerclient.v1.managers.actions.requests') + def test_get_none(self, mock_requests): + self.mock_response.status_code = 404 + mock_requests.get.return_value = self.mock_response + retval = self.action_manager.get('test_action_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.actions.requests') + def test_list_ok(self, mock_requests): + self.mock_response.status_code = 200 + action_list = [{'action_id_0': 'bomboloid'}, {'action_id_1': 'asdfasdf'}] + self.mock_response.json.return_value = {'actions': action_list} + mock_requests.get.return_value = self.mock_response + retval = self.action_manager.list() + self.assertEqual(action_list, retval) + + @patch('freezerclient.v1.managers.actions.requests') + def test_list_error(self, mock_requests): + self.mock_response.status_code = 404 + action_list = [{'action_id_0': 'bomboloid'}, {'action_id_1': 'asdfasdf'}] + self.mock_response.json.return_value = {'clients': action_list} + mock_requests.get.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.action_manager.list) + + @patch('freezerclient.v1.managers.actions.requests') + def test_update_ok(self, mock_requests): + self.mock_response.status_code = 200 + self.mock_response.json.return_value = { + "patch": {"status": "bamboozled"}, + "version": 12, + "action_id": "d454beec-1f3c-4d11-aa1a-404116a40502" + } + mock_requests.patch.return_value = self.mock_response + retval = self.action_manager.update('d454beec-1f3c-4d11-aa1a-404116a40502', {'status': 'bamboozled'}) + self.assertEqual(12, retval) + + @patch('freezerclient.v1.managers.actions.requests') + def test_update_raise_MetadataUpdateFailure_when_api_return_error_code(self, mock_requests): + self.mock_response.json.return_value = { + "patch": {"status": "bamboozled"}, + "version": 12, + "action_id": "d454beec-1f3c-4d11-aa1a-404116a40502" + } + self.mock_response.status_code = 404 + self.mock_response.text = '{"title": "Not Found","description":"No document found with ID d454beec-1f3c-4d11-aa1a-404116a40502x"}' + mock_requests.patch.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.action_manager.update, + 'd454beec-1f3c-4d11-aa1a-404116a40502', {'status': 'bamboozled'}) + diff --git a/freezerclient/tests/unit/v1/test_client_backups.py b/freezerclient/tests/unit/v1/test_client_backups.py new file mode 100644 index 0000000..bca769f --- /dev/null +++ b/freezerclient/tests/unit/v1/test_client_backups.py @@ -0,0 +1,129 @@ +# (c) Copyright 2014,2015,2016 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 unittest + +from freezerclient import exceptions +from freezerclient.v1.managers import backups + +from mock import Mock, patch + + +class TestBackupManager(unittest.TestCase): + + def setUp(self): + self.mock_client = Mock() + self.mock_client.endpoint = 'http://testendpoint:9999' + self.mock_client.auth_token = 'testtoken' + self.b = backups.BackupsManager(self.mock_client) + + @patch('freezerclient.v1.managers.backups.requests') + def test_create(self, mock_requests): + self.assertEqual('http://testendpoint:9999/v1/backups/', self.b.endpoint) + self.assertEqual({'X-Auth-Token': 'testtoken'}, self.b.headers) + + @patch('freezerclient.v1.managers.backups.requests') + def test_create_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 201 + mock_response.json.return_value = {'backup_id': 'qwerqwer'} + mock_requests.post.return_value = mock_response + retval = self.b.create(backup_metadata={'backup': 'metadata'}) + self.assertEqual('qwerqwer', retval) + + @patch('freezerclient.v1.managers.backups.requests') + def test_create_fail_when_api_return_error_code(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 500 + mock_requests.post.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, self.b.create, {'backup': 'metadata'}) + + @patch('freezerclient.v1.managers.backups.requests') + def test_delete_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 204 + mock_requests.delete.return_value = mock_response + retval = self.b.delete('test_backup_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.backups.requests') + def test_delete_fail(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 500 + mock_requests.delete.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, self.b.delete, 'test_backup_id') + + @patch('freezerclient.v1.managers.backups.requests') + def test_get_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'backup_id': 'qwerqwer'} + mock_requests.get.return_value = mock_response + retval = self.b.get('test_backup_id') + self.assertEqual({'backup_id': 'qwerqwer'}, retval) + + @patch('freezerclient.v1.managers.backups.requests') + def test_get_none(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 404 + mock_requests.get.return_value = mock_response + retval = self.b.get('test_backup_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.backups.requests') + def test_get_error(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 403 + mock_requests.get.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, + self.b.get, 'test_backup_id') + + @patch('freezerclient.v1.managers.backups.requests') + def test_list_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 200 + backup_list = [{'backup_id_0': 'qwerqwer'}, {'backup_id_1': 'asdfasdf'}] + mock_response.json.return_value = {'backups': backup_list} + mock_requests.get.return_value = mock_response + retval = self.b.list() + self.assertEqual(backup_list, retval) + + @patch('freezerclient.v1.managers.backups.requests') + def test_list_parameters(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 200 + backup_list = [{'backup_id_0': 'qwerqwer'}, {'backup_id_1': 'asdfasdf'}] + mock_response.json.return_value = {'backups': backup_list} + mock_requests.get.return_value = mock_response + retval = self.b.list(limit=5, + offset=5, + search={"time_before": 1428529956}) + mock_requests.get.assert_called_with( + 'http://testendpoint:9999/v1/backups/', + params={'limit': 5, 'offset': 5}, + data='{"time_before": 1428529956}', + headers={'X-Auth-Token': 'testtoken'}, + verify=True) + self.assertEqual(backup_list, retval) + + @patch('freezerclient.v1.managers.backups.requests') + def test_list_error(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 404 + backup_list = [{'backup_id_0': 'qwerqwer'}, {'backup_id_1': 'asdfasdf'}] + mock_response.json.return_value = {'backups': backup_list} + mock_requests.get.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, self.b.list) + diff --git a/freezerclient/tests/unit/v1/test_client_clients.py b/freezerclient/tests/unit/v1/test_client_clients.py new file mode 100644 index 0000000..99f33d1 --- /dev/null +++ b/freezerclient/tests/unit/v1/test_client_clients.py @@ -0,0 +1,109 @@ +# (c) Copyright 2014,2015,2016 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 unittest + +from freezerclient import exceptions +from freezerclient.v1.client import clients + +from mock import Mock, patch + + +class TestClientManager(unittest.TestCase): + + def setUp(self): + self.mock_client = Mock() + self.mock_client.endpoint = 'http://testendpoint:9999' + self.mock_client.auth_token = 'testtoken' + self.r = clients.ClientManager(self.mock_client) + + @patch('freezerclient.v1.managers.clients.requests') + def test_create(self, mock_requests): + self.assertEqual('http://testendpoint:9999/v1/clients/', self.r.endpoint) + self.assertEqual({'X-Auth-Token': 'testtoken'}, self.r.headers) + + @patch('freezerclient.v1.managers.clients.requests') + def test_create_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 201 + mock_response.json.return_value = {'client_id': 'qwerqwer'} + mock_requests.post.return_value = mock_response + retval = self.r.create(client_info={'client': 'metadata'}) + self.assertEqual('qwerqwer', retval) + + @patch('freezerclient.v1.managers.clients.requests') + def test_create_fail_when_api_return_error_code(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 500 + mock_requests.post.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, self.r.create, {'client': 'metadata'}) + + @patch('freezerclient.v1.managers.clients.requests') + def test_delete_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 204 + mock_requests.delete.return_value = mock_response + retval = self.r.delete('test_client_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.clients.requests') + def test_delete_fail(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 500 + mock_requests.delete.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, self.r.delete, 'test_client_id') + + @patch('freezerclient.v1.managers.clients.requests') + def test_get_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 200 + mock_response.json.return_value = {'client_id': 'qwerqwer'} + mock_requests.get.return_value = mock_response + retval = self.r.get('test_client_id') + self.assertEqual({'client_id': 'qwerqwer'}, retval) + + @patch('freezerclient.v1.managers.clients.requests') + def test_get_none(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 404 + mock_requests.get.return_value = mock_response + retval = self.r.get('test_client_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.clients.requests') + def test_get_raises_ApiClientException_on_error_not_404(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 500 + mock_requests.get.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, self.r.get, 'test_client_id') + + @patch('freezerclient.v1.managers.clients.requests') + def test_list_ok(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 200 + client_list = [{'client_id_0': 'qwerqwer'}, {'client_id_1': 'asdfasdf'}] + mock_response.json.return_value = {'clients': client_list} + mock_requests.get.return_value = mock_response + retval = self.r.list() + self.assertEqual(client_list, retval) + + @patch('freezerclient.v1.managers.clients.requests') + def test_list_error(self, mock_requests): + mock_response = Mock() + mock_response.status_code = 404 + client_list = [{'client_id_0': 'qwerqwer'}, {'client_id_1': 'asdfasdf'}] + mock_response.json.return_value = {'clients': client_list} + mock_requests.get.return_value = mock_response + self.assertRaises(exceptions.ApiClientException, self.r.list) + diff --git a/freezerclient/tests/unit/v1/test_client_jobs.py b/freezerclient/tests/unit/v1/test_client_jobs.py new file mode 100644 index 0000000..4338ca4 --- /dev/null +++ b/freezerclient/tests/unit/v1/test_client_jobs.py @@ -0,0 +1,239 @@ +# (c) Copyright 2014,2015,2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import unittest + +from freezerclient import exceptions +from freezerclient.v1.client import jobs + +from mock import Mock, patch + + +class TestJobManager(unittest.TestCase): + + def setUp(self): + self.mock_client = Mock() + self.mock_response = Mock() + self.mock_client.endpoint = 'http://testendpoint:9999' + self.mock_client.auth_token = 'testtoken' + self.headers = {'X-Auth-Token': 'testtoken'} + self.mock_client.client_id = 'test_client_id_78900987' + self.job_manager = jobs.JobManager(self.mock_client) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_create(self, mock_requests): + self.assertEqual('http://testendpoint:9999/v1/jobs/', self.job_manager.endpoint) + self.assertEqual({'X-Auth-Token': 'testtoken'}, self.job_manager.headers) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_create_ok(self, mock_requests): + self.mock_response.status_code = 201 + self.mock_response.json.return_value = {'job_id': 'qwerqwer'} + mock_requests.post.return_value = self.mock_response + retval = self.job_manager.create({'job': 'metadata'}) + self.assertEqual('qwerqwer', retval) + + @patch('freezerclient.v1.managers.jobs.json') + @patch('freezerclient.v1.managers.jobs.requests') + def test_create_adds_client_id_if_not_provided(self, mock_requests, mock_json): + self.mock_response.status_code = 201 + self.mock_response.json.return_value = {'job_id': 'qwerqwer'} + mock_json.dumps.return_value = {'job': 'mocked'} + mock_requests.post.return_value = self.mock_response + + retval = self.job_manager.create({'job': 'metadata'}) + + mock_json.dumps.assert_called_with({'job': 'metadata', + 'client_id': 'test_client_id_78900987'}) + self.assertEqual('qwerqwer', retval) + + @patch('freezerclient.v1.managers.jobs.json') + @patch('freezerclient.v1.managers.jobs.requests') + def test_create_leaves_provided_client_id(self, mock_requests, mock_json): + self.mock_response.status_code = 201 + self.mock_response.json.return_value = {'job_id': 'qwerqwer'} + mock_json.dumps.return_value = {'job': 'mocked'} + mock_requests.post.return_value = self.mock_response + + retval = self.job_manager.create({'job': 'metadata', 'client_id': 'parmenide'}) + + mock_json.dumps.assert_called_with({'job': 'metadata', + 'client_id': 'parmenide'}) + self.assertEqual('qwerqwer', retval) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_create_fail_when_api_return_error_code(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.create, {'job': 'metadata'}) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_delete_ok(self, mock_requests): + self.mock_response.status_code = 204 + mock_requests.delete.return_value = self.mock_response + retval = self.job_manager.delete('test_job_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_delete_fail(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.delete.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.delete, 'test_job_id') + + @patch('freezerclient.v1.managers.jobs.requests') + def test_get_ok(self, mock_requests): + self.mock_response.status_code = 200 + self.mock_response.json.return_value = {'job_id': 'qwerqwer'} + mock_requests.get.return_value = self.mock_response + retval = self.job_manager.get('test_job_id') + self.assertEqual({'job_id': 'qwerqwer'}, retval) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_get_fails_on_error_different_from_404(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.get.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.get, 'test_job_id') + + @patch('freezerclient.v1.managers.jobs.requests') + def test_get_none(self, mock_requests): + self.mock_response.status_code = 404 + mock_requests.get.return_value = self.mock_response + retval = self.job_manager.get('test_job_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_list_ok(self, mock_requests): + self.mock_response.status_code = 200 + job_list = [{'job_id_0': 'bomboloid'}, {'job_id_1': 'asdfasdf'}] + self.mock_response.json.return_value = {'jobs': job_list} + mock_requests.get.return_value = self.mock_response + retval = self.job_manager.list() + self.assertEqual(job_list, retval) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_list_error(self, mock_requests): + self.mock_response.status_code = 404 + job_list = [{'job_id_0': 'bomboloid'}, {'job_id_1': 'asdfasdf'}] + self.mock_response.json.return_value = {'clients': job_list} + mock_requests.get.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.list) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_update_ok(self, mock_requests): + self.mock_response.status_code = 200 + self.mock_response.json.return_value = { + "patch": {"status": "bamboozled"}, + "version": 12, + "job_id": "d454beec-1f3c-4d11-aa1a-404116a40502" + } + mock_requests.patch.return_value = self.mock_response + retval = self.job_manager.update('d454beec-1f3c-4d11-aa1a-404116a40502', {'status': 'bamboozled'}) + self.assertEqual(12, retval) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_update_raise_MetadataUpdateFailure_when_api_return_error_code(self, mock_requests): + self.mock_response.json.return_value = { + "patch": {"status": "bamboozled"}, + "version": 12, + "job_id": "d454beec-1f3c-4d11-aa1a-404116a40502" + } + self.mock_response.status_code = 404 + self.mock_response.text = '{"title": "Not Found","description":"No document found with ID d454beec-1f3c-4d11-aa1a-404116a40502x"}' + mock_requests.patch.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.update, + 'd454beec-1f3c-4d11-aa1a-404116a40502', {'status': 'bamboozled'}) + + + @patch('freezerclient.v1.managers.jobs.requests') + def test_start_job_posts_proper_data(self, mock_requests): + job_id = 'jobdfsfnqwerty1234' + self.mock_response.status_code = 202 + self.mock_response.json.return_value = {'result': 'success'} + mock_requests.post.return_value = self.mock_response + # /v1/jobs/{job_id}/event + + endpoint = '{0}/v1/jobs/{1}/event'.format(self.mock_client.endpoint, job_id) + data = {"start": None} + retval = self.job_manager.start_job(job_id) + self.assertEqual({'result': 'success'}, retval) + + args = mock_requests.post.call_args[0] + kwargs = mock_requests.post.call_args[1] + self.assertEquals(endpoint, args[0]) + self.assertEquals(data, json.loads(kwargs['data'])) + self.assertEquals(self.headers, kwargs['headers']) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_start_job_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + job_id = 'jobdfsfnqwerty1234' + self.mock_response.status_code = 500 + self.mock_response.json.return_value = {'result': 'success'} + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.start_job, job_id) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_stop_job_posts_proper_data(self, mock_requests): + job_id = 'jobdfsfnqwerty1234' + self.mock_response.status_code = 202 + self.mock_response.json.return_value = {'result': 'success'} + mock_requests.post.return_value = self.mock_response + # /v1/jobs/{job_id}/event + + endpoint = '{0}/v1/jobs/{1}/event'.format(self.mock_client.endpoint, job_id) + data = {"stop": None} + retval = self.job_manager.stop_job(job_id) + self.assertEqual({'result': 'success'}, retval) + + args = mock_requests.post.call_args[0] + kwargs = mock_requests.post.call_args[1] + self.assertEquals(endpoint, args[0]) + self.assertEquals(data, json.loads(kwargs['data'])) + self.assertEquals(self.headers, kwargs['headers']) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_stop_job_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + job_id = 'jobdfsfnqwerty1234' + self.mock_response.status_code = 500 + self.mock_response.json.return_value = {'result': 'success'} + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.start_job, job_id) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_abort_job_posts_proper_data(self, mock_requests): + job_id = 'jobdfsfnqwerty1234' + self.mock_response.status_code = 202 + self.mock_response.json.return_value = {'result': 'success'} + mock_requests.post.return_value = self.mock_response + # /v1/jobs/{job_id}/event + + endpoint = '{0}/v1/jobs/{1}/event'.format(self.mock_client.endpoint, job_id) + data = {"abort": None} + retval = self.job_manager.abort_job(job_id) + self.assertEqual({'result': 'success'}, retval) + + args = mock_requests.post.call_args[0] + kwargs = mock_requests.post.call_args[1] + self.assertEquals(endpoint, args[0]) + self.assertEquals(data, json.loads(kwargs['data'])) + self.assertEquals(self.headers, kwargs['headers']) + + @patch('freezerclient.v1.managers.jobs.requests') + def test_abort_job_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + job_id = 'jobdfsfnqwerty1234' + self.mock_response.status_code = 500 + self.mock_response.json.return_value = {'result': 'success'} + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.job_manager.abort_job, job_id) + diff --git a/freezerclient/tests/unit/v1/test_client_sessions.py b/freezerclient/tests/unit/v1/test_client_sessions.py new file mode 100644 index 0000000..ab09a95 --- /dev/null +++ b/freezerclient/tests/unit/v1/test_client_sessions.py @@ -0,0 +1,222 @@ +# (c) Copyright 2014,2015,2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import unittest + +from freezerclient import exceptions +from freezerclient.v1.client import sessions + +from mock import Mock, patch + + +class TestSessionManager(unittest.TestCase): + + def setUp(self): + self.mock_client = Mock() + self.mock_response = Mock() + self.mock_client.endpoint = 'http://testendpoint:9999' + self.mock_client.auth_token = 'testtoken' + self.mock_client.client_id = 'test_client_id_78900987' + self.session_manager = sessions.SessionManager(self.mock_client) + self.endpoint = 'http://testendpoint:9999/v1/sessions/' + self.headers = {'X-Auth-Token': 'testtoken'} + + @patch('freezerclient.v1.managers.sessions.requests') + def test_create(self, mock_requests): + self.assertEqual(self.endpoint, self.session_manager.endpoint) + self.assertEqual(self.headers, self.session_manager.headers) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_create_ok(self, mock_requests): + self.mock_response.status_code = 201 + self.mock_response.json.return_value = {'session_id': 'qwerqwer'} + mock_requests.post.return_value = self.mock_response + retval = self.session_manager.create({'session': 'metadata'}) + self.assertEqual('qwerqwer', retval) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_create_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.create, {'session': 'metadata'}) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_delete_ok(self, mock_requests): + self.mock_response.status_code = 204 + mock_requests.delete.return_value = self.mock_response + retval = self.session_manager.delete('test_session_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_delete_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.delete.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.delete, 'test_session_id') + + @patch('freezerclient.v1.managers.sessions.requests') + def test_get_ok(self, mock_requests): + self.mock_response.status_code = 200 + self.mock_response.json.return_value = {'session_id': 'qwerqwer'} + mock_requests.get.return_value = self.mock_response + retval = self.session_manager.get('test_session_id') + self.assertEqual({'session_id': 'qwerqwer'}, retval) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_get_raise_ApiClientException_when_api_return_error_different_from_404(self, mock_requests): + self.mock_response.status_code = 500 + mock_requests.get.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.get, 'test_session_id') + + @patch('freezerclient.v1.managers.sessions.requests') + def test_get_none(self, mock_requests): + self.mock_response.status_code = 404 + mock_requests.get.return_value = self.mock_response + retval = self.session_manager.get('test_session_id') + self.assertIsNone(retval) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_list_ok(self, mock_requests): + self.mock_response.status_code = 200 + session_list = [{'session_id_0': 'bomboloid'}, {'session_id_1': 'asdfasdf'}] + self.mock_response.json.return_value = {'sessions': session_list} + mock_requests.get.return_value = self.mock_response + retval = self.session_manager.list() + self.assertEqual(session_list, retval) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_list_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + self.mock_response.status_code = 404 + session_list = [{'session_id_0': 'bomboloid'}, {'session_id_1': 'asdfasdf'}] + self.mock_response.json.return_value = {'clients': session_list} + mock_requests.get.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.list) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_update_ok(self, mock_requests): + self.mock_response.status_code = 200 + self.mock_response.json.return_value = { + "patch": {"status": "bamboozled"}, + "version": 12, + "session_id": "d454beec-1f3c-4d11-aa1a-404116a40502" + } + mock_requests.patch.return_value = self.mock_response + retval = self.session_manager.update('d454beec-1f3c-4d11-aa1a-404116a40502', {'status': 'bamboozled'}) + self.assertEqual(12, retval) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_update_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + self.mock_response.json.return_value = { + "patch": {"status": "bamboozled"}, + "version": 12, + "session_id": "d454beec-1f3c-4d11-aa1a-404116a40502" + } + self.mock_response.status_code = 404 + self.mock_response.text = '{"title": "Not Found","description":"No document found with ID d454beec-1f3c-4d11-aa1a-404116a40502x"}' + mock_requests.patch.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.update, + 'd454beec-1f3c-4d11-aa1a-404116a40502', {'status': 'bamboozled'}) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_add_job_uses_proper_endpoint(self, mock_requests): + session_id, job_id = 'sessionqwerty1234', 'jobqwerty1234' + self.mock_response.status_code = 204 + mock_requests.put.return_value = self.mock_response + endpoint = '{0}{1}/jobs/{2}'.format(self.endpoint, session_id, job_id) + + retval = self.session_manager.add_job(session_id, job_id) + + self.assertIsNone(retval) + mock_requests.put.assert_called_with(endpoint, headers=self.headers, verify=True) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_add_job_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + session_id, job_id = 'sessionqwerty1234', 'jobqwerty1234' + self.mock_response.status_code = 500 + mock_requests.put.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.add_job, session_id, job_id) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_remove_job_uses_proper_endpoint(self, mock_requests): + session_id, job_id = 'sessionqwerty1234', 'jobqwerty1234' + self.mock_response.status_code = 204 + mock_requests.delete.return_value = self.mock_response + endpoint = '{0}{1}/jobs/{2}'.format(self.endpoint, session_id, job_id) + + retval = self.session_manager.remove_job(session_id, job_id) + + self.assertIsNone(retval) + mock_requests.delete.assert_called_with(endpoint, headers=self.headers, verify=True) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_remove_job_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + session_id, job_id = 'sessionqwerty1234', 'jobqwerty1234' + self.mock_response.status_code = 500 + mock_requests.delete.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.remove_job, session_id, job_id) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_start_session_posts_proper_data(self, mock_requests): + session_id, job_id, tag = 'sessionqwerty1234', 'jobqwerty1234', 23 + self.mock_response.status_code = 202 + self.mock_response.json.return_value = {'result': 'success', 'session_tag': 24} + mock_requests.post.return_value = self.mock_response + # /v1/sessions/{sessions_id}/action + endpoint = '{0}{1}/action'.format(self.endpoint, session_id) + data = {"start": {"current_tag": 23, "job_id": "jobqwerty1234"}} + retval = self.session_manager.start_session(session_id, job_id, tag) + self.assertEqual({'result': 'success', 'session_tag': 24}, retval) + + args = mock_requests.post.call_args[0] + kwargs = mock_requests.post.call_args[1] + self.assertEquals(endpoint, args[0]) + self.assertEquals(data, json.loads(kwargs['data'])) + self.assertEquals(self.headers, kwargs['headers']) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_start_session_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + session_id, job_id, tag = 'sessionqwerty1234', 'jobqwerty1234', 23 + self.mock_response.status_code = 500 + self.mock_response.json.return_value = {'result': 'success', 'session_tag': 24} + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.start_session, + session_id, job_id, tag) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_end_session_posts_proper_data(self, mock_requests): + session_id, job_id, tag = 'sessionqwerty1234', 'jobqwerty1234', 23 + self.mock_response.status_code = 202 + self.mock_response.json.return_value = {'result': 'success', 'session_tag': 24} + mock_requests.post.return_value = self.mock_response + # /v1/sessions/{sessions_id}/action + endpoint = '{0}{1}/action'.format(self.endpoint, session_id) + data = {"end": {"current_tag": 23, "job_id": "jobqwerty1234", "result": "fail"}} + retval = self.session_manager.end_session(session_id, job_id, tag, 'fail') + self.assertEqual({'result': 'success', 'session_tag': 24}, retval) + + args = mock_requests.post.call_args[0] + kwargs = mock_requests.post.call_args[1] + self.assertEquals(endpoint, args[0]) + self.assertEquals(data, json.loads(kwargs['data'])) + self.assertEquals(self.headers, kwargs['headers']) + + @patch('freezerclient.v1.managers.sessions.requests') + def test_end_session_raise_ApiClientException_when_api_return_error_code(self, mock_requests): + session_id, job_id, tag = 'sessionqwerty1234', 'jobqwerty1234', 23 + self.mock_response.status_code = 500 + self.mock_response.json.return_value = {'result': 'success', 'session_tag': 24} + mock_requests.post.return_value = self.mock_response + self.assertRaises(exceptions.ApiClientException, self.session_manager.end_session, + session_id, job_id, tag, 'fail') + diff --git a/freezerclient/utils.py b/freezerclient/utils.py new file mode 100644 index 0000000..aab4e42 --- /dev/null +++ b/freezerclient/utils.py @@ -0,0 +1,119 @@ +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +import json +import logging + + +logging = logging.getLogger(__name__) + + +class Namespace(dict): + """A dict subclass that exposes its items as attributes. + + Warning: Namespace instances do not have direct access to the + dict methods. + + """ + + def __init__(self, obj={}): + super(Namespace, self).__init__(obj) + + def __dir__(self): + return tuple(self) + + def __repr__(self): + return "%s(%s)" % (type(self).__name__, + super(Namespace, self).__repr__()) + + def __getattribute__(self, name): + try: + return self[name] + except KeyError: + # Return None in case the value doesn't exists + # this is not an issue for the apiclient because it skips + # None values + return None + + def __setattr__(self, name, value): + self[name] = value + + def __delattr__(self, name): + del self[name] + + @classmethod + def from_object(cls, obj, names=None): + if names is None: + names = dir(obj) + ns = {name:getattr(obj, name) for name in names} + return cls(ns) + + @classmethod + def from_mapping(cls, ns, names=None): + if names: + ns = {name: ns[name] for name in names} + return cls(ns) + + @classmethod + def from_sequence(cls, seq, names=None): + if names: + seq = {name: val for name, val in seq if name in names} + return cls(seq) + + @staticmethod + def hasattr(ns, name): + try: + object.__getattribute__(ns, name) + except AttributeError: + return False + return True + + @staticmethod + def getattr(ns, name): + return object.__getattribute__(ns, name) + + @staticmethod + def setattr(ns, name, value): + return object.__setattr__(ns, name, value) + + @staticmethod + def delattr(ns, name): + return object.__delattr__(ns, name) + + +class CachedProperty(object): + + def __init__(self, func): + self.__doc__ = getattr(func, '__doc__') + self.func = func + + def __get__(self, obj, cls): + if obj is None: + return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value + + +def doc_from_json_file(path_to_file): + """Build a json from a file in the file system + :param path_to_file: path to file + :return: in memory file in json format + """ + with open(path_to_file, 'rb') as fd: + try: + return json.load(fd) + except Exception as err: + logging.error(err) + raise Exception('Unable to load conf file. {0}'.format(err)) diff --git a/freezerclient/v1/__init__.py b/freezerclient/v1/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/freezerclient/v1/actions.py b/freezerclient/v1/actions.py new file mode 100644 index 0000000..718c14c --- /dev/null +++ b/freezerclient/v1/actions.py @@ -0,0 +1,153 @@ +# (c) Copyright 2014-2016 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 logging + +from cliff.command import Command +from cliff.lister import Lister +from cliff.show import ShowOne + +from freezerclient import exceptions +from freezerclient.utils import doc_from_json_file + + +logging = logging.getLogger(__name__) + + +class ActionShow(ShowOne): + """Show a single action """ + def get_parser(self, prog_name): + parser = super(ActionShow, self).get_parser(prog_name) + parser.add_argument(dest='action_id', + help='ID of the action') + return parser + + def take_action(self, parsed_args): + action = self.app.client.actions.get(parsed_args.action_id) + + if not action: + raise exceptions.ApiClientException('Action not found') + + column = ( + 'Action ID', + 'Name', + 'Action', + 'Mode', + 'Path to Backup or Restore', + 'Storage', + 'Snapshot' + ) + + data = ( + action.get('action_id'), + action.get('freezer_action', {}).get('backup_name', ''), + action.get('freezer_action', {}).get('action', 'backup'), + action.get('freezer_action', {}).get('mode', 'fs'), + action.get('freezer_action', {}).get('path_to_backup', ''), + action.get('freezer_action', {}).get('storage', 'swift'), + action.get('freezer_action', {}).get('snapshot', 'False'), + ) + + return column, data + + +class ActionList(Lister): + """List all actions for your user""" + def get_parser(self, prog_name): + parser = super(ActionList, self).get_parser(prog_name) + + parser.add_argument( + '--limit', + dest='limit', + default=100, + help='Specify a limit for search query', + ) + + parser.add_argument( + '--offset', + dest='offset', + default=0, + help='', + ) + + parser.add_argument( + '--search', + dest='search', + default='', + help='Define a filter for the query', + ) + return parser + + def take_action(self, parsed_args): + actions = self.app.client.actions.list( + limit=parsed_args.limit, + offset=parsed_args.offset, + search=parsed_args.search + ) + + return (('Action ID', 'Name', 'Action', + 'Path to Backup or Restore', 'Mode', 'Storage', 'snapshot'), + ((action.get('action_id'), + action.get('freezer_action', {}).get('backup_name', ''), + action.get('freezer_action', {}).get('action', 'backup'), + action.get('freezer_action', {}).get('path_to_backup', ''), + action.get('freezer_action', {}).get('mode', 'fs'), + action.get('freezer_action', {}).get('storage', 'swift'), + action.get('freezer_action', {}).get('snapshot', 'False') + ) for action in actions)) + + +class ActionDelete(Command): + """Delete an action from the api""" + def get_parser(self, prog_name): + parser = super(ActionDelete, self).get_parser(prog_name) + parser.add_argument(dest='action_id', + help='ID of the action') + return parser + + def take_action(self, parsed_args): + self.app.client.actions.delete(parsed_args.action_id) + logging.info('Action {0} deleted'.format(parsed_args.action_id)) + + +class ActionCreate(Command): + """Create an action from a file""" + def get_parser(self, prog_name): + parser = super(ActionCreate, self).get_parser(prog_name) + parser.add_argument('--file', + dest='file', + help='Path to json file with the action') + return parser + + def take_action(self, parsed_args): + action = doc_from_json_file(parsed_args.file) + action_id = self.app.client.actions.create(action) + logging.info('Action {0} created'.format(action_id)) + + +class ActionUpdate(Command): + """Update an action from a file""" + def get_parser(self, prog_name): + parser = super(ActionUpdate, self).get_parser(prog_name) + parser.add_argument(dest='action_id', + help='ID of the session') + + parser.add_argument(dest='file', + help='Path to json file with the action') + return parser + + def take_action(self, parsed_args): + action = doc_from_json_file(parsed_args.file) + self.app.client.actions.update(parsed_args.action_id, action) + logging.info('Action {0} updated'.format(parsed_args.action_id)) diff --git a/freezerclient/v1/backups.py b/freezerclient/v1/backups.py new file mode 100644 index 0000000..40db38d --- /dev/null +++ b/freezerclient/v1/backups.py @@ -0,0 +1,98 @@ +# (c) Copyright 2014-2016 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 logging + +from pprint import pformat + +from cliff.lister import Lister +from cliff.show import ShowOne + +from freezerclient import exceptions + + +logging = logging.getLogger(__name__) + + +class BackupShow(ShowOne): + """Show the metadata of a single backup""" + def get_parser(self, prog_name): + parser = super(BackupShow, self).get_parser(prog_name) + parser.add_argument(dest='backup_uuid', + help='UUID of the backup') + return parser + + def take_action(self, parsed_args): + # due to the fact that a backup_id is composed of several strings + # some of them may include a slash "/" so it will never find the correct + # backup, so the workaround for this version is to use the backup_uuid as + # a filter for the search. this won't work when the user wants to delete a + # backup, but that functionality is yet to be provided by the api. + search = {"match": [{"backup_uuid": parsed_args.backup_uuid}, ], } + backup = self.app.client.backups.list(search=search) + + if not backup: + raise exceptions.ApiClientException('Backup not found') + + backup = backup[0] + + column = ( + 'Backup ID', + 'Backup UUID', + 'Metadata' + ) + data = ( + backup.get('backup_id'), + backup.get('backup_uuid'), + pformat(backup.get('backup_metadata')) + ) + return column, data + + +class BackupList(Lister): + """List all backups for your user""" + def get_parser(self, prog_name): + parser = super(BackupList, self).get_parser(prog_name) + + parser.add_argument( + '--limit', + dest='limit', + default=100, + help='Specify a limit for search query', + ) + + parser.add_argument( + '--offset', + dest='offset', + default=0, + help='', + ) + + parser.add_argument( + '--search', + dest='search', + default='', + help='Define a filter for the query', + ) + return parser + + def take_action(self, parsed_args): + backups = self.app.client.backups.list(limit=parsed_args.limit, + offset=parsed_args.offset, + search=parsed_args.search) + return (('Backup ID', 'Backup UUID'), + ((backup.get('backup_id'), + backup.get('backup_uuid'), + ) for backup in backups)) + diff --git a/freezerclient/v1/client.py b/freezerclient/v1/client.py new file mode 100644 index 0000000..8d9b9a6 --- /dev/null +++ b/freezerclient/v1/client.py @@ -0,0 +1,147 @@ +# (c) Copyright 2014-2016 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 socket + +from freezerclient.utils import CachedProperty +from freezerclient.utils import Namespace +from freezerclient.v1.managers import actions +from freezerclient.v1.managers import backups +from freezerclient.v1.managers import clients +from freezerclient.v1.managers import jobs +from freezerclient.v1.managers import sessions + +from keystoneclient.auth.identity import v2 +from keystoneclient.auth.identity import v3 +from keystoneclient import session as ksc_session + + +def guess_auth_version(opts): + """ Guess keystone version to connect to""" + if opts.os_identity_api_version == '3': + return '3' + elif opts.os_identity_api_version == '2.0': + return '2.0' + elif opts.os_auth_url.endswith('v3'): + return '3' + elif opts.os_auth_url.endswith('v2.0'): + return '2.0' + raise Exception('Please provide valid keystone auth url with valid' + ' keystone api version to use') + + +def get_auth_plugin(opts): + """Create the right keystone connection depending on the version + for the api, if username/password and token are provided, username and + password takes precedence. + """ + auth_version = guess_auth_version(opts) + if opts.os_username: + if auth_version == '3': + return v3.Password(auth_url=opts.os_auth_url, + username=opts.os_username, + password=opts.os_password, + project_name=opts.os_project_name, + user_domain_name=opts.os_user_domain_name, + project_domain_name=opts.os_project_domain_name) + elif auth_version == '2.0': + return v2.Password(auth_url=opts.os_auth_url, + username=opts.os_username, + password=opts.os_password, + tenant_name=opts.os_tenant_name) + elif opts.os_token: + if auth_version == '3': + return v3.Token(auth_url=opts.os_auth_url, + token=opts.os_token, + project_name=opts.os_project_name, + project_domain_name=opts.os_project_domain_name) + elif auth_version == '2.0': + return v2.Token(auth_url=opts.os_auth_url, + token=opts.os_token, + tenant_name=opts.os_tenant_name) + raise Exception('Unable to determine correct auth method, please provide' + ' either username or token') + + +class Client(object): + """Client for the OpenStack Disaster Recovery v1 API. + """ + + def __init__(self, version='3', token=None, username=None, password=None, + tenant_name=None, auth_url=None, session=None, endpoint=None, + opts=None, project_name=None, user_domain_name=None, + project_domain_name=None, verify=True, cert=None): + """ + Initialize a new client for the Disaster Recovery v1 API. + :param version: keystone version to use + :param token: keystone token + :param username: openstack username + :param password: openstack password + :param tenant_name: tenant + :param auth_url: keystone-api endpoint + :param session: keystone.Session + :param endpoint: freezer-api endpoint + :param opts: a namespace to store all keystone data + :param project_name: only for version 3 + :param user_domain_name: only for version 3 + :param project_domain_name: only for version 3 + :param verify: The verification arguments to pass to requests. + These are of the same form as requests expects, + so True or False to verify (or not) against system + certificates or a path to a bundle or CA certs to + check against or None for requests to + attempt to locate and use certificates. (optional, + defaults to True) + :param cert: Path to cert + :return: freezerclient.Client + """ + self.opts = opts or Namespace({}) + self.opts.os_token = token or None + self.opts.os_username = username or None + self.opts.os_password = password or None + self.opts.os_tenant_name = tenant_name or None + self.opts.os_auth_url = auth_url or None + self.opts.os_backup_url = endpoint or None + self.opts.os_project_name = project_name or None + self.opts.os_user_domain_name = user_domain_name or None + self.opts.os_project_domain_name = project_domain_name or None + self.opts.auth_version = version + self.verify = verify + self.cert = cert + self._session = session + self.endpoint = endpoint + + self.jobs = jobs.JobManager(self, verify=verify) + self.clients = clients.ClientManager(self, verify=verify) + self.backups = backups.BackupsManager(self, verify=verify) + self.sessions = sessions.SessionManager(self, verify=verify) + self.actions = actions.ActionManager(self, verify=verify) + + @CachedProperty + def session(self): + if self._session: + return self._session + auth_plugin = get_auth_plugin(self.opts) + return ksc_session.Session(auth=auth_plugin, + verify=self.verify, + cert=self.cert) + + @property + def auth_token(self): + return self.session.get_token() + + @CachedProperty + def client_id(self): + return '{0}_{1}'.format(self.session.get_project_id(), + socket.gethostname()) diff --git a/freezerclient/v1/clients.py b/freezerclient/v1/clients.py new file mode 100644 index 0000000..9a0e265 --- /dev/null +++ b/freezerclient/v1/clients.py @@ -0,0 +1,127 @@ +# (c) Copyright 2014-2016 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 logging + +from cliff.command import Command +from cliff.lister import Lister +from cliff.show import ShowOne + +from freezerclient import exceptions +from freezerclient.utils import doc_from_json_file + + +logging = logging.getLogger(__name__) + + +class ClientShow(ShowOne): + """Show a single client""" + def get_parser(self, prog_name): + parser = super(ClientShow, self).get_parser(prog_name) + parser.add_argument(dest='client_id', + help='ID of the client') + return parser + + def take_action(self, parsed_args): + client = self.app.client.clients.get(parsed_args.client_id) + + if not client: + raise exceptions.ApiClientException('Client not found') + + column = ( + 'Client ID', + 'Client UUID', + 'hostname', + 'description' + ) + data = ( + client.get('client', {}).get('client_id'), + client.get('uuid'), + client.get('client', {}).get('hostname'), + client.get('client', {}).get('description', '') + ) + + return column, data + + +class ClientList(Lister): + """List of clients registered in the api""" + def get_parser(self, prog_name): + parser = super(ClientList, self).get_parser(prog_name) + + parser.add_argument( + '--limit', + dest='limit', + default=100, + help='Specify a limit for search query', + ) + + parser.add_argument( + '--offset', + dest='offset', + default=0, + help='', + ) + + parser.add_argument( + '--search', + dest='search', + default='', + help='Define a filter for the query', + ) + return parser + + def take_action(self, parsed_args): + clients = self.app.client.clients.list(limit=parsed_args.limit, + offset=parsed_args.offset, + search=parsed_args.search) + + return (('Client ID', 'uuid', 'hostname', 'description'), + ((client.get('client', {}).get('client_id'), + client.get('uuid'), + client.get('client', {}).get('hostname'), + client.get('client', {}).get('description', '') + ) for client in clients)) + + +class ClientDelete(Command): + """Delete a client from the api""" + def get_parser(self, prog_name): + parser = super(ClientDelete, self).get_parser(prog_name) + parser.add_argument(dest='client_id', + help='ID of the client') + return parser + + def take_action(self, parsed_args): + self.app.client.clients.delete(parsed_args.client_id) + logging.info('Client {0} deleted'.format(parsed_args.client_id)) + + +class ClientRegister(Command): + """Register a new client""" + def get_parser(self, prog_name): + parser = super(ClientRegister, self).get_parser(prog_name) + parser.add_argument('--file', + dest='file', + help='Path to json file with the client') + return parser + + def take_action(self, parsed_args): + client = doc_from_json_file(parsed_args.file) + try: + client_id = self.app.client.clients.create(client) + except Exception as err: + raise exceptions.ApiClientException(err.message) + else: + logging.info("Client {0} registered".format(client_id)) diff --git a/freezerclient/v1/jobs.py b/freezerclient/v1/jobs.py new file mode 100644 index 0000000..b16c63b --- /dev/null +++ b/freezerclient/v1/jobs.py @@ -0,0 +1,221 @@ +# (c) Copyright 2014-2016 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 logging + +from pprint import pformat +from pprint import pprint + +from cliff.command import Command +from cliff.lister import Lister +from cliff.show import ShowOne + +from freezerclient import exceptions +from freezerclient.utils import doc_from_json_file + + +logging = logging.getLogger(__name__) + + +class JobShow(ShowOne): + """Show a single job""" + def get_parser(self, prog_name): + parser = super(JobShow, self).get_parser(prog_name) + parser.add_argument(dest='job_id', + help='ID of the job') + return parser + + def take_action(self, parsed_args): + job = self.app.client.jobs.get(parsed_args.job_id) + + if not job: + raise exceptions.ApiClientException('Job not found') + + column = ( + 'Job ID', + 'Client ID', + 'User ID', + 'Session ID', + 'Description', + 'Actions', + 'Start Date', + 'End Date', + 'Interval', + ) + data = ( + job.get('job_id'), + job.get('client_id'), + job.get('user_id'), + job.get('session_id', ''), + job.get('description'), + pformat(job.get('job_actions')), + job.get('job_schedule', {}).get('schedule_start_date', ''), + job.get('job_schedule', {}).get('schedule_interval', ''), + job.get('job_schedule', {}).get('schedule_end_date', ''), + ) + return column, data + + +class JobList(Lister): + """List all the jobs for your user""" + def get_parser(self, prog_name): + parser = super(JobList, self).get_parser(prog_name) + + parser.add_argument( + '--limit', + dest='limit', + default=100, + help='Specify a limit for search query', + ) + + parser.add_argument( + '--offset', + dest='offset', + default=0, + help='', + ) + + parser.add_argument( + '--search', + dest='search', + default='', + help='Define a filter for the query', + ) + return parser + + def take_action(self, parsed_args): + jobs = self.app.client.jobs.list_all( + limit=parsed_args.limit, + offset=parsed_args.offset, + search=parsed_args.search + ) + + return (('Job ID', 'Description', '# Actions', 'Result', 'Event', 'Session ID'), + ((job.get('job_id'), + job.get('description'), + len(job.get('job_actions', [])), + job.get('job_schedule', {}).get('result', ''), + job.get('job_schedule', {}).get('event', ''), + job.get('session_id', '') + ) for job in jobs)) + + +class JobGet(Command): + """Download a job as a json file""" + def get_parser(self, prog_name): + parser = super(JobGet, self).get_parser(prog_name) + parser.add_argument(dest='job_id', + help='ID of the job') + + parser.add_argument('--no-format', + dest='no_format', + default=False, + action='store_true', + help='Return a job in json without pretty print') + return parser + + def take_action(self, parsed_args): + job = self.app.client.jobs.get(parsed_args.job_id) + + if not job: + raise exceptions.ApiClientException('Job not found') + + if parsed_args.no_format: + print(job) + else: + pprint(job) + + +class JobDelete(Command): + """Delete a job from the api""" + def get_parser(self, prog_name): + parser = super(JobDelete, self).get_parser(prog_name) + parser.add_argument(dest='job_id', + help='ID of the job') + return parser + + def take_action(self, parsed_args): + self.app.client.jobs.delete(parsed_args.job_id) + logging.info('Job {0} deleted'.format(parsed_args.job_id)) + + +class JobCreate(Command): + """Create a new job from a file""" + def get_parser(self, prog_name): + parser = super(JobCreate, self).get_parser(prog_name) + parser.add_argument('--file', + dest='file', + help='Path to json file with the job') + return parser + + def take_action(self, parsed_args): + job = doc_from_json_file(parsed_args.file) + job_id = self.app.client.jobs.create(job) + logging.info('Job {0} created'.format(job_id)) + + +class JobStart(Command): + """Send a start signal for a job""" + def get_parser(self, prog_name): + parser = super(JobStart, self).get_parser(prog_name) + parser.add_argument(dest='job_id', + help='ID of the job') + return parser + + def take_action(self, parsed_args): + self.app.client.jobs.start_job(parsed_args.job_id) + logging.info('Job {0} has started'.format(parsed_args.job_id)) + + +class JobStop(Command): + """Send a stop signal for a job""" + def get_parser(self, prog_name): + parser = super(JobStop, self).get_parser(prog_name) + parser.add_argument(dest='job_id', + help='ID of the job') + return parser + + def take_action(self, parsed_args): + self.app.client.jobs.stop_job(parsed_args.job_id) + logging.info('Job {0} has stopped'.format(parsed_args.job_id)) + + +class JobAbort(Command): + """Abort a running job""" + def get_parser(self, prog_name): + parser = super(JobAbort, self).get_parser(prog_name) + parser.add_argument(dest='job_id', + help='ID of the job') + return parser + + def take_action(self, parsed_args): + self.app.client.jobs.abort_job(parsed_args.job_id) + logging.info('Job {0} has been aborted'.format(parsed_args.job_id)) + + +class JobUpdate(Command): + """Update a job from a file""" + def get_parser(self, prog_name): + parser = super(JobUpdate, self).get_parser(prog_name) + parser.add_argument(dest='job_id', + help='ID of the job') + + parser.add_argument(dest='file', + help='Path to json file with the job') + return parser + + def take_action(self, parsed_args): + job = doc_from_json_file(parsed_args.file) + self.app.client.jobs.update(parsed_args.job_id, job) + logging.info('Job {0} updated'.format(parsed_args.job_id)) diff --git a/freezerclient/v1/managers/__init__.py b/freezerclient/v1/managers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/actions.py b/freezerclient/v1/managers/actions.py similarity index 76% rename from actions.py rename to freezerclient/v1/managers/actions.py index 405da78..1704d2e 100644 --- a/actions.py +++ b/freezerclient/v1/managers/actions.py @@ -1,24 +1,21 @@ -""" -Copyright 2015 Hewlett-Packard - -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. - -""" +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json import requests -from freezer.apiclient import exceptions +from freezerclient import exceptions class ActionManager(object): diff --git a/backups.py b/freezerclient/v1/managers/backups.py similarity index 76% rename from backups.py rename to freezerclient/v1/managers/backups.py index 956fbfa..bc598bf 100644 --- a/backups.py +++ b/freezerclient/v1/managers/backups.py @@ -1,23 +1,21 @@ -""" -(c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json import requests -from freezer.apiclient import exceptions +from freezerclient import exceptions class BackupsManager(object): diff --git a/registration.py b/freezerclient/v1/managers/clients.py similarity index 77% rename from registration.py rename to freezerclient/v1/managers/clients.py index fb28a46..c79d28a 100644 --- a/registration.py +++ b/freezerclient/v1/managers/clients.py @@ -1,26 +1,24 @@ -""" -(c) Copyright 2014,2015 Hewlett-Packard Development Company, L.P. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -""" +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json import requests -from freezer.apiclient import exceptions +from freezerclient import exceptions -class RegistrationManager(object): +class ClientManager(object): def __init__(self, client, verify=True): self.client = client diff --git a/jobs.py b/freezerclient/v1/managers/jobs.py similarity index 87% rename from jobs.py rename to freezerclient/v1/managers/jobs.py index f66f17e..98a490f 100644 --- a/jobs.py +++ b/freezerclient/v1/managers/jobs.py @@ -1,23 +1,21 @@ -""" -Copyright 2015 Hewlett-Packard - -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. -""" +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json import requests -from freezer.apiclient import exceptions +from freezerclient import exceptions class JobManager(object): diff --git a/sessions.py b/freezerclient/v1/managers/sessions.py similarity index 88% rename from sessions.py rename to freezerclient/v1/managers/sessions.py index 7e00783..4645d9e 100644 --- a/sessions.py +++ b/freezerclient/v1/managers/sessions.py @@ -1,23 +1,21 @@ -""" -Copyright 2015 Hewlett-Packard - -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. -""" +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json import requests -from freezer.apiclient import exceptions +from freezerclient import exceptions class SessionManager(object): @@ -138,7 +136,7 @@ class SessionManager(object): def end_session(self, session_id, job_id, session_tag, result): """ Informs the freezer service that the job has ended. - Privides information about the job's result and the session tag + Provides information about the job's result and the session tag :param session_id: :param job_id: diff --git a/freezerclient/v1/sessions.py b/freezerclient/v1/sessions.py new file mode 100644 index 0000000..d83494f --- /dev/null +++ b/freezerclient/v1/sessions.py @@ -0,0 +1,198 @@ +# (c) Copyright 2014-2016 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 logging + +from pprint import pformat + +from cliff.command import Command +from cliff.lister import Lister +from cliff.show import ShowOne + +from freezerclient import exceptions +from freezerclient.utils import doc_from_json_file + + +logging = logging.getLogger(__name__) + + +class SessionShow(ShowOne): + """Show a single session""" + def get_parser(self, prog_name): + parser = super(SessionShow, self).get_parser(prog_name) + parser.add_argument(dest='session_id', + help='ID of the session') + return parser + + def take_action(self, parsed_args): + session = self.app.client.sessions.get(parsed_args.session_id) + + if not session: + raise exceptions.ApiClientException('Session not found') + + column = ( + 'Session ID', + 'Description', + 'Status', + 'Jobs' + ) + + data = ( + session.get('session_id'), + session.get('description'), + session.get('status'), + pformat(session.get('jobs')) + ) + return column, data + + +class SessionList(Lister): + """List all the sessions for your user""" + def get_parser(self, prog_name): + parser = super(SessionList, self).get_parser(prog_name) + + parser.add_argument( + '--limit', + dest='limit', + default=100, + help='Specify a limit for search query', + ) + + parser.add_argument( + '--offset', + dest='offset', + default=0, + help='', + ) + + parser.add_argument( + '--search', + dest='search', + default='', + help='Define a filter for the query', + ) + return parser + + def take_action(self, parsed_args): + sessions = self.app.client.sessions.list_all( + limit=parsed_args.limit, + offset=parsed_args.offset, + search=parsed_args.search + ) + + return (('Session ID', 'Description', 'Status', '# Jobs'), + ((session.get('session_id'), + session.get('description'), + session.get('status'), + len(session.get('jobs', [])), + ) for session in sessions)) + + +class SessionCreate(Command): + """Create a session from a file""" + def get_parser(self, prog_name): + parser = super(SessionCreate, self).get_parser(prog_name) + parser.add_argument('--file', + dest='file', + help='Path to json file with the job') + return parser + + def take_action(self, parsed_args): + session = doc_from_json_file(parsed_args.file) + session_id = self.app.client.sessions.create(session) + logging.info('Session {0} created'.format(session_id)) + + +class SessionAddJob(Command): + """Add a job to a session""" + def get_parser(self, prog_name): + parser = super(SessionAddJob, self).get_parser(prog_name) + parser.add_argument('--session-id', + dest='session_id', + help='ID of the session') + parser.add_argument('--job-id', + dest='job_id', + help='ID of the job to add') + return parser + + def take_action(self, parsed_args): + self.app.client.sessions.add_job(parsed_args.session_id, + parsed_args.job_id) + logging.info('Job {0} added correctly to session {1}'.format( + parsed_args.job_id, parsed_args.session_id)) + + +class SessionRemoveJob(Command): + """Remove a job from a session""" + def get_parser(self, prog_name): + parser = super(SessionRemoveJob, self).get_parser(prog_name) + parser.add_argument('--session-id', + dest='session_id', + help='ID of the session') + parser.add_argument('--job-id', + dest='job_id', + help='ID of the job to add') + return parser + + def take_action(self, parsed_args): + try: + self.app.client.sessions.remove_job(parsed_args.session_id, + parsed_args.job_id) + except Exception as error: + # there is an error coming from the api when a job is removed + # with the following text: + # Additional properties are not allowed ('job_event' was unexpected) + # but in reality the job gets removed correctly. + if 'Additional properties are not allowed' in error.message: + pass + else: + raise exceptions.ApiClientException(error.message) + else: + logging.info('Job {0} removed correctly from session {1}'.format( + parsed_args.job_id, parsed_args.session_id)) + + +class SessionStart(Command): + """Start a session""" + def get_parser(self, prog_name): + pass + + def take_action(self, parsed_args): + pass + + +class SessionEnd(Command): + """Stop a session""" + def get_parser(self, prog_name): + pass + + def take_action(self, parsed_args): + pass + + +class SessionUpdate(Command): + """Update a session from a file""" + def get_parser(self, prog_name): + parser = super(SessionUpdate, self).get_parser(prog_name) + parser.add_argument(dest='session_id', + help='ID of the session') + + parser.add_argument(dest='file', + help='Path to json file with the session') + return parser + + def take_action(self, parsed_args): + session = doc_from_json_file(parsed_args.file) + self.app.client.sessions.update(parsed_args.session_id, session) + logging.info('Session {0} updated'.format(parsed_args.session_id)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..085fba7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +astroid<1.4.0 # breaks pylint 1.4.4 +setuptools>=16.0 +pbr>=1.6 +python-keystoneclient>=1.6.0,!=1.8.0 + +cliff!=1.16.0,>=1.15.0 # Apache-2.0 +oslo.utils>=3.2.0 +oslo.i18n>=1.5.0 # Apache-2.0 +oslo.log>=1.14.0 +oslo.config>=3.2.0 # Apache-2.0 + +six>=1.9.0 # MIT diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..82b533f --- /dev/null +++ b/setup.cfg @@ -0,0 +1,49 @@ +[metadata] +name = python-freezerclient +summary = OpenStack Disaster Recovery API Client Library +description-file = + README.rst +license = Apache License, Version 2.0 +author = Freezer Team +author-email = openstack-dev@lists.openstack.org +home-page = https://wiki.openstack.org/wiki/Freezer +classifier = + Programming Language :: Python + Development Status :: 5 - Production/Stable + Natural Language :: English + Environment :: OpenStack + Intended Audience :: Developers + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: MacOS + Operating System :: POSIX :: BSD :: FreeBSD + Operating System :: POSIX :: BSD :: NetBSD + Operating System :: POSIX :: BSD :: OpenBSD + Operating System :: POSIX :: Linux + Operating System :: Microsoft :: Windows + Operating System :: Unix + Topic :: System :: Archiving :: Backup + Topic :: System :: Archiving :: Compression + Topic :: System :: Archiving + +[files] +packages = + freezerclient + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[entry_points] +console_scripts = + freezer = freezerclient.shell:main + +[pbr] +# Have pbr generate the module indexes like sphinx autodoc +autodoc_index_modules = True + +# Treat sphinx warnings as errors during the docs build; this helps us keep +# the documentation clean. +warnerrors = true diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..47d3180 --- /dev/null +++ b/setup.py @@ -0,0 +1,28 @@ +# (c) Copyright 2014-2016 Hewlett-Packard Development Company, L.P. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT +import setuptools + +# In python < 2.7.4, a lazy loading of package `pbr` will break +# setuptools if some other modules registered functions in `atexit`. +# solution from: http://bugs.python.org/issue15881#msg170215 +try: + import multiprocessing # noqa +except ImportError: + pass + +setuptools.setup( + setup_requires=['pbr'], + pbr=True) diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..b830d53 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,11 @@ +flake8>=2.2.4,<=2.4.1 +hacking>=0.10.2,<0.11 +coverage>=3.6 +discover +mock>=1.2 +pylint==1.4.5 # GNU GPL v2 +python-subunit>=0.0.18 +sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3 # BSD +oslosphinx>=2.5.0,!=3.4.0 # Apache-2.0 +testrepository>=0.0.18 +testtools>=1.4.0 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..7950186 --- /dev/null +++ b/tox.ini @@ -0,0 +1,67 @@ +[tox] +envlist = py27,py34,pep8,pylint,docs +skipsdist = True + +[testenv] +usedevelop = True +deps = + -r{toxinidir}/requirements.txt + -r{toxinidir}/test-requirements.txt + +install_command = pip install -U {opts} {packages} +setenv = VIRTUAL_ENV={envdir} + +whitelist_externals = + find + coverage + rm + +python_files = test_*.py +norecursedirs = .tox .venv + +[testenv:venv] +commands = {posargs} + +[testenv:py27] +basepython = python2.7 +setenv = + OS_TEST_PATH = ./freezerclient/tests/unit +commands = + find . -type f -name "*.pyc" -delete + python setup.py testr --coverage --testr-args="{posargs}" + coverage report -m + rm -f .coverage + rm -rf .testrepository + +[testenv:py34] +basepython = python3.4 +setenv = + OS_TEST_PATH = ./freezerclient/tests/unit +commands = + find . -type f -name "*.pyc" -delete + python setup.py testr --coverage --testr-args="{posargs}" + coverage report -m + rm -f .coverage + rm -rf .testrepository + +[testenv:docs] +commands = + python setup.py build_sphinx + +[testenv:pep8] +commands = flake8 freezerclient + +[testenv:pylint] +commands = pylint --rcfile .pylintrc freezerclient + +[flake8] +# it's not a bug that we aren't using all of hacking +# H102 -> apache2 license exists +# H103 -> license is apache +# H201 -> no bare excepts +# H501 -> don't use locals() for str formatting +# H903 -> \n not \r\n +ignore = H +select = H102, H103, H201, H501, H903, H201, H306, H301, H233 +show-source = True +exclude = .venv,.tox,dist,doc,test,*egg,tests