From 8752149f66c1a6a2168791d04f4e36b333b299ed Mon Sep 17 00:00:00 2001 From: sijeesh Date: Wed, 4 Dec 2019 20:18:32 +0530 Subject: [PATCH] Initial commit --- .gitignore | 69 ++++ CHANGELOG.md | 11 + CONTRIBUTING.md | 74 ++++ LICENSE | 2 +- README.md | 176 ++++++++++ deploy.sh | 69 ++++ docs/source/conf.py | 179 ++++++++++ docs/source/index.rst | 20 ++ endpoints-support.md | 28 ++ examples/backups.py | 64 ++++ examples/config-rename.json | 7 + examples/datastores.py | 67 ++++ examples/hosts.py | 67 ++++ examples/omnistack_clusters.py | 67 ++++ examples/policies.py | 72 ++++ examples/virtual_machines.py | 152 ++++++++ setup.cfg | 2 + setup.py | 30 ++ simplivity/__init__.py | 3 + simplivity/connection.py | 252 ++++++++++++++ simplivity/exceptions.py | 94 +++++ simplivity/ovc_client.py | 175 ++++++++++ simplivity/resources/__init__.py | 0 simplivity/resources/backups.py | 134 ++++++++ simplivity/resources/datastores.py | 115 +++++++ simplivity/resources/hosts.py | 153 +++++++++ simplivity/resources/omnistack_clusters.py | 112 ++++++ simplivity/resources/policies.py | 100 ++++++ simplivity/resources/resource.py | 307 +++++++++++++++++ simplivity/resources/tasks.py | 141 ++++++++ simplivity/resources/virtual_machines.py | 325 ++++++++++++++++++ test_requirements.txt | 6 + tests/__init__.py | 0 tests/unit/__init__.py | 0 tests/unit/resources/test_backups.py | 95 +++++ tests/unit/resources/test_datastores.py | 95 +++++ tests/unit/resources/test_hosts.py | 95 +++++ .../unit/resources/test_omnistack_clusters.py | 95 +++++ tests/unit/resources/test_policies.py | 95 +++++ tests/unit/resources/test_resource.py | 137 ++++++++ tests/unit/resources/test_tasks.py | 98 ++++++ tests/unit/resources/test_virtual_machines.py | 278 +++++++++++++++ tests/unit/test_connection.py | 259 ++++++++++++++ tests/unit/test_exceptions.py | 112 ++++++ tests/unit/test_ovc_client.py | 145 ++++++++ tox.ini | 54 +++ 46 files changed, 4630 insertions(+), 1 deletion(-) create mode 100644 .gitignore create mode 100755 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100755 deploy.sh create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100755 endpoints-support.md create mode 100644 examples/backups.py create mode 100644 examples/config-rename.json create mode 100644 examples/datastores.py create mode 100644 examples/hosts.py create mode 100644 examples/omnistack_clusters.py create mode 100644 examples/policies.py create mode 100644 examples/virtual_machines.py create mode 100644 setup.cfg create mode 100644 setup.py create mode 100644 simplivity/__init__.py create mode 100644 simplivity/connection.py create mode 100644 simplivity/exceptions.py create mode 100755 simplivity/ovc_client.py create mode 100644 simplivity/resources/__init__.py create mode 100644 simplivity/resources/backups.py create mode 100644 simplivity/resources/datastores.py create mode 100644 simplivity/resources/hosts.py create mode 100644 simplivity/resources/omnistack_clusters.py create mode 100644 simplivity/resources/policies.py create mode 100755 simplivity/resources/resource.py create mode 100644 simplivity/resources/tasks.py create mode 100644 simplivity/resources/virtual_machines.py create mode 100644 test_requirements.txt create mode 100644 tests/__init__.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/resources/test_backups.py create mode 100644 tests/unit/resources/test_datastores.py create mode 100644 tests/unit/resources/test_hosts.py create mode 100644 tests/unit/resources/test_omnistack_clusters.py create mode 100644 tests/unit/resources/test_policies.py create mode 100644 tests/unit/resources/test_resource.py create mode 100644 tests/unit/resources/test_tasks.py create mode 100644 tests/unit/resources/test_virtual_machines.py create mode 100644 tests/unit/test_connection.py create mode 100644 tests/unit/test_exceptions.py create mode 100755 tests/unit/test_ovc_client.py create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2386fcf --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +*.py[cod] + +# C extensions +*.so + +# Local Certificates +*.pem + +# Packages +*.egg +*.egg-info +dist +build/* +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ + +# Rope +.ropeproject/ + + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml +htmlcov + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +#coverage +coverage.xml +.coverage +htmlcov + +#virtualenv +env + +#pycharm +.idea +.idea/ + +#Global configuration to run examples +examples/config.json + +#sphinx generated files +docs/build/* +docs/source/* +!docs/source/conf.py +!docs/source/index.rst +.vscode + +#certificate files +*.crt diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100755 index 0000000..1721ccc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# v1.0.0 +#### Notes +This is the first release of the SimpliVity Python SDK and it adds support for the below features. + +#### Features supported + - Backup + - Datastore + - Host + - OmniStack cluster + - Policy + - Virtual machine diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..23e2316 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,74 @@ +# Contribution Guide + +We welcome and encourage community contributions to simplivity-python. + +## Contributing + +The best way to directly collaborate with the project contributors is through GitHub: + +* If you want to contribute to our code by either fixing a problem or creating a new feature, please open a GitHub pull request. +* If you want to raise an issue such as a defect, an enhancement request or a general issue, please open a GitHub issue. + +Before you start to code, we recommend discussing your plans through a GitHub issue, especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give you feedback on your design, and help you find out if someone else is working on the same thing. + +Note that all patches from all contributors get reviewed. +After a pull request is made, other contributors will offer feedback. If the patch passes review, a maintainer will accept it with a comment. +When a pull request fails review, the author is expected to update the pull request to address the issue until it passes review and the pull request merges successfully. + +At least one review from a maintainer is required for all patches. + +### Developer's Certificate of Origin + +All contributions must include acceptance of the DCO: + +> Developer Certificate of Origin Version 1.1 +> +> Copyright (C) 2004, 2006 The Linux Foundation and its contributors. 660 +> York Street, Suite 102, San Francisco, CA 94110 USA +> +> Everyone is permitted to copy and distribute verbatim copies of this +> license document, but changing it is not allowed. +> +> Developer's Certificate of Origin 1.1 +> +> By making a contribution to this project, I certify that: +> +> \(a) The contribution was created in whole or in part by me and I have +> the right to submit it under the open source license indicated in the +> file; or +> +> \(b) The contribution is based upon previous work that, to the best of my +> knowledge, is covered under an appropriate open source license and I +> have the right under that license to submit that work with +> modifications, whether created in whole or in part by me, under the same +> open source license (unless I am permitted to submit under a different +> license), as indicated in the file; or +> +> \(c) The contribution was provided directly to me by some other person +> who certified (a), (b) or (c) and I have not modified it. +> +> \(d) I understand and agree that this project and the contribution are +> public and that a record of the contribution (including all personal +> information I submit with it, including my sign-off) is maintained +> indefinitely and may be redistributed consistent with this project or +> the open source license(s) involved. + +### Sign your work + +To accept the DCO, simply add this line to each commit message with your +name and email address (git commit -s will do this for you): + + Signed-off-by: Jane Example + +For legal reasons, no anonymous or pseudonymous contributions are +accepted. + +## Submitting Code Pull Requests + +We encourage and support contributions from the community. No fix is too +small. We strive to process all pull requests as soon as possible and +with constructive feedback. If your pull request is not accepted at +first, please try again after addressing the feedback you received. + +To make a pull request you will need a GitHub account. For help, see +GitHub's documentation on forking and pull requests. diff --git a/LICENSE b/LICENSE index 261eeb9..04cf87b 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright [2019] Hewlett Packard Enterprise Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9616fc4 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# HPE SimpliVity SDK for Python + +This library provides a Python interface to the HPE SimpliVity REST APIs. + +HPE SimpliVity is an intelligent hyperconverged platform that speeds application performance, +improves efficiency and resiliency, and backs up and restores VMs in seconds. + +## Installation + +### From source + +Either: + +```bash +$ git clone https://github.com/HewlettPackard/simplivity-python.git +$ cd simplivity-python +$ python setup.py install --user # to install in the user directory (~/.local) +$ sudo python setup.py install # to install globally +``` + +Or if using PIP: + +```bash +$ git clone https://github.com/HewlettPackard/simplivity-python.git +$ cd simplivity-python +$ pip install . +``` + +Both installation methods work if you are using virtualenv, which you should be! + +### From Pypi + +```bash +$ pip install simplivity +``` + + +## API Implementation + +Status of the HPE SimpliVity REST interfaces that have been implemented in this Python library can be found in the [Wiki section](https://github.com/HewlettPackard/simplivity-python/blob/master/endpoints-support.md). + + +## SDK Documentation + +The latest version of the SDK documentation can be found in the [SDK Documentation section](https://hewlettpackard.github.io/simplivity-python/index.html). + +## Configuration + +### JSON + +Connection properties for accessing the OVC can be set in a JSON file. + +Before running the samples or your own scripts, you must create the JSON file. +An example can be found at: [configuration sample](examples/config-rename.json). + +Once you have created the JSON file, you can initialize the OVC client: + +```python +ovc_client = OVC.from_json_file('/path/config.json') +``` + +:lock: Tip: Check the file permissions because the password is stored in clear-text. + +### Environment Variables + +Configuration can also be stored in environment variables: + +```bash +# Required +export SIMPLIVITYSDK_OVC_IP='10.30.4.45' +export SIMPLIVITYSDK_USERNAME='admin' +export SIMPLIVITYSDK_PASSWORD='secret' + +# Optional +export SIMPLIVITYSDK_SSL_CERTIFICATE='' +export SIMPLIVITYSDK_CONNECTION_TIMEOUT='' +``` + +:lock: Tip: Make sure no unauthorized person has access to the environment variables, since the password is stored in clear-text. + +Once you have defined the environment variables, you can initialize the OVC client using the following code snippet: + +```python +ovc_client = OVC.from_environment_variables() +``` + +### Dictionary + +You can also set the configuration using a dictionary. As described above, for authentication you can use username/password: + + +```python +config = { + "ip": "10.30.4.45", + "credentials": { + "username": "admin", + "password": "secret" + } +} + +ovc_client_client = OVC(config) +``` + +:lock: Tip: Check the file permissions because the password is stored in clear-text. + + +### SSL Server Certificate + +To enable the SDK to establish a SSL connection to the SimpliVity OVC, it is necessary to generate a CA certificate file containing the OVC credentials. + +1. Fetch the SimpliVity OVC CA certificate. +```bash +$ openssl s_client -showcerts -host -port 443 +``` + +2. Copy the OVC certificate wrapped with a header line and a footer line into a `.crt` file. +``` +-----BEGIN CERTIFICATE----- +... (OVC certificate in base64 PEM encoding) ... +-----END CERTIFICATE----- +``` + +3. Declare the CA Certificate location when creating a `config` dictionary. +```python +config = { + "ip": "172.16.102.82", + "credentials": { + "username": "admin", + "password": "secret" + }, + "ssl_certificate": "/path/ovc_certificate.crt" +} +``` + +### SimpliVity Connection Timeout +By default the system timeout is used when connecting to OVC. If you want to change this, +then the timeout can be set by either: + +1. Setting the appropriate environment variable: +```bash +export SIMPLIVITYSDK_CONNECTION_TIMEOUT='' +``` + +2. Setting the time-out in the JSON configuration file using the following syntax: +```json +"timeout": +``` + +## Contributing and feature requests +Contribution guide can be found at: [CONTRIBUTION GUIDE](CONTRIBUTING.md). + +#### Testing + +When contributing code to this project, we require tests to accompany the code being delivered. +That ensures a higher standing of quality, and also helps to avoid minor mistakes and future regressions. + +When writing the unit tests, the standard approach we follow is to use the python library [unittest.mock](https://docs.python.org/3/library/unittest.mock.html) to patch all calls that would be made to the OVC and return mocked values. + +We have packaged everything required to verify if the code is passing the tests in a tox file. +The tox call runs all unit tests against Python 3, runs a flake8 validation, and generates the test coverage report. + +To run it, use the following command: + +``` +$ tox +``` + +You can also check out examples of tests for different resources in the [tests](tests) folder. + +## License + +This project is licensed under the Apache license. Please see [LICENSE](LICENSE) for more information. + +## Version and changes + +To view history and notes for this version, view the [Changelog](CHANGELOG.md). diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..05e3385 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,69 @@ +#!/bin/bash +set -e # Exit with nonzero exit code if anything fails + +SOURCE_BRANCH="master" +TARGET_BRANCH="gh-pages" + +# Pull requests and commits to other branches shouldn't try to deploy, just build to verify +if [ "$TRAVIS_PULL_REQUEST" != "false" -o "$TRAVIS_BRANCH" != "$SOURCE_BRANCH" ]; then + echo "Skipping deploy; just doing a build." + exit 0 +fi + +# Save some useful information +REPO=`git config remote.origin.url` +SSH_REPO=${REPO/https:\/\/github.com\//git@github.com:} +SHA=`git rev-parse --verify HEAD` + +# Clone the existing gh-pages for this repo into out/ +# Create a new empty branch if gh-pages doesn't exist yet (should only happen on first deply) +rm -rf out || exit 0 + +git clone $REPO out +cd out +git checkout $TARGET_BRANCH || git checkout --orphan $TARGET_BRANCH +cd .. + + +# Clean out existing contents +rm -rf out/**/* || exit 0 + +# Copy generated documentation to the new directory +cp -a docs/build/html/. out/ + + +cd out +git config user.name "Travis CI" +git config user.email "simplivity-pythonsdk@hpe.com" + +# If there are no changes (e.g. this is a README update) then just bail. +if [ $(git status --porcelain | wc -l) -lt 1 ]; then + echo "No changes to the spec on this push; exiting." + exit 0 +fi + +# Commit the "changes", i.e. the new version. +# The delta will show diffs between new and old versions. +git add -A . +git commit -m "Deploy to GitHub Pages: ${SHA}" + + +## Get the deploy key by using Travis's stored variables to decrypt deploy_key.enc +openssl aes-256-cbc -K $encrypted_207a3e71573a_key -iv $encrypted_207a3e71573a_iv -in deploy_key.enc -out deploy_key -d + + +chmod 600 deploy_key + +eval `ssh-agent -s` + +ssh-add deploy_key + + +## Now that we're all set up, we can push. +git push $SSH_REPO $TARGET_BRANCH + +echo "Documentation pushed to gh-pages ................................" +cd .. +rm -rf out || exit 0 + +echo "Documentation deploy finished ..................................." diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..8b7b3a2 --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# 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. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = u'HPE SimpliVity Python SDK' +copyright = u'2019, Hewlett Packard Enterprise Development LP' +author = u'Hewlett Packard Enterprise' + +# The short X.Y version +version = u'1.0.0' +# The full version, including alpha/beta/rc tags +release = u'v1.0.0' + + +# -- 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', + 'sphinx.ext.githubpages', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# 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 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'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'HPESimpliVityPythonSDKdoc' + + +# -- 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': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# 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 = [ + (master_doc, 'HPESimpliVityPythonSDK.tex', u'HPE SimpliVity Python SDK Documentation', + u'Sijeesh Kattumunda', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'hpesimplivitypythonsdk', u'HPE SimpliVity Python SDK Documentation', + [author], 1) +] + + +# -- 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 = [ + (master_doc, 'HPESimpliVityPythonSDK', u'HPE SimpliVity Python SDK Documentation', + author, 'HPESimpliVityPythonSDK', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..eb41229 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. HPE SimpliVity Python SDK documentation master file, created by + sphinx-quickstart on Wed Dec 4 02:02:52 2019. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to HPE SimpliVity Python SDK's documentation! +===================================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/endpoints-support.md b/endpoints-support.md new file mode 100755 index 0000000..b3bda80 --- /dev/null +++ b/endpoints-support.md @@ -0,0 +1,28 @@ +Refer SimpliVity REST API doc for the resource endpoints documentation [HPE SimpliVity REST API Documentation](https://developer.hpe.com/api/simplivity/). + +
+ +## Supported resources and endpoints + +| Endpoints | Action | +| --------------------------------------------------------------------------------------- | -------- | +| **Backups** +|/backups |GET | +| **Datastores** +|/datastores |GET | +| **Hosts** +|/hosts |GET | +| **OmniStack Clusters** +|/omnistack_clusters |GET | +| **Policies** +|/policiess |GET | +| **Virtual Machines** +|/virtual_machines |GET | +|/virtual_machines/set_policy |POST | +|/virtual_machines/{vmId} |GET | +|/virtual_machines/{vmId}/backup |POST | +|/virtual_machines/{vmId}/backup_parameters |POST | +|/virtual_machines/{vmId}/backups |GET | +|/virtual_machines/{vmId}/clone |POST | +|/virtual_machines/{vmId}/move |POST | +|/virtual_machines/{vmId}/set_policy |POST | diff --git a/examples/backups.py b/examples/backups.py new file mode 100644 index 0000000..7306d01 --- /dev/null +++ b/examples/backups.py @@ -0,0 +1,64 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.ovc_client import OVC +from simplivity.exceptions import HPESimpliVityException + +config = { + "ip": "", + "credentials": { + "username": "", + "password": "" + } +} + +ovc = OVC(config) +backups = ovc.backups + +print("\n\n get_all with default params") +all_backups = backups.get_all() +count = len(all_backups) +for backup in all_backups: + print(backup) + +print("\nTotal number of backups {}".format(count)) +backup_object = all_backups[0] + +print("\n\n get_all with filers") +all_backups = backups.get_all(filters={'name': backup_object.data["name"]}) +for backup in all_backups: + print(backup) + +print("\n\n get_all with pagination") +pagination = backups.get_all(limit=105, pagination=True, page_size=50) +end = False +while not end: + data = pagination.data + print("Page size:", len(data["resources"])) + print(data) + + try: + pagination.next_page() + except HPESimpliVityException: + end = True + +print("\n\n get_by_id") +backup = backups.get_by_id(backup_object.data["id"]) +print(backup, backup.data) + +print("\n\n get_by_name") +backup = backups.get_by_name(backup_object.data["name"]) +print(backup, backup.data) diff --git a/examples/config-rename.json b/examples/config-rename.json new file mode 100644 index 0000000..476795a --- /dev/null +++ b/examples/config-rename.json @@ -0,0 +1,7 @@ +{ + "ip": "", + "credentials": { + "username": "", + "password": "" + } +} diff --git a/examples/datastores.py b/examples/datastores.py new file mode 100644 index 0000000..1a2d593 --- /dev/null +++ b/examples/datastores.py @@ -0,0 +1,67 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.ovc_client import OVC +from simplivity.exceptions import HPESimpliVityException + +config = { + "ip": "", + "credentials": { + "username": "", + "password": "" + } +} + +ovc = OVC(config) +datastores = ovc.datastores + +print("\n\n get_all with default params") +all_datastores = datastores.get_all() +count = len(all_datastores) +for datastore in all_datastores: + print(datastore.data) + +print("\nTotal number of datastores {}".format(count)) +datastore_object = all_datastores[0] + +print("\n\n get_all with filers") +all_datatores = datastores.get_all(filters={'name': datastore_object.data["name"]}) +count = len(all_datastores) +for datastore in all_datastores: + print(datastore.data) + +print("\n Total number of clusters {}".format(count)) + +print("\n\n get_all with pagination") +pagination = datastores.get_all(limit=105, pagination=True, page_size=50) +end = False +while not end: + data = pagination.data + print("Page size:", len(data["resources"])) + print(data) + + try: + pagination.next_page() + except HPESimpliVityException: + end = True + +print("\n\n get_by_id") +datastore = datastores.get_by_id(datastore_object.data["id"]) +print(datastore, datastore.data) + +print("\n\n get_by_name") +datastore = datastores.get_by_name(datastore_object.data["name"]) +print(datastore, datastore.data) diff --git a/examples/hosts.py b/examples/hosts.py new file mode 100644 index 0000000..0b4bd29 --- /dev/null +++ b/examples/hosts.py @@ -0,0 +1,67 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.ovc_client import OVC +from simplivity.exceptions import HPESimpliVityException + +config = { + "ip": "", + "credentials": { + "username": "", + "password": "" + } +} + +ovc = OVC(config) +hosts = ovc.hosts + +print("\n\n get_all with default params") +all_hosts = hosts.get_all() +count = len(all_hosts) +for host in all_hosts: + print(host.data) + +print("\nTotal number of hosts {}".format(count)) +host_object = all_hosts[0] + +print("\n\n get_all with filers") +all_hosts = hosts.get_all(filters={'name': host_object.data["name"]}) +count = len(all_hosts) +for host in all_hosts: + print(host.data) + +print("\n Total number of hosts {}".format(count)) + +print("\n\n get_all with pagination") +pagination = hosts.get_all(limit=105, pagination=True, page_size=50) +end = False +while not end: + data = pagination.data + print("Page size:", len(data["resources"])) + print(data) + + try: + pagination.next_page() + except HPESimpliVityException: + end = True + +print("\n\n get_by_id") +hosst = hosts.get_by_id(host_object.data["id"]) +print(host, host.data) + +print("\n\n get_by_name") +host = hosts.get_by_name(host_object.data["name"]) +print(host, host.data) diff --git a/examples/omnistack_clusters.py b/examples/omnistack_clusters.py new file mode 100644 index 0000000..87422dc --- /dev/null +++ b/examples/omnistack_clusters.py @@ -0,0 +1,67 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.ovc_client import OVC +from simplivity.exceptions import HPESimpliVityException + +config = { + "ip": "", + "credentials": { + "username": "", + "password": "" + } +} + +ovc = OVC(config) +clusters = ovc.omnistack_clusters + +print("\n\n get_all with default params") +all_clusters = clusters.get_all() +count = len(all_clusters) +for cluster in all_clusters: + print(cluster.data) + +print("\nTotal number of clusterss {}".format(count)) +cluster_object = all_clusters[0] + +print("\n\n get_all with filers") +all_clusters = clusters.get_all(filters={'name': cluster_object.data["name"]}) +count = len(all_clusters) +for cluster in all_clusters: + print(cluster.data) + +print("\n Total number of clusters {}".format(count)) + +print("\n\n get_all with pagination") +pagination = clusters.get_all(limit=105, pagination=True, page_size=50) +end = False +while not end: + data = pagination.data + print("Page size:", len(data["resources"])) + print(data) + + try: + pagination.next_page() + except HPESimpliVityException: + end = True + +print("\n\n get_by_id") +cluster = clusters.get_by_id(cluster_object.data["id"]) +print(cluster, cluster.data) + +print("\n\n get_by_name") +cluster = clusters.get_by_name(cluster_object.data["name"]) +print(cluster, cluster.data) diff --git a/examples/policies.py b/examples/policies.py new file mode 100644 index 0000000..c4bf776 --- /dev/null +++ b/examples/policies.py @@ -0,0 +1,72 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.ovc_client import OVC +from simplivity.exceptions import HPESimpliVityException + +config = { + "ip": "", + "credentials": { + "username": "", + "password": "" + } +} + +ovc = OVC(config) +policies = ovc.policies + +print("\n\n get_all with default params") +all_policies = policies.get_all() +count = len(all_policies) +for policy in all_policies: + print(policy) + +print("\nTotal number of policies {}".format(count)) +policy_object = all_policies[0] + +print("\n\n get_all with filers") +all_policies = policies.get_all(filters={'name': policy_object.data["name"]}) +count = len(all_policies) +for policy in all_policies: + print(policy) + +print("\n Total number of policies {}".format(count)) + +print("\n\n get_all with pagination") +pagination = policies.get_all(limit=105, pagination=True, page_size=50) +end = False +while not end: + data = pagination.data + print("Page size:", len(data["resources"])) + print(data) + + try: + pagination.next_page() + except HPESimpliVityException: + end = True + +print("\n\n get_by_id") +policy = policies.get_by_id(policy_object.data["id"]) +print(policy, policy.data) + +print("\n\n get_by_name") +policy = policies.get_by_name(policy_object.data["name"]) +print(policy, policy.data) + +print("\n\n get_all VMs using this policy") +vms = policy.get_vms() +print(policy.data) +print(vms) diff --git a/examples/virtual_machines.py b/examples/virtual_machines.py new file mode 100644 index 0000000..2bd3ea3 --- /dev/null +++ b/examples/virtual_machines.py @@ -0,0 +1,152 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +import time + +from simplivity.ovc_client import OVC +from simplivity.exceptions import HPESimpliVityException + +config = { + "ip": "", + "credentials": { + "username": "", + "password": "" + } +} + +# Create resource clients +ovc = OVC(config) +machines = ovc.virtual_machines +policies = ovc.policies +datastores = ovc.datastores +omnistack_clusters = ovc.omnistack_clusters + +# variable declaration +vm_1_name = "health_tool_move_test" +vm_2_name = "new_vm_for_sdk_test" +datastore_1_name = "SVT-DS1" +datastore_2_name = "DS2" +policy_name = "Simple" +omnistack_cluster_name = "Epyc" + + +print("\n\n get_all with default params") +vms = machines.get_all() +count = len(vms) +for vm in vms: + print(vm.data) + +print("\nTotal number of vms {}".format(count)) +vm_object = vms[0] + +print("\n\n get_all with filers") +vms = machines.get_all(filters={'name': vm_object.data["name"]}) +count = len(vms) +for vm in vms: + print(vm.data) + +print("\n Total number of vms {}".format(count)) + +print("\n\n get_all with pagination") +pagination = machines.get_all(limit=500, pagination=True, page_size=50) +end = False +while not end: + data = pagination.data + print("Page size:", len(data["resources"])) + print(data) + + try: + pagination.next_page() + except HPESimpliVityException: + end = True + +print("\n\n get_by_id") +vm = machines.get_by_id(vm_object.data["id"]) +print(vm, vm.data) + +print("\n\n get_by_name") +vm1 = machines.get_by_name(vm_1_name) +print(vm1, vm1.data) + +vm2 = machines.get_by_name(vm_2_name) +print(vm2, vm2.data) + +policy = policies.get_by_name(policy_name) +print("Policy: ", policy, policy.data) + +print("\n\n get_backups") +backups = vm2.get_backups() +for backup in backups: + print(backup.data) + +print("\n\n set_policy_for_multiple_vms") +vms = [vm1, vm2] +response = machines.set_policy_for_multiple_vms(policy, vms) +for vm in response: + print(vm, vm.data) + +print("\n\n clone") +cloned_vm = vm1.clone(vm_1_name + " clone_test") +print(cloned_vm, cloned_vm.data) + +print("\n\n clone and move to different datastore") +print("VM id :", vm1.data["id"]) +cloned_vm = vm1.clone(vm_1_name + " clone_move_test", datastore=datastore_2_name) + +print("\nMoved vm details") +print(cloned_vm, cloned_vm.data) + +print("\nMove VM to different datastore using datastore's name") +print("\n VM data before move") +print(vm1.data) + +newvm = vm2.move(vm_2_name + "_move_test", datastore_2_name) +print("\n New VM details, after move") +print(newvm.data) + +newvm = newvm.move(vm_2_name, datastore_1_name) +print("\n Move VM back to the original datastore") +print(newvm.data) + +print("\n Move using datastore object") +datastore = datastores.get_by_name(datastore_2_name) +newvm = vm1.move(vm_1_name + "move_with_obj", datastore) +print(newvm.data) + +print("\n Move VM back to the original datastore") +newvm = newvm.move(vm_1_name, datastore_1_name) +print(newvm.data) + +print("\n create_backup") +cluster = omnistack_clusters.get_by_name(omnistack_cluster_name) +print("Cluster data", cluster.data) + +vm_backup = vm2.create_backup("backup_test_from_sdk_" + str(time.time()), cluster) +print(vm_backup.data) + +print("Get backups of a single vm") +backups = vm1.get_backups() +print(backups) + +print("\n Set backup parameters of a VM") +guest_username = "svt" +guest_password = "svtpassword" +set_parameters = vm1.set_backup_parameters(guest_username, guest_password) +print(set_parameters) + +print("\n Set policy") +set_policy = vm1.set_policy(policy_name) +print(set_policy.data) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..12871ff --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file=README.md diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..641cca8 --- /dev/null +++ b/setup.py @@ -0,0 +1,30 @@ +#### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from setuptools import find_packages +from setuptools import setup + +setup(name='simplivity', + version='1.0.0', + description='HPE SimpliVity Python Library', + url='https://github.com/HewlettPackard/simplivity-python', + download_url="https://github.com/HewlettPackard/simplivity-python/tarball/v1.0.0", + author='Hewlett Packard Enterprise Development LP', + author_email='simplivity-pythonsdk@hpe.com', + license='Apache', + packages=find_packages(exclude=['examples*', 'tests*']), + keywords=['simplivity', 'hpe'], + python_requires='>=3.3') diff --git a/simplivity/__init__.py b/simplivity/__init__.py new file mode 100644 index 0000000..95fc5a9 --- /dev/null +++ b/simplivity/__init__.py @@ -0,0 +1,3 @@ + +from simplivity.connection import * +from simplivity.exceptions import * diff --git a/simplivity/connection.py b/simplivity/connection.py new file mode 100644 index 0000000..5e44c5c --- /dev/null +++ b/simplivity/connection.py @@ -0,0 +1,252 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 module maintains communication with SimplVity.""" + +import http.client +from base64 import b64encode + +import json +import logging +import ssl +import urllib +import traceback + +from simplivity import exceptions + +logger = logging.getLogger(__name__) + + +class Connection(object): + """Helps to make connection with the OVC and do rest calls.""" + + def __init__(self, ovc_ip, ssl_bundle=False, timeout=None): + """Initialize Connection class""" + self._ovc_ip = ovc_ip + self._timeout = timeout + self._ssl_trusted_bundle = ssl_bundle + self._ssl_trust_all = False if ssl_bundle else True + self._username = None + self._password = None + self._access_token = None + + self._headers = {'Accept': 'application/vnd.simplivity.v1+json'} + self._base_url = "https://{}/api".format(ovc_ip) + self.__connection = None + + def do_http(self, method, path, body, custom_headers=None, login=False): + """Makes http calls. + + Args: + method: HTTP methods (GET, POST, PUT, DELETE). + path: URL + body: Request body. + custom_headers: Custom headers to update/append default headers. + login: True if the call is for login and get the token. + + Returns: + tuple: Tuple with two members (HTTP response object and the response body in json). + """ + http_headers = self._headers.copy() + path = "{}{}".format(self._base_url, path) + + if login: + user_pass = b64encode(b"simplivity:").decode("ascii") + http_headers.update({'Content-type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic %s' % user_pass}) + else: + if not self._access_token: + raise exceptions.HPESimpliVityException("There is no active session, please login") + + http_headers['Content-type'] = 'application/vnd.simplivity.v1.8+json' + http_headers['Authorization'] = "Bearer " + self._access_token + + # Updates default headers with the custom headers + if custom_headers: + http_headers.update(custom_headers) + + try: + if not self.__connection: + self.__connection = self.get_connection() + self.__connection.request(method, path, body, http_headers) + resp = self.__connection.getresponse() + response = resp.read() + body = json.loads(response.decode('utf-8')) + except http.client.HTTPException: + raise exceptions.HPESimpliVityException(traceback.format_exc()) + + # Updates token if expired + if 'error' in body and body['error'] == 'invalid_token': + self.login(self._username, self._password) + self.do_http(method, path, body, custom_headers) + + return resp, body + + def get_connection(self): + """Makes connection with the OVC. + + Returns: + HTTPSConnection object + """ + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + if self._ssl_trust_all is False: + context.verify_mode = ssl.CERT_REQUIRED + context.load_verify_locations(self._ssl_trusted_bundle) + conn = http.client.HTTPSConnection(self._ovc_ip, + context=context, + timeout=self._timeout) + else: + context.verify_mode = ssl.CERT_NONE + conn = http.client.HTTPSConnection(self._ovc_ip, + context=context, + timeout=self._timeout) + return conn + + def get(self, url): + """Calls get http method. + + Args: + url: Resource URL + + Returns: + tuple: Tuple with two members (HTTP response object and the response body in json). + + Raises: + HPESimpliVityException: if the response status is 400 and above + """ + resp, body = self.do_http('GET', url, '') + if resp.status >= 400: + raise exceptions.HPESimpliVityException(body) + + return body + + def post(self, uri, body, custom_headers=None): + """Calls post http method. + + Args: + uri: Resource URI. + body: Request body. + custom_headers: Custome headers to update/append default headers. + + Returns: + dict: Response body + """ + + return self.__do_rest_call('POST', uri, body, custom_headers=custom_headers) + + def put(self, uri, body, custom_headers=None): + """Calls put http method. + + Args: + uri: Resource URI. + body: Request body. + custom_headers: Custome headers to update/append default headers. + + Returns: + tuple: Tuple with two members (HTTP response object and the response body in json). + """ + return self.__do_rest_call('PUT', uri, body, custom_headers=custom_headers) + + def delete(self, uri, custom_headers=None): + """Calls delete http method. + + Args: + uri: Resource URI. + custom_headers: Custom headers to appened/update default headers. + + Returns: + tuple: Tuple with two members (HTTP response object and the response body in json). + """ + return self.__do_rest_call('DELETE', uri, {}, custom_headers=custom_headers) + + def __body_content_is_task(self, body): + """Check to find task in response body + + Args: + body: Response body of a rest call + + Returns: + boolean: Returns True if the body is task data or False + """ + return isinstance(body, dict) and 'task' in body + + def __do_rest_call(self, http_method, url, body, custom_headers): + """Calls do_http method and handles the http status code. + + Args: + http_method: HTTP method (GET, POST, PUT and DELETE) + url: Resource URL + body: Request body + custom_headers: Headers to appened/update default headers + + Returns: + tuple: A tuple of two elements (task and response body) + + Raises: + HPESimpliVityException: if the response status code is 401/403/404 + """ + resp, body = self.do_http(method=http_method, + path=url, + body=json.dumps(body), + custom_headers=custom_headers) + + if resp.status in [400, 401, 403, 404]: + raise exceptions.HPESimpliVityException(body) + + if self.__body_content_is_task(body): + return body, body + + return None, body + + def login(self, username, password): + """Login using OVC username and password. + + Args: + username: OVC username + password: OVC password + + Returns: + boolean: Returns True if login is successfull. + """ + login_url = "/oauth/token" + data = {'grant_type': 'password', + 'username': username, + 'password': password} + + resp, body = self.do_http('POST', login_url, body=urllib.parse.urlencode(data), login=True) + + try: + self._access_token = body["access_token"] + logger.info('Logged in successfully') + except KeyError: + raise exceptions.HPESimpliVityAuthenticationError("Invalid credentials") + + # Save the username and password for refreshing the connection + self._username = username + self._password = password + + return True + + def logout(self): + """Removes the access token. + + Returns: + boolean: Returns True + """ + self._access_token = None + logger.info('Logged out successfully') + + return True diff --git a/simplivity/exceptions.py b/simplivity/exceptions.py new file mode 100644 index 0000000..95c328c --- /dev/null +++ b/simplivity/exceptions.py @@ -0,0 +1,94 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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. +## + +"""Module to define SimpliVity exception classes.""" + +import logging + +logger = logging.getLogger(__name__) + + +class HPESimpliVityException(Exception): + """ + SimpliVity base Exception. + + Attributes: + msg (str): Exception message. + response (dict): SimpliVity rest response. + """ + + def __init__(self, data, error=None): + self.msg = None + self.response = None + + if isinstance(data, str): + self.msg = data + else: + self.response = data + + if data and isinstance(data, dict): + self.msg = data.get('message') + + if self.response: + Exception.__init__(self, self.msg, self.response) + else: + Exception.__init__(self, self.msg) + + +class HPESimpliVityTaskError(HPESimpliVityException): + """ + SimpliVity Task Error Exception. + + Attributes: + msg (str): Exception message. + error_code (str): A code which uniquely identifies the specific error. + """ + + def __init__(self, msg, error_code=None): + super(HPESimpliVityTaskError, self).__init__(msg) + self.error_code = error_code + + +class HPESimpliVityTimeout(HPESimpliVityException): + """ + SimpliVity Timeout Exception. + + Attributes: + msg (str): Exception message. + """ + pass + + +class HPESimpliVityResourceNotFound(HPESimpliVityException): + """ + SimpliVity Resource Not Found Exception. + The exception is raised when an associated resource was not found. + + Attributes: + msg (str): Exception message. + """ + pass + + +class HPESimpliVityAuthenticationError(HPESimpliVityException): + """ + SimpliVity Authentication Exception. + The exception is raised when the credentials supplied is not valid. + + Attributes: + msg (str): Exception message. + """ + pass diff --git a/simplivity/ovc_client.py b/simplivity/ovc_client.py new file mode 100755 index 0000000..fdc4429 --- /dev/null +++ b/simplivity/ovc_client.py @@ -0,0 +1,175 @@ +#### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 module implements a common client for HPE SimpliVity resources. +""" +import json +import os + +from simplivity.connection import Connection +from simplivity import exceptions + +from simplivity.resources.virtual_machines import VirtualMachines +from simplivity.resources.policies import Policies +from simplivity.resources.datastores import Datastores +from simplivity.resources.omnistack_clusters import OmnistackClusters +from simplivity.resources.backups import Backups +from simplivity.resources.hosts import Hosts + + +class OVC(object): + """Client class for all the resources.""" + + def __init__(self, config): + """Initialize OVC class.""" + self.__connection = Connection(config["ip"], config.get('ssl_certificate', False), config.get('timeout')) + if config.get("credentials"): + username = config["credentials"].get("username") + password = config["credentials"].get("password") + self.__connection.login(username, password) + else: + exceptions.HPESimpliVityException("Credentials not provided") + + self.__virtual_machines = None + self.__policies = None + self.__datastores = None + self.__omnistack_clusters = None + self.__backups = None + self.__hosts = None + + @classmethod + def from_json_file(cls, file_name): + """ + Construct OVC client using a json file. + + Args: + file_name: json full path. + + Returns: + OVC object + """ + with open(file_name) as json_data: + config = json.load(json_data) + + return cls(config) + + @classmethod + def from_environment_variables(cls): + """ + Construct OVC Client using environment variables. + + Returns: + OVC object + """ + ip = os.environ.get('SIMPLIVITYSDK_OVC_IP', '') + username = os.environ.get('SIMPLIVITYSDK_USERNAME', '') + password = os.environ.get('SIMPLIVITYSDK_PASSWORD', '') + ssl_certificate = os.environ.get('SIMPLIVITYSDK_SSL_CERTIFICATE', '') + timeout = os.environ.get('SIMPLIVITYSDK_CONNECTION_TIMEOUT') + + if not ip or not username or not password: + raise exceptions.SimplivityExceptions("Make sure you have set mandatory env variables \ + (SIMPLIVITYSDK_OVC_IP, SIMPLIVITYSDK_USERNAME, SIMPLIVITYSDK_PASSWORD)") + + config = dict(ip=ip, + ssl_certificate=ssl_certificate, + credentials=dict(username=username, password=password), + timeout=timeout) + + return cls(config) + + @property + def connection(self): + """ + Gets the underlying OVC connection used by the OVC client. + + Returns: + Connection object + """ + return self.__connection + + @property + def virtual_machines(self): + """ + Gets the Virtual Machines client. + + Returns: + VirtualMachines object + """ + if not self.__virtual_machines: + self.__virtual_machines = VirtualMachines(self.__connection) + return self.__virtual_machines + + @property + def policies(self): + """ + Gets the Policies client. + + Returns: + Policies object + """ + if not self.__policies: + self.__policies = Policies(self.__connection) + return self.__policies + + @property + def datastores(self): + """ + Gets the Datastores client. + + Returns: + Datastores object + """ + if not self.__datastores: + self.__datastores = Datastores(self.__connection) + return self.__datastores + + @property + def omnistack_clusters(self): + """ + Gets the Omnistack clusters client. + + Returns: + OmnistackClusters object + """ + if not self.__omnistack_clusters: + self.__omnistack_clusters = OmnistackClusters(self.__connection) + return self.__omnistack_clusters + + @property + def backups(self): + """ + Gets the Backups client. + + Returns: + Backups object + """ + if not self.__backups: + self.__backups = Backups(self.__connection) + return self.__backups + + @property + def hosts(self): + """ + Gets the Hosts resource client. + + Returns: + Hosts object + """ + if not self.__hosts: + self.__hosts = Hosts(self.__connection) + return self.__hosts diff --git a/simplivity/resources/__init__.py b/simplivity/resources/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/simplivity/resources/backups.py b/simplivity/resources/backups.py new file mode 100644 index 0000000..50f9150 --- /dev/null +++ b/simplivity/resources/backups.py @@ -0,0 +1,134 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.resources.resource import ResourceBase + +URL = '/backups' +DATA_FIELD = 'backups' + + +class Backups(ResourceBase): + """Implements features available for SimpliVity Backup resources.""" + + def __init__(self, connection): + super(Backups, self).__init__(connection) + + def get_all(self, pagination=False, page_size=0, limit=500, offset=0, + sort=None, order='descending', filters=None, fields=None, + case_sensitive=True): + """Gets all backups. + Args: + pagination: True if need pagination + page_size: Size of the page (Required when pagination is on) + limit: A positive integer that represents the maximum number of results to return + offset: A positive integer that directs the service to start returning + the instance, up to the limit. + sort: The name of the field where the sort occurs. + order: The sort order preference. Valid values: ascending or descending. + filters: Dictionary with filter values. Example: {'name': 'name'} + id: the unique identifier (UID) of the backups to return + Accepts: Single value, comma-separated list + name: The name of the backups to return + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard. + sent_min: The minimum sent data size (in bytes) of the remote backups to return. + sent_max: The maximum sent data size (in bytes) of the remote backups to return. + state: The current state of the backups to return + Accepts: Single value, comma-separated list + type: The type of backups to return. + Accepts: Single value, comma-separated list. + omnistack_cluster_id: The unique identifier (UID) of the omnistack_cluster + that is associated with the instances to return + Accepts: Single value, comma-separated list + omnistack_cluster_name: The name of the omnistack_cluster that is associated + with the instances to return + Accepts: Single value, comma-separated list + compute_cluster_parent_hypervisor_object_id: The unique identifier (UID) of the + hypervisor that contains the omnistack_cluster that is associated with the instances to return + Accepts: Single value, comma-separated list + compute_cluster_parent_name: The name of the hypervisor that contains the + omnistack_cluster that is associated with the instances to return + Accepts: Single value, comma-separated list + datastore_id: The unique identifier (UID) of the datastore that is associated + with the instances to return + Accepts: Single value, comma-separated list + datastore_name: The name of the datastore that is associated with the instances to return + Accepts: Single value, comma-separated list + expires_before: The latest expiration time before the backups to return expire, + expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + expires_after: The earliest expiration time after the backups to return expire, + expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + virtual_machine_id: The unique identifier (UID) of the virtual_machine that is + associated with the instances to return + Accepts: Single value, comma-separated list + virtual_machine_name: The name of the virtual_machine that is associated with + the instances to return + Accepts: Single value, comma-separated list + virtual_machine_type: The type of the virtual_machine that is associated with the + instances to return + Accepts: Single value, comma-separated list + size_min: The minimum size (in bytes) of the backups to return + size_max: The maximum size (in bytes) of the backups to return + application_consistent: The application-consistent setting of the backups to return + consistency_type: The consistency type of the backups to return + Accepts: Single value, comma-separated list + created_before: The latest creation time before the backups to return were created, + expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + created_after: The earliest creation time after the backups to return were created, + expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + sent_duration_min: The minimum number of seconds that elapsed while replicating + the backups to return + sent_duration_max: The maximum number of seconds that elapsed while replicating the + backups to return + sent_completion_before: The latest time before the replication of backups to return was + completed, expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + sent_completion_after: The earliest time after the replication of backups to return was + completed, expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + + Returns: + list: list of resources + """ + return self._client.get_all(URL, + members_field=DATA_FIELD, + pagination=pagination, + page_size=page_size, + limit=limit, + offset=offset, + sort=sort, + order=order, + filters=filters, + fields=fields, + case_sensitive=case_sensitive) + + def get_by_data(self, data): + """Gets Backup object from backup data. + + Args: + data: Backup data + + Returns: + object: Backup object. + """ + return Backup(self._connection, self._client, data) + + +class Backup(object): + """Implements features available for a single Backup resources.""" + + def __init__(self, connection, resource_client, data): + self.data = data + self._connection = connection + self._client = resource_client diff --git a/simplivity/resources/datastores.py b/simplivity/resources/datastores.py new file mode 100644 index 0000000..3c8cfcd --- /dev/null +++ b/simplivity/resources/datastores.py @@ -0,0 +1,115 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.resources.resource import ResourceBase + +URL = '/datastores' +DATA_FIELD = 'datastores' + + +class Datastores(ResourceBase): + """Implements features available for SimpliVity Datastore resources.""" + + def __init__(self, connection): + super(Datastores, self).__init__(connection) + + def get_all(self, pagination=False, page_size=0, limit=500, offset=0, + sort=None, order='descending', filters=None, fields=None, + case_sensitive=True, show_optional_fields=False): + """Gets all datastores. + + Args: + pagination: True if need pagination + page_size: Size of the page (Required when pagination is on) + limit: A positive integer that represents the maximum number of results to return + offset: A positive integer that directs the service to start returning + the instance, up to the limit. + sort: The name of the field where the sort occurs. + order: The sort order preference. Valid values: ascending or descending. + filters: Dictionary with filter values. Example: {'name': 'name'} + id: The unique identifier (UID) of the datastores to return + Accepts: Single value, comma-separated list + name: The name of the datastores to return + Accepts: Single value, comma-separated list, pattern using one + or more asterisk characters as a wildcard + min_size: The minimum size (in bytes) of datastores to return + max_size: The maximum size (in bytes) of datastores to return + created_before: The latest creation time before the datastores to return were created, + expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + created_after: The earliest creation time after the datastores to return were created, + expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + omnistack_cluster_id: The unique identifier (UID) of the omnistack_cluster that is + associated with the instances to return + Accepts: Single value, comma-separated list + omnistack_cluster_name: The name of the omnistack_cluster that is associated with + the instances to return + Accepts: Single value, comma-separated list + compute_cluster_parent_hypervisor_object_id: The unique identifier (UID) of the hypervisor + that contains the omnistack_cluster that is associated with the instances to return + Accepts: Single value, comma-separated list + compute_cluster_parent_name: The name of the hypervisor that contains the omnistack + cluster that is associated with the instances to return + Accepts: Single value, comma-separated list + hypervisor_management_system_name: The name of the Hypervisor Management System (HMS) + associated with the datastore + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + policy_id: The unique identifier (UID) of the policy that is associated with the + instances to return + Accepts: Single value, comma-separated list + policy_name: The name of the policy that is associated with the instances to return + Accepts: Single value, comma-separated list + hypervisor_object_id: The unique identifier (UID) of the hypervisor-based instance + that is associated with the instances to return + Accepts: Single value, comma-separated list + mount_directory: A comma-separated list of fields to include in the returned objects + Default: Returns all fields + + Returns: + list: list of Datastore objects. + """ + return self._client.get_all(URL, + members_field=DATA_FIELD, + pagination=pagination, + page_size=page_size, + limit=limit, + offset=offset, + sort=sort, + order=order, + filters=filters, + fields=fields, + case_sensitive=case_sensitive, + show_optional_fields=show_optional_fields) + + def get_by_data(self, data): + """Gets Datastore object from data. + + Args: + data: Datastore data + + Returns: + object: Datastore object. + """ + return Datastore(self._connection, self._client, data) + + +class Datastore(object): + """Implements features available for single Datastore resource.""" + + def __init__(self, connection, resource_client, data): + self.data = data + self._connection = connection + self._client = resource_client diff --git a/simplivity/resources/hosts.py b/simplivity/resources/hosts.py new file mode 100644 index 0000000..3cd6b39 --- /dev/null +++ b/simplivity/resources/hosts.py @@ -0,0 +1,153 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.resources.resource import ResourceBase + +URL = '/hosts' +DATA_FIELD = 'hosts' + + +class Hosts(ResourceBase): + """Implements features available for SimpliVity Host resources.""" + + def __init__(self, connection): + super(Hosts, self).__init__(connection) + + def get_all(self, pagination=False, page_size=0, limit=500, offset=0, + sort=None, order='descending', filters=None, fields=None, + case_sensitive=True, show_optional_fields=False): + """Gets all hosts. + + Args: + pagination: True if need pagination + page_size: Size of the page (Required when pagination is on) + limit: A positive integer that represents the maximum number of results to return + offset: A positive integer that directs the service to start returning + the instance, up to the limit. + sort: The name of the field where the sort occurs. + order: The sort order preference. Valid values: ascending or descending. + filters: Dictionary with filter values. Example: {'name': 'name'} + id: The unique identifier (UID) of the host + Accepts: Single value, comma-separated list + name: The name of the host + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + type: The type of host + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + model: The model of the host + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + version: The version of the host + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + hypervisor_management_system: The IP address of the Hypervisor Management System (HMS) + associated with the host + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + hypervisor_management_system_name: The name of the Hypervisor Management System (HMS) + associated with the host + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + hypervisor_object_id: The unique identifier (UID) of the hypervisor associated + with the host + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + compute_cluster_name: The name of the compute cluster associated with the host + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + compute_cluster_hypervisor_object_id: The unique identifier (UID) + of the Hypervisor Management System (HMS) for the associated compute cluster + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + management_ip: The IP address of the HPE OmniStack management module that + runs on the host + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + storage_ip: The IP address of the HPE OmniStack storage module that runs on the host + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + federation_ip: The IP address of the federation + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + virtual_controller_name: The name of the Virtual Controller that runs on the host + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + compute_cluster_parent_name: The name of the hypervisor that contains the omnistack + cluster that is associated with the instance + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + compute_cluster_parent_hypervisor_object_id: The unique identifier (UID) of the + hypervisor that contains the omnistack_cluster that is associated with the instance + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + policy_enabled: An indicator to show the status of the backup policy for the host + Valid values: + True: The backup policy for the host is enabled. + False: The backup policy for the host is disabled. + current_feature_level_min: The minimum current feature level of the HPE OmniStack + software running on the host + current_feature_level_max: The maximum current feature level of the HPE OmniStack + software running on the host + potential_feature_level_min: The minimum potential feature level of the HPE OmniStack + software running on the host + potential_feature_level_max: The maximum potential feature level of the HPE OmniStack + software running on the host + upgrade_state: The state of the most recent HPE OmniStack software upgrade for this + host (SUCCESS, FAIL, IN_PROGRESS, NOOP, UNKNOWN) + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + can_rollback: An indicator to show if the current HPE OmniStack software running on + the host can roll back to the previous version + Valid values: + True: The current HPE OmniStack software for the host can roll back to the previous version. + False: The current HPE OmniStack software for the host cannot roll back to the previous version. + + Returns: + list: list of Host objects + """ + return self._client.get_all(URL, + members_field=DATA_FIELD, + pagination=pagination, + page_size=page_size, + limit=limit, + offset=offset, + sort=sort, + order=order, + filters=filters, + fields=fields, + case_sensitive=case_sensitive, + show_optional_fields=show_optional_fields) + + def get_by_data(self, data): + """Gets Host object from host data. + + Args: + data: host data + + Returns: + object: Host object. + """ + return Host(self._connection, self._client, data) + + +class Host(object): + """Implements features available for single Host resource.""" + + def __init__(self, connection, resource_client, data): + self.data = data + self._connection = connection + self._client = resource_client diff --git a/simplivity/resources/omnistack_clusters.py b/simplivity/resources/omnistack_clusters.py new file mode 100644 index 0000000..122487f --- /dev/null +++ b/simplivity/resources/omnistack_clusters.py @@ -0,0 +1,112 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +## + +from simplivity.resources.resource import ResourceBase + +URL = '/omnistack_clusters' +DATA_FIELD = 'omnistack_clusters' + + +class OmnistackClusters(ResourceBase): + """Implements features available for OmniStack cluster resources.""" + + def __init__(self, connection): + super(OmnistackClusters, self).__init__(connection) + + def get_all(self, pagination=False, page_size=0, limit=500, offset=0, + sort=None, order='descending', filters=None, fields=None, + case_sensitive=True, show_optional_fields=False): + """Gets all omnistack clusters. + + Args: + pagination: True if need pagination + page_size: Size of the page (Required when pagination is on) + limit: A positive integer that represents the maximum number of results to return + offset: A positive integer that directs the service to start returning + the instance, up to the limit. + sort: The name of the field where the sort occurs. + order: The sort order preference. Valid values: ascending or descending. + filters: Dictionary with filter values. Example: {'name': 'name'} + id: The unique identifier (UID) of the omnistack_clusters to return + Accepts: Single value, comma-separated list + name: The name of the omnistack_clusters to return + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + hypervisor_object_id: The unique identifier (UID) of the hypervisor associated + with the objects to return + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + hypervisor_object_parent_id: The unique identifier (UID) of the hypervisor that + contains the objects to return + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + hypervisor_object_parent_name: The name of the hypervisor that contains the objects + to return + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + hypervisor_management_system_name: The name of the hypervisor associated with the + omnistack_cluster + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + type: The type of omnistack_clusters to return + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + arbiter_address: The address of the Arbiter connected to the objects to return + Accepts: Single value, comma-separated list, pattern using one or more asterisk + characters as a wildcard + arbiter_connected: An indicator to show if the omnistack_cluster is connected to Arbiter + Valid values: + True: Only returns omnistack_clusters connected to Arbiters that you identified + in arbiter_address + False: Only returns omnistack_clusters not connected to Arbiters that you identified + in arbiter_address + + Returns: + list: list of OmnistackCluster + """ + return self._client.get_all(URL, + members_field=DATA_FIELD, + pagination=pagination, + page_size=page_size, + limit=limit, + offset=offset, + sort=sort, + order=order, + filters=filters, + fields=fields, + case_sensitive=case_sensitive, + show_optional_fields=show_optional_fields) + + def get_by_data(self, data): + """Gets OmnistackCluster object from data. + + Args: + data: OmnistackCluster data + + Returns: + object: OmnistackCluster object. + """ + + return OmnistackCluster(self._connection, self._client, data) + + +class OmnistackCluster(object): + """Implements features available for single OmniStack cluster resource.""" + + def __init__(self, connection, resource_client, data): + self.data = data + self._connection = connection + self._client = resource_client diff --git a/simplivity/resources/policies.py b/simplivity/resources/policies.py new file mode 100644 index 0000000..2561f11 --- /dev/null +++ b/simplivity/resources/policies.py @@ -0,0 +1,100 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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. +## + +"""Implements operations for policies.""" + +from simplivity.resources.resource import ResourceBase +import simplivity.resources as resources + +URL = '/policies' +DATA_FIELD = 'policies' + + +class Policies(ResourceBase): + """Implements features for SimpliVity Policy resources.""" + + def __init__(self, connection): + super(Policies, self).__init__(connection) + + def get_all(self, pagination=False, page_size=0, limit=500, offset=0, + sort=None, order='descending', filters=None, fields=None, + case_sensitive=True): + """Gets all policies. + + Args: + pagination: True if need pagination + page_size: Size of the page (Required when pagination is on) + limit: A positive integer that represents the maximum number of results to return + offset: A positive integer that directs the service to start returning + the instance, up to the limit. + sort: The name of the field where the sort occurs. + order: The sort order preference. Valid values: ascending or descending. + filters: Dictionary with filter values. Example: {'name': 'name'} + id: The unique identifier (UID) of the policy + Accepts: Single value, comma-separated list + name:The name of the policy + Accepts: Single value, comma-separated list + Returns: + list: list of Policy objects + """ + return self._client.get_all(URL, + members_field=DATA_FIELD, + pagination=pagination, + page_size=page_size, + limit=limit, + offset=offset, + sort=sort, + order=order, + filters=filters, + fields=fields, + case_sensitive=case_sensitive) + + def get_by_data(self, data): + """Gets Policy object from data. + + Args: + data: Policy data + + Returns: + object: Policy object. + """ + return Policy(self._connection, self._client, data) + + +class Policy(object): + """Implements features available for a single Policy resource.""" + + def __init__(self, connection, resource_client, data): + self.data = data + self._connection = connection + self._client = resource_client + + def get_vms(self): + """Retrieves the virtual machines using this policy. + + Returns: + list: List of vms. + """ + method_url = "{}/{}/virtual_machines".format(URL, self.data["id"]) + vm_data = self._client.do_get(method_url).get("virtual_machines", []) + + vms_obj = resources.virtual_machines.VirtualMachines(self._connection) + vms = [] + + for vm in vm_data: + vms.append(vms_obj.get_by_id(vm["id"])) + + return vms diff --git a/simplivity/resources/resource.py b/simplivity/resources/resource.py new file mode 100755 index 0000000..4eac4dc --- /dev/null +++ b/simplivity/resources/resource.py @@ -0,0 +1,307 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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. +## + +"""Implements helper methods for the resource classes.""" + +import logging +from urllib.parse import quote + +from simplivity.resources.tasks import Task +from simplivity import exceptions + +PAGE_SIZE_NOT_SET = "page_size param should be set when pagination is on" +PAGINATION_NO_MORE_PAGES = "No more pages" + +logger = logging.getLogger(__name__) + + +def build_uri_with_query_string(base_url, kwargs): + """Creates URL using base url and the parameters. + + Args: + base_url: URL string. + kwargs: Dictionary of parameters + + Returns: + string: URL with query parameters + """ + query_string = '&'.join('{}={}'.format(key, kwargs[key]) for key in sorted(kwargs)) + symbol = '?' if '?' not in base_url else '&' + return "{}{}{}".format(base_url, symbol, query_string) + + +class Pagination(object): + """Implements pagination features for get_all method.""" + + def __init__(self, con, url, resource_obj, params, members_field, page_size): + """Initializes Pagination class.""" + self._url = url + self._resource_obj = resource_obj + self._params = params + self._members_field = members_field + self._page_size = page_size + self._connection = con + + self._total_count = params["limit"] + self._params["limit"] = page_size + self._params["offset"] = 0 + + self._first_page = 1 + self._last_page = 1 + self._last_page_size = page_size + + if self._total_count > page_size: + last_page, one_more_page = divmod(self._total_count, page_size) + self._last_page = last_page + if one_more_page: + self._last_page += 1 + self._last_page_size = one_more_page + + self.data = {"resources": [], "page": 1, + "size": 0, "requested_pages": self._last_page} + self._set_data() + + def next_page(self): + """Gets next page. + + Returns list of resources from next page. + + Raises: + HPESimpliVityException: if no more pages to return. + """ + if self.data["page"] + 1 > self._last_page: + raise exceptions.HPESimpliVityException(PAGINATION_NO_MORE_PAGES) + + self.data["page"] += 1 + + if self.data["page"] == self._last_page: + self._params["limit"] = self._last_page_size + + self._params["offset"] += self._page_size + self._set_data() + + return self.data + + def previous_page(self): + """Gets previous page. + + Returns list of resources from previous page. + + Raises: + HPESimpliVityException: if no more pages to return. + """ + if self.data["page"] - 1 < self._first_page: + raise exceptions.HPESimpliVityException(PAGINATION_NO_MORE_PAGES) + + self.data["page"] -= 1 + + self._params["limit"] = self._page_size + self._params["offset"] -= self._page_size + self._set_data() + + return self.data + + def _set_data(self): + """ + Sets data(page size and the resources) of the current page. + """ + url = build_uri_with_query_string(self._url, self._params) + + response = self._connection.get(url) + resources = response.get(self._members_field, []) + + if not len(resources): + raise exceptions.HPESimpliVityException(PAGINATION_NO_MORE_PAGES) + + for index, resource in enumerate(resources): + resources[index] = self._resource_obj.get_by_data(resource) + + self.data["resources"] = resources + self.data["size"] = len(resources) + + +class ResourceClient(object): + """Implements helper methods for resource classes.""" + + def __init__(self, connection, resource_obj): + """Initializes with a resource object and connection.""" + self._resource_obj = resource_obj + self._connection = connection + + def get_all(self, resource_url, members_field=None, pagination=False, + page_size=0, limit=500, offset=0, sort=None, order='descending', + filters=None, fields=None, case_sensitive=True, + show_optional_fields=False): + """Gets all resources. + + Args: + resource_url: URL of the resource + members_field: Name of the resource field(to fetch the resources from get call response) + pagination: Default value is False, set to True if pagination is required + page_size: Number of resources per page - mandatory field if pagination is on + limit: A positive integer that represents the maximum number of results to return + sort: The name of the field where the sort occurs + order: The sort order preference, valid values: ascending or descending + filters: Dictionary of filers, example: {'name': 'name'} + fields: A comma-separated list of fields to include in the returned objects. Default: all + case_sensitive: An indicator that specifies if the filter and sort results + use a case-sensitive or insensitive manner. Default: True + show_optional_fields: An indicator to show or not show the ha_status, + ha_resynchronization_progress, hypervisor_virtual_machine_power_state, and hypervisor_is_template + + Returns: + list/pagination object: Pagination object if pagination is on or list of resources + """ + query_params = {"limit": limit, + "offset": offset, + "order": order} + + if filters and isinstance(filters, dict): + query_params.update(filters) + + if fields: + query_params["fields"] = quote(fields) + + if show_optional_fields: + query_params["show_optional_fields"] = quote(show_optional_fields) + + query_params["sort"] = sort if sort else 'name' + query_params["case"] = "sensitive" if case_sensitive else "insensitive" + + if pagination: + if not page_size: + raise exceptions.HPESimpliVityException(PAGE_SIZE_NOT_SET) + + out = Pagination(self._connection, resource_url, self._resource_obj, + query_params, members_field, page_size) + else: + url = build_uri_with_query_string(resource_url, query_params) + response = self._connection.get(url) + data_list = response.get(members_field, []) + out = [] + for data in data_list: + out.append(self._resource_obj.get_by_data(data)) + + return out + + def task_affected_resources(self, task, timeout): + """Handles asynchronous calls. + + Args: + task: Task data retunrned by a REST call + timeout: Timeout value + + Returns: + list: Returns ids of affected resources + """ + task_obj = Task(self._connection, task) + affected_resources = task_obj.wait_for_task(timeout) + + return affected_resources + + def do_get(self, uri): + """Makes get requests + + Args: + uri: URI of the resource + + Returns: + Returns: Returns the resource data + """ + return self._connection.get(uri) + + def do_post(self, uri, data, timeout, custom_headers): + """Makes post requests. + + Args: + uri: URI of the resource. + data: Request body of the call + timeout: Time out for the request in seconds. + cutom_headers: Allows to add custom http headers. + + Returns: + list: Returns ids of the affected resources. + """ + task, entity = self._connection.post(uri, data, custom_headers=custom_headers) + + if not task: + return entity + + return self.task_affected_resources(task, timeout) + + def do_put(self, uri, data, timeout, custom_headers): + """Makes put requests. + + Args: + uri: URI of the resource + data: Request body of the call + timeout: Time out for the request in seconds. + custom_headers: Allows to set custom http headers. + + Retuns: + list: Returns ids of the affected resources. + """ + task, body = self._connection.put(uri, data, custom_headers=custom_headers) + + if not task: + return body + + return self.task_affected_resources(task, timeout) + + +class ResourceBase(object): + """Implements base class for resource classes.""" + + def __init__(self, connection): + """Initializes class with connection and resource client.""" + self._connection = connection + self._client = ResourceClient(self._connection, self) + + def get_by_name(self, name): + """Gets resource by name. + + Args: + name: Name of the resource + + Returns: + object: object of the resource + + Raises: + HPESimpliVityResourceNotFound: if resource doesn't exist with the name passed. + """ + resources = self.get_all(filters={'name': name}) + if not len(resources): + raise exceptions.HPESimpliVityResourceNotFound("Resource not found with the name {}".format(name)) + + return resources[0] + + def get_by_id(self, resource_id): + """Gets resource by id. + + Args: + id: ID of the resource + + Returns: + object: Resource object + + Raises: + HPESimpliVityResourceNotFound: if resource doesn't exist with the id passed. + """ + resources = self.get_all(filters={'id': resource_id}) + if not len(resources): + raise exceptions.HPESimpliVityResourceNotFound("Resource not found with the id {}".format(resource_id)) + + return resources[0] diff --git a/simplivity/resources/tasks.py b/simplivity/resources/tasks.py new file mode 100644 index 0000000..db337e8 --- /dev/null +++ b/simplivity/resources/tasks.py @@ -0,0 +1,141 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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. +## + +"""Implements operations for task.""" + +import logging +import time + +from simplivity import exceptions + +TASK_PENDING_STATES = ['IN_PROGRESS'] +TASK_ERROR_STATES = ['ERROR'] +TASK_COMPLETED_STATES = ['COMPLETED'] + +MSG_TIMEOUT = 'Waited %s seconds for task to complete, aborting' +MSG_INVALID_TASK = 'Invalid task was provided' + + +UNLIMITED_TIMEOUT = -1 +URL = '/tasks' + +logger = logging.getLogger(__name__) + + +class Task(object): + """Implements operations for task.""" + + def __init__(self, con, data): + """Initializes Task with connection and data.""" + self._connection = con + if 'task' in data: + self.data = data["task"] + else: + self.data = data + + self.state = self.data["state"] + + @staticmethod + def get_current_seconds(): + """Returns current time.""" + return int(time.time()) + + def wait_for_task(self, timeout=-1): + """Wait for task execution and return affected resources. + + Args: + timeout: timeout in seconds + + Returns: + list: Affected resources when creating or updating + """ + self.__wait_task_completion(timeout) + + self.update_status() + + logger.debug("Waiting for task. Task state: " + str(self.data.get('state'))) + + return self.get_affected_resources() + + def __wait_task_completion(self, timeout): + """Wait for task completion. + + Args: + timeout: timeout in seconds + """ + if not self.data: + raise exceptions.HPESimpliVityUnknownType(MSG_INVALID_TASK) + + logger.debug('Waiting for task completion...') + + # gets current cpu second for timeout + start_time = self.get_current_seconds() + # connection_failure_control = dict(last_success=self.get_current_seconds()) + + i = 0 + while self.is_task_running(): + # wait 1 to 10 seconds + # the value increases to avoid flooding server with requests + i = i + 1 if i < 10 else 10 + + logger.debug("Waiting for task. Task state: " + str(self.data.get('state'))) + + time.sleep(i) + if (timeout != UNLIMITED_TIMEOUT) and (start_time + timeout < self.get_current_seconds()): + raise exceptions.HPESimpliVityTimeout(MSG_TIMEOUT % str(timeout)) + + def is_task_running(self): + """ + Check if a task is running according to: TASK_PENDING_STATES + + Returns: + True when in TASK_PENDING_STATES; False when not. + """ + self.update_status() + if self.data['state'] in TASK_PENDING_STATES: + return True + + return False + + def update_status(self): + """ + Retrieve a task by its uri. + + Returns: + task dict + """ + task = self._connection.get("{}/{}".format(URL, self.data["id"])) + self.data = task["task"] + self.state = self.data["state"] + + return self.data + + def get_affected_resources(self): + """ + Retrieve a resource associated with a task. + + Args: + task: task dict + + Returns: + list: list of resource ids + """ + if self.state not in TASK_COMPLETED_STATES: + raise exceptions.HPESimpliVityException(self.data["message"]) + + affected_resources = self.data['affected_objects'] + + return affected_resources diff --git a/simplivity/resources/virtual_machines.py b/simplivity/resources/virtual_machines.py new file mode 100644 index 0000000..ca99805 --- /dev/null +++ b/simplivity/resources/virtual_machines.py @@ -0,0 +1,325 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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. +## + +"""Implements features available for Virtual Machine resource.""" + +from simplivity.resources.resource import ResourceBase +from simplivity.resources import datastores +from simplivity.resources import omnistack_clusters +from simplivity.resources import backups +from simplivity.resources import policies + +URL = '/virtual_machines' +DATA_FIELD = 'virtual_machines' + + +class VirtualMachines(ResourceBase): + """Implements features for SympliVity VM resources.""" + + def __init__(self, connection): + """Initialize VirtualMachines class.""" + super(VirtualMachines, self).__init__(connection) + + def get_all(self, pagination=False, page_size=0, limit=500, offset=0, + sort=None, order='descending', filters=None, fields=None, + case_sensitive=True, show_optional_fields=False): + """Get all vms. + + Args: + pagination: True if need pagination + page_size: Size of the page (Required when pagination is on) + limit: A positive integer that represents the maximum number of results to return + offset: A positive integer that directs the service to start returning + the instance, up to the limit. + sort: The name of the field where the sort occurs. + order: The sort order preference. Valid values: ascending or descending. + filters: Dictionary with filter values. Example: {'name': 'name'} + id: The unique identifier (UID) of the virtual_machines to return + Accepts: Single value, comma-separated list + name: The name of the virtual_machines to return + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + omnistack_cluster_id: The unique identifier (UID) of the omnistack_cluste + that is associated with the instances to return + Accepts: Single value, comma-separated list + omnistack_cluster_name: The name of the omnistack_cluster that + is associated with the instances to return. + Accepts: Single value, comma-separated list. + compute_cluster_parent_hypervisor_object_id: The unique identifier (UID) + of the hypervisor that contains the omnistack_cluster that is associated + with the instances to return + Accepts: Single value, comma-separated list. + compute_cluster_parent_name: The name of the hypervisor that contains the + omnistack_cluster that is associated with the instances to return + Accepts: Single value, comma-separated list + hypervisor_management_system: The IP address of the hypervisor associated + with the virtual machine. + Accepts: Single value, comma-separated list, pattern using one + or more asterisk characters as a wildcard + hypervisor_management_system_name: The name of the hypervisor associated + with the virtual machine + Accepts: Single value, comma-separated list, pattern using one or more + asterisk characters as a wildcard + datastore_id: The unique identifier (UID) of the datastore that is associated + with the instances to return + Accepts: Single value, comma-separated list + datastore_name: The name of the datastore that is associated with the + instances to return + Accepts: Single value, comma-separated list + policy_id: The unique identifier (UID) of the policy that is associated + with the instances to return + Accepts: Single value, comma-separated list + policy_name: The name of the policy that is associated with the instances to return + Accepts: Single value, comma-separated list + hypervisor_object_id: The unique identifier (UID) of the hypervisor-based instance + that is associated with the instances to return + Accepts: Single value, comma-separated list + created_after: The earliest creation time after the virtual machines to return were + created, expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + created_before: The latest creation time before the virtual machines to return were + created, expressed in ISO-8601 form, based on Coordinated Universal Time (UTC) + state: The state of the virtual_machine that is associated with the instances to return + Accepts: Single value, comma-separated list + app_aware_vm_status: The status of the ability of the virtual machine to take + an application-consistent backup that uses Microsoft VSS + Accepts: Single value, comma-separated list + hypervisor_is_template: An indicator that shows if the virtual machine is a template. + host_id: The unique identifier (UID) of the virtual_machine host. + fields: A comma-separated list of fields to include in the returned objects + case_sensitive: An indicator that specifies if the filter and sort results + use a case-sensitive or insensitive manner. + show_optional_fields: An indicator to show or not show the ha_status, + ha_resynchronization_progress, hypervisor_virtual_machine_power_state, + and hypervisor_is_template. + + Returns: + list/pagination object: list of VirtualMachine objects/ Pagination object + """ + return self._client.get_all(URL, + members_field=DATA_FIELD, + pagination=pagination, + page_size=page_size, + limit=limit, + offset=offset, + sort=sort, + order=order, + filters=filters, + fields=fields, + case_sensitive=case_sensitive, + show_optional_fields=show_optional_fields) + + def get_by_data(self, data): + """Gets VM object from VM data. + + Args: + data: VM data + + Returns: + object: Virtual Machine object. + """ + return VirtualMachine(self._connection, self._client, data) + + def set_policy_for_multiple_vms(self, policy, vms, timeout=-1): + """Sets the backup policy for virtual machines. + + Args: + vms: list of vm objects + policy: policy object + """ + method_url = "{}/set_policy".format(URL) + + vm_ids = [vm.data["id"] for vm in vms] + policy_id = policy.data["id"] + data = {"virtual_machine_id": vm_ids, + "policy_id": policy_id} + + affected_resources = self._client.do_post(method_url, data, timeout, None) + + vm_ids = [resource["object_id"] for resource in affected_resources] + comma_separated_ids = ','.join(vm_ids) + + return self.get_all(filters={'id': comma_separated_ids}) + + +class VirtualMachine(object): + """Implements features available for a single VM.""" + + def __init__(self, connection, resource_client, data): + """Initialize with connection object, resource client and VM data""" + self.data = data + self._connection = connection + self._client = resource_client + self._vms = VirtualMachines(self._connection) + + def __refresh(self): + """Updates the VM data.""" + resource_uri = "{}/{}".format(URL, self.data["id"]) + self.data = self._client.do_get(resource_uri) + + def clone(self, new_vm_name, app_consistent=False, datastore=None, timeout=-1): + """Clones a virtual machine. + + Args: + new_vm_name: The name of the virtual_machine created from this action. + app_consistent: An indicator to show if the backup represents a snapshot + of a virtual machine with data that was first flushed to disk. + datastore: Object/name of the datastore. + if passed, new VM will be moved to the datastore. + timeout: Time out for the request in seconds. + + Returns: + object: Object of the new VM + """ + method_url = "{}/{}/clone".format(URL, self.data["id"]) + data = {"virtual_machine_name": new_vm_name, + "app_consistent": app_consistent} + + out = self._client.do_post(method_url, data, timeout, None) + vm = self._vms.get_by_id(out[0]["object_id"]) + + if datastore: + return vm.move(new_vm_name, datastore) + + return vm + + def move(self, new_vm_name, datastore, timeout=-1): + """Moves a virtual machine to another datastore. + + Args: + new_vm_name: Name of the new vm + datastore: Object/name of the destination datastore + timeout: Time out for the request in seconds. + + Returns: + VirtualMachine object: Object of the moved VM + """ + method_url = "{}/{}/move".format(URL, self.data["id"]) + + if not isinstance(datastore, datastores.Datastore): + # if passed name of the datastore + datastores_obj = datastores.Datastores(self._connection) + datastore = datastores_obj.get_by_name(datastore) + + data = {"virtual_machine_name": new_vm_name, + "destination_datastore_id": datastore.data["id"]} + + affected_object = self._client.do_post(method_url, data, timeout, None)[0] + vm_obj = self._vms.get_by_id(affected_object["object_id"]) + self.data = vm_obj.data + + return self + + def create_backup(self, backup_name, cluster=None, app_consistent=False, + consistency_type=None, retention=0, timeout=-1): + """Backs up a virtual machine. + + Args: + backup_name: The name of the new backup created from this action. + cluster: Destination OmnistackCluster object/name. + app_consistent: An indicator to show if the backup represents + a snapshot of a virtual machine with data that was first flushed to disk. + consistency_type: The consistency type of the backup. + retention: The number of minutes to keep backups. + timeout: Time out for the request in seconds. + + Returns: + Backup object: object of the newly created backup. + """ + method_url = "{}/{}/backup".format(URL, self.data["id"]) + + if cluster and not isinstance(cluster, omnistack_clusters.OmnistackCluster): + # if passed name of the omnistack cluster + clusters_obj = omnistack_clusters.OmnistackClusters(self._connection) + cluster = clusters_obj.get_by_name(cluster) + + data = {"backup_name": backup_name, + "app_consistent": app_consistent, + "consistency_type": consistency_type, + "retention": retention} + + if cluster: + data["destination_id"] = cluster.data["id"] + + backup = self._client.do_post(method_url, data, timeout, None)[0] + return backups.Backups(self._connection).get_by_id(backup["object_id"]) + + def get_backups(self): + """Retrieves all backups associated with this virtual_machine. + + Returns: + list: List of backup objects + """ + method_url = "{}/{}/backups".format(URL, self.data["id"]) + backup_data = self._client.do_get(method_url).get("backups", []) + + backup_objs = [] + for backup in backup_data: + obj = backups.Backups(self._connection).get_by_id(backup["id"]) + backup_objs.append(obj) + + return backup_objs + + def set_backup_parameters(self, guest_username, guest_password, override_guest_validation=False, + app_aware_type=None, timeout=-1): + """Set the virtual machine backup parameters used for application consistent backups. + + Args: + guest_username: Username of the virtual machine. + guest_password: Password of the virtual machine. + override_guest_validation: Set to true to disable virtual machine validation logic. + app_aware_type: Set the application aware backup type: + VSS - Application-consistentbackup using Microsoft VSS + DEFAULT - Crash-consistent + NONE - Application-consistent backup using a VMware snapshot + timeout: Time out for the request in seconds. + + Returns: + self: Returns the same object. + """ + method_url = "{}/{}/backup_parameters".format(URL, self.data["id"]) + + data = {"guest_username": guest_username, + "guest_password": guest_password, + "override_guest_validation": override_guest_validation, + "app_aware_type": app_aware_type} + + self._client.do_post(method_url, data, timeout, None) + self.__refresh() + + return self + + def set_policy(self, policy, timeout=-1): + """Sets the backup policy for virtual machine. + + Args: + policy: Policy object/name + timeout: Time out for the request in seconds. + + Returns: + self: Returns the same object. + """ + method_url = "{}/{}/set_policy".format(URL, self.data["id"]) + + if not isinstance(policy, policies.Policy): + # if passed name of the policy + policy = policies.Policies(self._connection).get_by_name(policy) + + data = {"policy_id": policy.data["id"]} + + self._client.do_post(method_url, data, timeout, None) + self.__refresh() + + return self diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..99e3587 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,6 @@ +coverage +coveralls +flake8 +mock +pytest +six diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/resources/test_backups.py b/tests/unit/resources/test_backups.py new file mode 100644 index 0000000..34f88ff --- /dev/null +++ b/tests/unit/resources/test_backups.py @@ -0,0 +1,95 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import mock + +from simplivity.connection import Connection +from simplivity import exceptions +from simplivity.resources import backups + + +class BackupTest(unittest.TestCase): + def setUp(self): + self.connection = Connection('127.0.0.1') + self.connection._access_token = "123456789" + self.backups = backups.Backups(self.connection) + + @mock.patch.object(Connection, "get") + def test_get_all_returns_resource_obj(self, mock_get): + url = "{}?case=sensitive&limit=500&offset=0&order=descending&sort=name".format(backups.URL) + resource_data = [{'id': '12345'}, {'id': '67890'}] + mock_get.return_value = {backups.DATA_FIELD: resource_data} + + backup_objs = self.backups.get_all() + self.assertIsInstance(backup_objs[0], backups.Backup) + self.assertEquals(backup_objs[0].data, resource_data[0]) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_found(self, mock_get): + backup_name = "testname" + url = "{}?case=sensitive&limit=500&name={}&offset=0&order=descending&sort=name".format(backups.URL, backup_name) + resource_data = [{'id': '12345', 'name': backup_name}] + mock_get.return_value = {backups.DATA_FIELD: resource_data} + + backup_obj = self.backups.get_by_name(backup_name) + self.assertIsInstance(backup_obj, backups.Backup) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_not_found(self, mock_get): + backup_name = "testname" + resource_data = [] + mock_get.return_value = {backups.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.backups.get_by_name(backup_name) + + self.assertEquals(error.exception.message, "Resource not found with the name {}".format(backup_name)) + + @mock.patch.object(Connection, "get") + def test_get_by_id_found(self, mock_get): + backup_id = "12345" + url = "{}?case=sensitive&id={}&limit=500&offset=0&order=descending&sort=name".format(backups.URL, backup_id) + resource_data = [{'id': backup_id}] + mock_get.return_value = {backups.DATA_FIELD: resource_data} + + backup_obj = self.backups.get_by_id(backup_id) + self.assertIsInstance(backup_obj, backups.Backup) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_id_not_found(self, mock_get): + backup_id = "12345" + resource_data = [] + mock_get.return_value = {backups.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.backups.get_by_id(backup_id) + + self.assertEquals(error.exception.message, "Resource not found with the id {}".format(backup_id)) + + def test_get_by_data(self): + resource_data = {'id': '12345'} + + backup_obj = self.backups.get_by_data(resource_data) + self.assertIsInstance(backup_obj, backups.Backup) + self.assertEquals(backup_obj.data, resource_data) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/resources/test_datastores.py b/tests/unit/resources/test_datastores.py new file mode 100644 index 0000000..c7ec479 --- /dev/null +++ b/tests/unit/resources/test_datastores.py @@ -0,0 +1,95 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import mock + +from simplivity.connection import Connection +from simplivity import exceptions +from simplivity.resources import omnistack_clusters as clusters + + +class OmnistackClustersTest(unittest.TestCase): + def setUp(self): + self.connection = Connection('127.0.0.1') + self.connection._access_token = "123456789" + self.clusters = clusters.OmnistackClusters(self.connection) + + @mock.patch.object(Connection, "get") + def test_get_all_returns_resource_obj(self, mock_get): + url = "{}?case=sensitive&limit=500&offset=0&order=descending&sort=name".format(clusters.URL) + resource_data = [{'id': '12345'}, {'id': '67890'}] + mock_get.return_value = {clusters.DATA_FIELD: resource_data} + + objs = self.clusters.get_all() + self.assertIsInstance(objs[0], clusters.OmnistackCluster) + self.assertEquals(objs[0].data, resource_data[0]) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_found(self, mock_get): + name = "testname" + url = "{}?case=sensitive&limit=500&name={}&offset=0&order=descending&sort=name".format(clusters.URL, name) + resource_data = [{'id': '12345', 'name': name}] + mock_get.return_value = {clusters.DATA_FIELD: resource_data} + + obj = self.clusters.get_by_name(name) + self.assertIsInstance(obj, clusters.OmnistackCluster) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_not_found(self, mock_get): + name = "testname" + resource_data = [] + mock_get.return_value = {clusters.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.clusters.get_by_name(name) + + self.assertEquals(error.exception.message, "Resource not found with the name {}".format(name)) + + @mock.patch.object(Connection, "get") + def test_get_by_id_found(self, mock_get): + resource_id = "12345" + url = "{}?case=sensitive&id={}&limit=500&offset=0&order=descending&sort=name".format(clusters.URL, resource_id) + resource_data = [{'id': resource_id}] + mock_get.return_value = {clusters.DATA_FIELD: resource_data} + + obj = self.clusters.get_by_id(resource_id) + self.assertIsInstance(obj, clusters.OmnistackCluster) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_id_not_found(self, mock_get): + resource_id = "12345" + resource_data = [] + mock_get.return_value = {clusters.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.clusters.get_by_id(resource_id) + + self.assertEquals(error.exception.message, "Resource not found with the id {}".format(resource_id)) + + def test_get_by_data(self): + resource_data = {'id': '12345'} + + obj = self.clusters.get_by_data(resource_data) + self.assertIsInstance(obj, clusters.OmnistackCluster) + self.assertEquals(obj.data, resource_data) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/resources/test_hosts.py b/tests/unit/resources/test_hosts.py new file mode 100644 index 0000000..d81c255 --- /dev/null +++ b/tests/unit/resources/test_hosts.py @@ -0,0 +1,95 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import mock + +from simplivity.connection import Connection +from simplivity import exceptions +from simplivity.resources import hosts + + +class HostsTest(unittest.TestCase): + def setUp(self): + self.connection = Connection('127.0.0.1') + self.connection._access_token = "123456789" + self.hosts = hosts.Hosts(self.connection) + + @mock.patch.object(Connection, "get") + def test_get_all_returns_resource_obj(self, mock_get): + url = "{}?case=sensitive&limit=500&offset=0&order=descending&sort=name".format(hosts.URL) + resource_data = [{'id': '12345'}, {'id': '67890'}] + mock_get.return_value = {hosts.DATA_FIELD: resource_data} + + objs = self.hosts.get_all() + self.assertIsInstance(objs[0], hosts.Host) + self.assertEquals(objs[0].data, resource_data[0]) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_found(self, mock_get): + name = "testname" + url = "{}?case=sensitive&limit=500&name={}&offset=0&order=descending&sort=name".format(hosts.URL, name) + resource_data = [{'id': '12345', 'name': name}] + mock_get.return_value = {hosts.DATA_FIELD: resource_data} + + obj = self.hosts.get_by_name(name) + self.assertIsInstance(obj, hosts.Host) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_not_found(self, mock_get): + name = "testname" + resource_data = [] + mock_get.return_value = {hosts.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.hosts.get_by_name(name) + + self.assertEquals(error.exception.message, "Resource not found with the name {}".format(name)) + + @mock.patch.object(Connection, "get") + def test_get_by_id_found(self, mock_get): + resource_id = "12345" + url = "{}?case=sensitive&id={}&limit=500&offset=0&order=descending&sort=name".format(hosts.URL, resource_id) + resource_data = [{'id': resource_id}] + mock_get.return_value = {hosts.DATA_FIELD: resource_data} + + obj = self.hosts.get_by_id(resource_id) + self.assertIsInstance(obj, hosts.Host) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_id_not_found(self, mock_get): + resource_id = "12345" + resource_data = [] + mock_get.return_value = {hosts.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.hosts.get_by_id(resource_id) + + self.assertEquals(error.exception.message, "Resource not found with the id {}".format(resource_id)) + + def test_get_by_data(self): + resource_data = {'id': '12345'} + + obj = self.hosts.get_by_data(resource_data) + self.assertIsInstance(obj, hosts.Host) + self.assertEquals(obj.data, resource_data) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/resources/test_omnistack_clusters.py b/tests/unit/resources/test_omnistack_clusters.py new file mode 100644 index 0000000..3eaa686 --- /dev/null +++ b/tests/unit/resources/test_omnistack_clusters.py @@ -0,0 +1,95 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import mock + +from simplivity.connection import Connection +from simplivity import exceptions +from simplivity.resources import datastores + + +class DatastoresTest(unittest.TestCase): + def setUp(self): + self.connection = Connection('127.0.0.1') + self.connection._access_token = "123456789" + self.datastores = datastores.Datastores(self.connection) + + @mock.patch.object(Connection, "get") + def test_get_all_returns_resource_obj(self, mock_get): + url = "{}?case=sensitive&limit=500&offset=0&order=descending&sort=name".format(datastores.URL) + resource_data = [{'id': '12345'}, {'id': '67890'}] + mock_get.return_value = {datastores.DATA_FIELD: resource_data} + + objs = self.datastores.get_all() + self.assertIsInstance(objs[0], datastores.Datastore) + self.assertEquals(objs[0].data, resource_data[0]) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_found(self, mock_get): + name = "testname" + url = "{}?case=sensitive&limit=500&name={}&offset=0&order=descending&sort=name".format(datastores.URL, name) + resource_data = [{'id': '12345', 'name': name}] + mock_get.return_value = {datastores.DATA_FIELD: resource_data} + + obj = self.datastores.get_by_name(name) + self.assertIsInstance(obj, datastores.Datastore) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_not_found(self, mock_get): + name = "testname" + resource_data = [] + mock_get.return_value = {datastores.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.datastores.get_by_name(name) + + self.assertEquals(error.exception.message, "Resource not found with the name {}".format(name)) + + @mock.patch.object(Connection, "get") + def test_get_by_id_found(self, mock_get): + resource_id = "12345" + url = "{}?case=sensitive&id={}&limit=500&offset=0&order=descending&sort=name".format(datastores.URL, resource_id) + resource_data = [{'id': resource_id}] + mock_get.return_value = {datastores.DATA_FIELD: resource_data} + + obj = self.datastores.get_by_id(resource_id) + self.assertIsInstance(obj, datastores.Datastore) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_id_not_found(self, mock_get): + resource_id = "12345" + resource_data = [] + mock_get.return_value = {datastores.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.datastores.get_by_id(resource_id) + + self.assertEquals(error.exception.message, "Resource not found with the id {}".format(resource_id)) + + def test_get_by_data(self): + resource_data = {'id': '12345'} + + obj = self.datastores.get_by_data(resource_data) + self.assertIsInstance(obj, datastores.Datastore) + self.assertEquals(obj.data, resource_data) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/resources/test_policies.py b/tests/unit/resources/test_policies.py new file mode 100644 index 0000000..ab9df84 --- /dev/null +++ b/tests/unit/resources/test_policies.py @@ -0,0 +1,95 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import mock + +from simplivity.connection import Connection +from simplivity import exceptions +from simplivity.resources import policies + + +class PoliciesTest(unittest.TestCase): + def setUp(self): + self.connection = Connection('127.0.0.1') + self.connection._access_token = "123456789" + self.policies = policies.Policies(self.connection) + + @mock.patch.object(Connection, "get") + def test_get_all_returns_resource_obj(self, mock_get): + url = "{}?case=sensitive&limit=500&offset=0&order=descending&sort=name".format(policies.URL) + resource_data = [{'id': '12345'}, {'id': '67890'}] + mock_get.return_value = {policies.DATA_FIELD: resource_data} + + objs = self.policies.get_all() + self.assertIsInstance(objs[0], policies.Policy) + self.assertEquals(objs[0].data, resource_data[0]) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_found(self, mock_get): + name = "testname" + url = "{}?case=sensitive&limit=500&name={}&offset=0&order=descending&sort=name".format(policies.URL, name) + resource_data = [{'id': '12345', 'name': name}] + mock_get.return_value = {policies.DATA_FIELD: resource_data} + + obj = self.policies.get_by_name(name) + self.assertIsInstance(obj, policies.Policy) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_not_found(self, mock_get): + name = "testname" + resource_data = [] + mock_get.return_value = {policies.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.policies.get_by_name(name) + + self.assertEquals(error.exception.message, "Resource not found with the name {}".format(name)) + + @mock.patch.object(Connection, "get") + def test_get_by_id_found(self, mock_get): + resource_id = "12345" + url = "{}?case=sensitive&id={}&limit=500&offset=0&order=descending&sort=name".format(policies.URL, resource_id) + resource_data = [{'id': resource_id}] + mock_get.return_value = {policies.DATA_FIELD: resource_data} + + obj = self.policies.get_by_id(resource_id) + self.assertIsInstance(obj, policies.Policy) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_id_not_found(self, mock_get): + resource_id = "12345" + resource_data = [] + mock_get.return_value = {policies.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.policies.get_by_id(resource_id) + + self.assertEquals(error.exception.message, "Resource not found with the id {}".format(resource_id)) + + def test_get_by_data(self): + resource_data = {'id': '12345'} + + obj = self.policies.get_by_data(resource_data) + self.assertIsInstance(obj, policies.Policy) + self.assertEquals(obj.data, resource_data) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/resources/test_resource.py b/tests/unit/resources/test_resource.py new file mode 100644 index 0000000..e253747 --- /dev/null +++ b/tests/unit/resources/test_resource.py @@ -0,0 +1,137 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import mock + +from simplivity.connection import Connection +from simplivity import exceptions +from simplivity.resources.resource import ResourceClient, Pagination +from simplivity.resources.resource import PAGE_SIZE_NOT_SET + + +class ResourceStub(): + def __init__(self): + self.data = None + + def get_by_data(self, data): + self.data = data + return self + + +class ResourceTest(unittest.TestCase): + + def setUp(self): + self.Connection = Connection('127.0.0.1') + resource_obj = ResourceStub() + self.resource_client = ResourceClient(self.Connection, resource_obj) + + @mock.patch.object(Connection, "get") + def test_get_all_called_once(self, mock_get): + filter = {'name': 'test'} + sort = 'name' + optional_fields = "testfield,testfield1" + fields = "field1,field2" + mock_get.return_value = {'member_field': [{'name': 'testname', 'id': '1234567'}]} + + result = self.resource_client.get_all('/api/resource', 'member_field', + filters=filter, sort=sort, + show_optional_fields=optional_fields, + fields=fields) + + url = '/api/resource?case=sensitive&fields={}&limit=500&name={}' \ + '&offset=0&order=descending&show_optional_fields={}' \ + '&sort={}'.format('field1%2Cfield2', filter["name"], 'testfield%2Ctestfield1', sort) + + self.assertIsInstance(result[0], ResourceStub) + self.assertEqual({'name': 'testname', 'id': '1234567'}, result[0].data) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_all_with_defaults(self, mock_get): + self.resource_client.get_all('/api/resource') + uri = '/api/resource?case=sensitive&limit=500&offset=0&order=descending&sort=name' + + mock_get.assert_called_once_with(uri) + + @mock.patch.object(Connection, "get") + def test_get_all_should_return_empty_list_when_response_has_no_items(self, mock_get): + mock_get.return_value = {} + result = self.resource_client.get_all('/api/resource') + self.assertEqual(result, []) + + @mock.patch.object(Connection, "get") + def test_get_all_with_pagination(self, mock_get): + result = self.resource_client.get_all('/api/resource', pagination=True, page_size=10) + self.assertIsInstance(result, Pagination) + + @mock.patch.object(Connection, "get") + def test_get_all_with_paginatin_without_page_size(self, mock_get): + with self.assertRaises(exceptions.HPESimpliVityException) as error: + self.resource_client.get_all('/api/resource', pagination=True) + self.assertEqual(error.exception.message, PAGE_SIZE_NOT_SET) + + @mock.patch.object(Connection, "get") + def test_get_call(self, mock_get): + url = "/api/resource" + self.resource_client.do_get(url) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "post") + def test_post_call(self, mock_post): + url = "/api/resource" + data = {"name": "name"} + mock_post.return_value = None, {} + self.resource_client.do_post(url, data, -1, None) + mock_post.assert_called_once_with(url, data, custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_post_call_with_task(self, mock_get, mock_post): + url = "/api/resource" + data = {"name": "name"} + affected_objects = ["1234567"] + mock_post.return_value = {'id': '12345', 'state': 'INPROGRESS'}, {} + mock_get.return_value = {'task': {'id': '12345', 'state': 'COMPLETED', 'affected_objects': affected_objects}} + result = self.resource_client.do_post(url, data, -1, None) + + mock_post.assert_called_once_with(url, data, custom_headers=None) + self.assertEqual(result, affected_objects) + + @mock.patch.object(Connection, "put") + def test_put_call(self, mock_put): + url = "/api/resource" + data = {"name": "name"} + mock_put.return_value = None, {} + self.resource_client.do_put(url, data, -1, None) + mock_put.assert_called_once_with(url, data, custom_headers=None) + + @mock.patch.object(Connection, "put") + @mock.patch.object(Connection, "get") + def test_put_call_with_task(self, mock_get, mock_put): + url = "/api/resource" + data = {"name": "name"} + affected_objects = ["1234567"] + mock_put.return_value = {'id': '12345', 'state': 'INPROGRESS'}, {} + mock_get.return_value = {'task': {'id': '12345', 'state': 'COMPLETED', 'affected_objects': affected_objects}} + result = self.resource_client.do_put(url, data, -1, None) + + mock_put.assert_called_once_with(url, data, custom_headers=None) + self.assertEqual(result, affected_objects) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/resources/test_tasks.py b/tests/unit/resources/test_tasks.py new file mode 100644 index 0000000..6f54fb1 --- /dev/null +++ b/tests/unit/resources/test_tasks.py @@ -0,0 +1,98 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 mock import mock, call + +from simplivity.connection import Connection +from simplivity.resources.tasks import Task +from simplivity import exceptions + +ERR_MSG = "Message error" + + +class TaskTest(unittest.TestCase): + def setUp(self): + self.host = '127.0.0.1' + self.connection = Connection(self.host) + self.task_data = {"state": "IN_PROGRESS", 'id': '123456'} + self.task = Task(self.connection, self.task_data) + + @mock.patch.object(Connection, 'get') + def test_is_task_running(self, mock_get): + mock_get.return_value = {'task': self.task_data} + self.assertTrue(self.task.is_task_running()) + + @mock.patch.object(Connection, 'get') + def test_is_task_running_false(self, mock_get): + updated_task_data = self.task_data.copy() + updated_task_data["state"] = 'COMPLETED' + mock_get.return_value = {'task': updated_task_data} + + self.assertFalse(self.task.is_task_running()) + + @mock.patch.object(Connection, 'get') + def test_is_task_running_with_generic_failure(self, mock_get): + mock_get.side_effect = Exception(ERR_MSG) + self.assertRaises(Exception, self.task.is_task_running) + + @mock.patch.object(Task, 'is_task_running') + def test_wait_for_task_timeout(self, mock_is_running): + mock_is_running.return_value = True + timeout = 2 + + with self.assertRaises(exceptions.HPESimpliVityTimeout) as error: + self.task.wait_for_task(timeout) + + self.assertEqual('Waited {} seconds for task to complete, aborting'.format(timeout), + error.exception.message) + + @mock.patch.object(Task, 'is_task_running') + @mock.patch('time.sleep') + def test_wait_for_task_increasing_sleep(self, mock_sleep, mock_is_running): + + mock_is_running.return_value = True + timeout = 0.1 + + # should call sleep increasing 1 until 10 + calls = [call(1), call(2), call(3), call(4), call(5), call(6), call(7), + call(8), call(9), call(10), call(10), call(10)] + + with self.assertRaises(exceptions.HPESimpliVityTimeout) as error: + self.task.wait_for_task(timeout) + + mock_sleep.assert_has_calls(calls) + self.assertEqual('Waited {} seconds for task to complete, aborting'.format(timeout), + error.exception.message) + + @mock.patch.object(Task, 'is_task_running') + @mock.patch.object(Connection, 'get') + def test_wait_for_task(self, mock_get, mock_is_running): + task = {'state': 'COMPLETED', 'id': '12345'} + affected_objects = ['12345'] + self.task.data = task + + mock_is_running.return_value = False + task_completed = task.copy() + task_completed["affected_objects"] = affected_objects + mock_get.return_value = {'task': task_completed} + + ret_entity = self.task.wait_for_task() + self.assertEqual(ret_entity, affected_objects) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/resources/test_virtual_machines.py b/tests/unit/resources/test_virtual_machines.py new file mode 100644 index 0000000..e440c67 --- /dev/null +++ b/tests/unit/resources/test_virtual_machines.py @@ -0,0 +1,278 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import mock +from mock import call + +from simplivity.connection import Connection +from simplivity import exceptions +from simplivity.resources import virtual_machines as machines +from simplivity.resources import policies +from simplivity.resources import datastores +from simplivity.resources import omnistack_clusters + + +class VirtualMachinesTest(unittest.TestCase): + def setUp(self): + self.connection = Connection('127.0.0.1') + self.connection._access_token = "123456789" + self.machines = machines.VirtualMachines(self.connection) + self.policies = policies.Policies(self.connection) + self.datastores = datastores.Datastores(self.connection) + self.clusters = omnistack_clusters.OmnistackClusters(self.connection) + + @mock.patch.object(Connection, "get") + def test_get_all_returns_resource_obj(self, mock_get): + url = "{}?case=sensitive&limit=500&offset=0&order=descending&sort=name".format(machines.URL) + resource_data = [{'id': '12345'}, {'id': '67890'}] + mock_get.return_value = {machines.DATA_FIELD: resource_data} + + vm_objs = self.machines.get_all() + self.assertIsInstance(vm_objs[0], machines.VirtualMachine) + self.assertEquals(vm_objs[0].data, resource_data[0]) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_found(self, mock_get): + vm_name = "testname" + url = "{}?case=sensitive&limit=500&name={}&offset=0&order=descending&sort=name".format(machines.URL, vm_name) + resource_data = [{'id': '12345', 'name': vm_name}] + mock_get.return_value = {machines.DATA_FIELD: resource_data} + + vm_obj = self.machines.get_by_name(vm_name) + self.assertIsInstance(vm_obj, machines.VirtualMachine) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_name_not_found(self, mock_get): + vm_name = "testname" + resource_data = [] + mock_get.return_value = {machines.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.machines.get_by_name(vm_name) + + self.assertEquals(error.exception.message, "Resource not found with the name {}".format(vm_name)) + + @mock.patch.object(Connection, "get") + def test_get_by_id_found(self, mock_get): + vm_id = "12345" + url = "{}?case=sensitive&id={}&limit=500&offset=0&order=descending&sort=name".format(machines.URL, vm_id) + resource_data = [{'id': vm_id}] + mock_get.return_value = {machines.DATA_FIELD: resource_data} + + vm_obj = self.machines.get_by_id(vm_id) + self.assertIsInstance(vm_obj, machines.VirtualMachine) + mock_get.assert_called_once_with(url) + + @mock.patch.object(Connection, "get") + def test_get_by_id_not_found(self, mock_get): + vm_id = "12345" + resource_data = [] + mock_get.return_value = {machines.DATA_FIELD: resource_data} + + with self.assertRaises(exceptions.HPESimpliVityResourceNotFound) as error: + self.machines.get_by_id(vm_id) + + self.assertEquals(error.exception.message, "Resource not found with the id {}".format(vm_id)) + + def test_get_by_data(self): + resource_data = {'id': '12345'} + + vm_obj = self.machines.get_by_data(resource_data) + self.assertIsInstance(vm_obj, machines.VirtualMachine) + self.assertEquals(vm_obj.data, resource_data) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_set_policy_in_bulk(self, mock_get, mock_post): + mock_post.return_value = {}, {} + vm1_data = {'name': 'name1', 'id': 'id1'} + vm2_data = {'name': 'name2', 'id': 'id2'} + vm_objs = [self.machines.get_by_data(vm1_data), + self.machines.get_by_data(vm2_data)] + policy_obj = self.policies.get_by_data({'name': 'name', 'id': 'id'}) + + mock_get.side_effect = [vm1_data, vm2_data] + + self.machines.set_policy_in_bulk(policy_obj, vm_objs) + mock_post.assert_called_once_with('/virtual_machines/set_policy', + {'policy_id': 'id', 'virtual_machine_id': ['id1', 'id2']}, + custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_clone(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.return_value = {'virtual_machines': {'id': '12345'}} + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.clone('new_vm_name') + + mock_post.assert_called_once_with('/virtual_machines/12345/clone', + {'app_consistent': False, 'virtual_machine_name': 'new_vm_name'}, + custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + @mock.patch.object(machines.VirtualMachine, "move") + def test_clone_with_datastore_name(self, mock_move, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.return_value = {'virtual_machines': {'id': '12345'}} + datastore_name = 'testdatastore' + new_vm_name = "new_vm_name" + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.clone(new_vm_name, datastore=datastore_name) + + mock_post.assert_called_once_with('/virtual_machines/12345/clone', + {'app_consistent': False, 'virtual_machine_name': 'new_vm_name'}, + custom_headers=None) + + mock_move.assert_called_once_with(new_vm_name, datastore_name) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_move_with_datastore_name(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.side_effect = [{'datastores': [{'name': 'datastore', 'id': '12345'}]}, + {'virtual_machines': {'id': '12345'}}] + new_vm_name = "new_vm_name" + datastore_name = "datastorename" + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.move(new_vm_name, datastore_name) + + mock_post.assert_called_once_with('/virtual_machines/12345/move', + {'destination_datastore_id': '12345', 'virtual_machine_name': new_vm_name}, + custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_move_with_datastore_obj(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.return_value = {'virtual_machines': {'id': '12345'}} + new_vm_name = "new_vm_name" + datastore_obj = self.datastores.get_by_data({'id': '12345', 'name': 'name'}) + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.move(new_vm_name, datastore_obj) + + mock_post.assert_called_once_with('/virtual_machines/12345/move', + {'destination_datastore_id': '12345', 'virtual_machine_name': new_vm_name}, + custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_create_backup_with_cluster_name(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.side_effect = [{'omnistack_clusters': [{'name': 'name', 'id': '12345'}]}, + {'backups': {'id': '12345'}}] + cluster_name = "cluster_name" + backup_name = "backup name" + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.create_backup(backup_name, cluster_name) + + mock_post.assert_called_once_with('/virtual_machines/12345/backup', + {'app_consistent': False, 'backup_name': 'backup name', + 'consistency_type': None, 'destination_id': '12345', 'retention': 0}, + custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_create_backup_with_cluster_obj(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.return_value = {'backups': {'id': '12345'}} + backup_name = "backup name" + cluster_obj = self.clusters.get_by_data({'id': '12345', 'name': 'name'}) + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.create_backup(backup_name, cluster_obj) + + mock_post.assert_called_once_with('/virtual_machines/12345/backup', + {'app_consistent': False, 'backup_name': backup_name, + 'consistency_type': None, 'destination_id': '12345', 'retention': 0}, + custom_headers=None) + + @mock.patch.object(Connection, "get") + def test_get_backups(self, mock_get): + mock_get.side_effect = [{'backups': [{'id': '12345'}]}, {'backups': [{'id': '12345'}]}] + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.get_backups() + + mock_get.assert_has_calls([call('/virtual_machines/12345/backups'), + call('/backups?case=sensitive&id=12345&limit=500&offset=0&order=descending&sort=name')]) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_setup_backup_parameters(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.return_value = {'virtual_machines': [{'name': 'name', 'id': '12345'}]} + + username = "username" + password = "password" + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.set_backup_parameters(username, password) + + mock_post.assert_called_once_with('/virtual_machines/12345/backup_parameters', + {'app_aware_type': None, 'override_guest_validation': False, + 'guest_username': username, 'guest_password': password}, + custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_set_policy_with_policy_name(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.side_effect = [{'policies': [{'name': 'datastore', 'id': '12345'}]}, + {'virtual_machines': {'id': '12345'}}] + policy_name = "policy name" + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.set_policy(policy_name) + + mock_post.assert_called_once_with('/virtual_machines/12345/set_policy', {'policy_id': '12345'}, + custom_headers=None) + + @mock.patch.object(Connection, "post") + @mock.patch.object(Connection, "get") + def test_set_policy_with_policy_obj(self, mock_get, mock_post): + mock_post.return_value = None, [{'object_id': '12345'}] + mock_get.return_value = {'virtual_machines': {'id': '12345'}} + policy_obj = self.policies.get_by_data({'id': 'policy12345', 'name': 'name'}) + + vm1_data = {'name': 'name1', 'id': '12345'} + vm = self.machines.get_by_data(vm1_data) + vm.set_policy(policy_obj) + + mock_post.assert_called_once_with('/virtual_machines/12345/set_policy', + {'policy_id': 'policy12345'}, custom_headers=None) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py new file mode 100644 index 0000000..14db2d5 --- /dev/null +++ b/tests/unit/test_connection.py @@ -0,0 +1,259 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 ssl +import unittest + +from mock import patch, call, Mock, ANY +from http.client import HTTPSConnection, HTTPException +from simplivity.connection import Connection +from simplivity.exceptions import HPESimpliVityException + + +class ConnectionTest(unittest.TestCase): + def setUp(self): + self.host = '127.0.0.1' + self.connection = Connection(self.host) + self.connection._access_token = "123456789" + + self.new_content_type = { + 'Content-type': 'application/vnd.simplivity.v1.7+json' + } + self.default_headers = {'Content-type': 'application/vnd.simplivity.v1.8+json', + 'Authorization': 'Bearer 123456789', + 'Accept': 'application/vnd.simplivity.v1+json'} + + self.updated_headers = self.default_headers.copy() + self.updated_headers.update(self.new_content_type) + + self.request_body = {"request body": "content"} + self.response_body = {"response body": "content"} + + self.dumped_request_body = json.dumps(self.request_body.copy()) + self.expected_response_body = self.response_body.copy() + + def __make_http_response(self, status): + mock_response = Mock(status=status) + mock_response.read.return_value = json.dumps(self.response_body).encode('utf-8') + return mock_response + + def __create_fake_mapped_file(self): + mock_mapped_file = Mock() + mock_mapped_file.tell.side_effect = [0, 1048576, 2097152, 2621440] # 0, 1MB, 2MB 2.5MB + mock_mapped_file.size.return_value = 2621440 # 2.5MB + mock_mapped_file.read.side_effect = ['data chunck 1', 'data chunck 2', 'data chunck 3'] + return mock_mapped_file + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_post_when_response_is_task(self, mock_response, mock_request): + mock_request.return_value = {} + + fake_task = {"task": {"state": "COMPLETED"}} + + response = Mock(status=202) + response.read.return_value = json.dumps(fake_task).encode('utf-8') + response.getheader.return_value = '' + mock_response.return_value = response + + task, body = self.connection.post('/path', self.request_body) + self.assertEqual(task, fake_task) + self.assertEqual(body, fake_task) + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_post_when_response_is_not_a_task(self, mock_response, mock_request): + mock_request.return_value = {} + + response = Mock(status=202) + response.read.return_value = json.dumps(self.response_body).encode('utf-8') + response.getheader.return_value = '' + mock_response.return_value = response + + task, body = self.connection.post('/path', self.request_body) + + self.assertEqual(task, None) + self.assertEqual(body, self.response_body) + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_post_should_do_rest_call_when_status_ok(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=200) + + self.connection.post('/path', self.request_body) + + mock_request.assert_called_once_with('POST', "https://{}/api/path".format(self.host), + self.dumped_request_body, self.default_headers) + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_post_should_send_updated_headers_when_headers_provided(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=202) + + self.connection.post('/path', self.request_body, custom_headers=self.new_content_type) + + expected_calls = [call('POST', ANY, ANY, self.updated_headers)] + self.assertEqual(expected_calls, mock_request.call_args_list) + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_post_should_raise_exception_when_status_internal_error(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=401) + + try: + self.connection.post('/path', self.request_body) + except HPESimpliVityException as e: + self.assertEqual(str(e), str((None, self.expected_response_body))) + else: + self.fail() + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_post_should_raise_exception_when_status_forbidden(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=403) + + try: + self.connection.post('/path', self.request_body) + except HPESimpliVityException as e: + self.assertEqual(str(e), str((None, self.expected_response_body))) + else: + self.fail() + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_post_should_raise_exception_when_status_not_found(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=404) + + try: + self.connection.post('/path', self.request_body) + except HPESimpliVityException as e: + self.assertEqual(str(e), str((None, self.expected_response_body))) + else: + self.fail() + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_put_should_do_rest_call_when_status_ok(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=200) + + self.connection.put('/path', self.request_body) + + mock_request.assert_called_once_with('PUT', + 'https://{}/api/path'.format(self.host), + self.dumped_request_body, self.default_headers) + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_put_should_return_body_when_status_ok(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=200) + + result = self.connection.put('/path', self.response_body, custom_headers=self.new_content_type) + + expected_result = (None, self.expected_response_body) + self.assertEqual(result, expected_result) + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_delete_should_do_rest_calls_when_status_ok(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=200) + + self.connection.delete('/path') + + mock_request.assert_called_once_with('DELETE', + 'https://{}/api/path'.format(self.host), + json.dumps({}), self.default_headers) + + @patch.object(HTTPSConnection, 'request') + @patch.object(HTTPSConnection, 'getresponse') + def test_delete_should_return_body_when_status_ok(self, mock_response, mock_request): + mock_request.return_value = {} + mock_response.return_value = self.__make_http_response(status=200) + + result = self.connection.delete('/path', + custom_headers=self.new_content_type) + + expected_result = (None, self.expected_response_body) + self.assertEqual(result, expected_result) + + @patch.object(Connection, 'do_http') + def test_task_in_response_body(self, mock_do_http): + mockedResponse = type('mockResponse', (), {'status': 200})() + mockedTaskBody = {'task': {'state': 'INPROGRESS'}} + mock_do_http.return_value = (mockedResponse, mockedTaskBody) + + (testTask, testBody) = self.connection._Connection__do_rest_call('PUT', '/path', '{ "body": "test" }', None) + + self.assertEqual(mockedTaskBody, testTask) + self.assertEqual(mockedTaskBody, testBody) + + @patch.object(Connection, 'get_connection') + def test_do_http_with_timeout_error(self, mock_get_connection): + + mock_conn = mock_get_connection.return_value = Mock() + mock_response = Mock() + mock_conn.getresponse.side_effect = [HTTPException('timed out'), mock_response] + + with self.assertRaises(HPESimpliVityException) as context: + resp, body = self.connection.do_http('POST', '/rest/test', 'body') + + self.assertTrue('timed out' in context.exception.msg) + + @patch.object(Connection, 'do_http') + def test_login(self, mock_post): + mock_post.return_value = (None, {'access_token': '1234567'}) + + self.connection.login('username', 'password') + + self.assertEqual(self.connection._access_token, '1234567') + + @patch.object(Connection, 'login') + @patch.object(Connection, 'get_connection') + def test_login_again_if_token_expired(self, mock_connection, mock_login): + mock_response1 = Mock() + mock_response1.read.return_value = json.dumps({'error': 'invalid_token'}).encode('utf-8') + + mock_response2 = Mock() + mock_response2.read.return_value = json.dumps({'access_token': '1234567'}).encode('utf-8') + + mock_conn = mock_connection.return_value = Mock() + mock_conn.getresponse.side_effect = [mock_response1, mock_response2] + + self.connection._username = 'username' + self.connection._password = 'password' + + resp, body = self.connection.do_http('POST', '/rest/test', 'body') + mock_login.assert_called_once_with('username', 'password') + + def test_get_connection_ssl_trust_all(self): + + conn = self.connection.get_connection() + + self.assertEqual(conn.host, '127.0.0.1') + self.assertEqual(conn.port, 443) + self.assertEqual(conn._context.protocol, ssl.PROTOCOL_TLSv1_2) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py new file mode 100644 index 0000000..d491f02 --- /dev/null +++ b/tests/unit/test_exceptions.py @@ -0,0 +1,112 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 +import os +import tempfile +import pickle + +from simplivity import exceptions + + +class ExceptionsTest(unittest.TestCase): + def test_exception_constructor_with_string(self): + exception = exceptions.HPESimpliVityException("A message string") + + self.assertEqual(exception.msg, "A message string") + self.assertEqual(exception.response, None) + self.assertEqual(exception.args[0], "A message string") + self.assertEqual(len(exception.args), 1) + + def test_exception_constructor_with_valid_dict(self): + exception = exceptions.HPESimpliVityException({'message': "A message string"}) + + self.assertEqual(exception.msg, "A message string") + self.assertEqual(exception.response, {'message': "A message string"}) + self.assertEqual(exception.args[0], "A message string") + self.assertEqual(exception.args[1], {'message': 'A message string'}) + + def test_exception_constructor_with_invalid_dict(self): + exception = exceptions.HPESimpliVityException({'msg': "A message string"}) + + self.assertEqual(exception.msg, None) + self.assertEqual(exception.response, {'msg': "A message string"}) + self.assertEqual(exception.args[0], None) + self.assertEqual(exception.args[1], {'msg': "A message string"}) + + def test_exception_constructor_with_invalid_type(self): + exception = exceptions.HPESimpliVityException(['List, item 1', "List, item 2: A message string"]) + + self.assertEqual(exception.msg, None) + self.assertEqual(exception.response, ['List, item 1', "List, item 2: A message string"]) + self.assertEqual(exception.args[0], None) + self.assertEqual(exception.args[1], ['List, item 1', "List, item 2: A message string"]) + + def test_exception_constructor_with_unicode(self): + exception = exceptions.HPESimpliVityException(u"A message string") + + self.assertEqual(exception.msg, "A message string") + self.assertEqual(exception.response, None) + self.assertEqual(exception.args[0], "A message string") + self.assertEqual(len(exception.args), 1) + + def test_task_error_constructor_with_string(self): + exception = exceptions.HPESimpliVityTaskError("A message string", 100) + + self.assertIsInstance(exception, exceptions.HPESimpliVityException) + self.assertEqual(exception.msg, "A message string") + self.assertEqual(exception.response, None) + self.assertEqual(exception.args[0], "A message string") + self.assertEqual(len(exception.args), 1) + self.assertEqual(exception.error_code, 100) + + def test_simplivity_resource_not_found_inheritance(self): + exception = exceptions.HPESimpliVityResourceNotFound("The resource was not found!") + + self.assertIsInstance(exception, exceptions.HPESimpliVityException) + self.assertEqual(exception.msg, "The resource was not found!") + self.assertEqual(exception.response, None) + self.assertEqual(exception.args[0], "The resource was not found!") + + def test_pickle_HPESimpliVityException_dict(self): + message = {"msg": "test message"} + exception = exceptions.HPESimpliVityException(message) + tempf = tempfile.NamedTemporaryFile(delete=False) + with tempf as f: + pickle.dump(exception, f) + + with open(tempf.name, 'rb') as f: + exception = pickle.load(f) + + os.remove(tempf.name) + self.assertEqual('HPESimpliVityException', exception.__class__.__name__) + + def test_pickle_HPESimpliVityException_message(self): + message = "test message" + exception = exceptions.HPESimpliVityException(message) + tempf = tempfile.NamedTemporaryFile(delete=False) + with tempf as f: + pickle.dump(exception, f) + + with open(tempf.name, 'rb') as f: + exception = pickle.load(f) + + os.remove(tempf.name) + self.assertEqual('HPESimpliVityException', exception.__class__.__name__) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/unit/test_ovc_client.py b/tests/unit/test_ovc_client.py new file mode 100755 index 0000000..99938d2 --- /dev/null +++ b/tests/unit/test_ovc_client.py @@ -0,0 +1,145 @@ +### +# (C) Copyright [2019] Hewlett Packard Enterprise Development LP +# +# 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 io +import unittest +import mock +import sys + +from simplivity.connection import Connection +from simplivity.ovc_client import OVC +from simplivity.resources.virtual_machines import VirtualMachines +from simplivity.resources.policies import Policies +from simplivity.resources.datastores import Datastores +from simplivity.resources.omnistack_clusters import OmnistackClusters +from simplivity.resources.backups import Backups + + +OS_ENVIRON_CONFIG = { + 'SIMPLIVITYSDK_OVC_IP': '10.30.4.245', + 'SIMPLIVITYSDK_USERNAME': 'simplicity', + 'SIMPLIVITYSDK_PASSWORD': 'root', + 'SIMPLIVITYSDK_SSL_CERTIFICATE': 'certificate', + 'SIMPLIVITYSDK_CONNECTION_TIMEOUT': '-1' +} + + +def mock_builtin(method_name='open'): + package_name = 'builtins' if sys.version_info[:3] >= (3,) else '__builtin__' + return "%s.%s" % (package_name, method_name) + + +class OVCTest(unittest.TestCase): + @mock.patch.object(Connection, 'login') + def setUp(self, mock_login): + super(OVCTest, self).setUp() + + config = {"ip": "10.30.4.245", + "credentials": { + "username": "simplivity", + "password": "root"}} + + self._ovc = OVC(config) + + def __mock_file_open(self, json_config_content): + return io.StringIO(json_config_content) + + @mock.patch.object(Connection, 'login') + @mock.patch(mock_builtin('open')) + def test_from_json_file(self, mock_open, mock_login): + json_config_content = u"""{ + "ip": "10.30.4.245", + "credentials": { + "username": "simplicity", + "password": "root" + } + }""" + mock_open.return_value = self.__mock_file_open(json_config_content) + ovc_client = OVC.from_json_file("config.json") + + self.assertIsInstance(ovc_client, OVC) + self.assertEqual("10.30.4.245", ovc_client.connection._ovc_ip) + + @mock.patch.object(Connection, 'login') + @mock.patch.dict('os.environ', OS_ENVIRON_CONFIG) + def test_from_environment_variables(self, mock_login): + OVC.from_environment_variables() + mock_login.assert_called_once_with('simplicity', 'root') + + @mock.patch.dict('os.environ', OS_ENVIRON_CONFIG) + @mock.patch.object(OVC, '__init__') + def test_from_environment_variables_is_passing_right_arguments_to_the_constructor(self, mock_cls): + mock_cls.return_value = None + OVC.from_environment_variables() + mock_cls.assert_called_once_with({'timeout': '-1', + 'ip': '10.30.4.245', + 'ssl_certificate': 'certificate', + 'credentials': {'username': 'simplicity', + 'password': 'root'}}) + + def test_virtual_machines_has_right_type(self): + self.assertIsInstance(self._ovc.virtual_machines, VirtualMachines) + + def test_virtual_machines_has_value(self): + self.assertIsNotNone(self._ovc.virtual_machines) + + def test_lazy_loading_virtual_machines(self): + virtual_machines = self._ovc.virtual_machines + self.assertEqual(virtual_machines, self._ovc.virtual_machines) + + def test_policies_has_right_type(self): + self.assertIsInstance(self._ovc.policies, Policies) + + def test_policies_has_value(self): + self.assertIsNotNone(self._ovc.policies) + + def test_lazy_loading_policies(self): + policies = self._ovc.policies + self.assertEqual(policies, self._ovc.policies) + + def test_datastores_has_right_type(self): + self.assertIsInstance(self._ovc.datastores, Datastores) + + def test_datastores_has_value(self): + self.assertIsNotNone(self._ovc.datastores) + + def test_lazy_loading_datastores(self): + datastores = self._ovc.datastores + self.assertEqual(datastores, self._ovc.datastores) + + def test_omnistack_clusters_has_right_type(self): + self.assertIsInstance(self._ovc.omnistack_clusters, OmnistackClusters) + + def test_omnistack_clusters_has_value(self): + self.assertIsNotNone(self._ovc.omnistack_clusters) + + def test_lazy_loading_omnistack_clusters(self): + omnistack_clusters = self._ovc.omnistack_clusters + self.assertEqual(omnistack_clusters, self._ovc.omnistack_clusters) + + def test_backups_has_right_type(self): + self.assertIsInstance(self._ovc.backups, Backups) + + def test_backups_has_value(self): + self.assertIsNotNone(self._ovc.backups) + + def test_lazy_loading_backups(self): + backups = self._ovc.backups + self.assertEqual(backups, self._ovc.backups) + + +if __name__ == '__main__': + unittest.main() diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..ce261ce --- /dev/null +++ b/tox.ini @@ -0,0 +1,54 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + + +[tox] +envlist = docs, py34, py36, py36-coverage, py36-flake8 +skip_missing_interpreters = true + +[flake8] +# E402 module level import not at top of file +# W504 line break after binary operator +# W605 invalid escape sequence '\_' +ignore = E402, W504, W605 +max-line-length = 160 +exclude = simplivity/__init__.py +max-complexity = 14 + +[testenv] +deps = + -r{toxinidir}/test_requirements.txt +commands = + {envpython} -m unittest discover + +[testenv:py36-coverage] +basepython = + python3.6 +passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH +deps = + -r{toxinidir}/test_requirements.txt + coverage + coveralls +commands = + coverage erase + coverage run --source=simplivity -m unittest discover + - coveralls + +[testenv:py36-flake8] +basepython = + python3.6 +deps = + flake8 +commands = + flake8 {posargs} simplivity/ tests/ examples/ + +[testenv:docs] +basepython=python3.6 +deps= + sphinx + sphinx_rtd_theme +commands= + sphinx-apidoc -f -o docs/source ./simplivity/ + sphinx-build -b html docs/source docs/build/html