From 9b039d3981173471cc62486e1da124572d6f6f02 Mon Sep 17 00:00:00 2001 From: Alex Kavanagh Date: Wed, 16 Nov 2016 10:59:53 +0000 Subject: [PATCH] First commit of charm-manila-generic The generic manila charm provides the generic backend NFS configuration for the manila file service charm (charm-manila). This is the first commit of code, and the CI tests have been disabled to enable it to land. This is because of a circular dependency with the manila charm which requires this charm to be able to configure at least one backend. This patchset is dependent on the interface-manila-plugin interface and an updated version of charms.openstack that provides the 'options' member: these are declared below. Change-Id: I052f272dcd310091d988afd7104dea68115053ac Depends-On: Ied0ad014ab7b1d4778113b0d3f2bbae08075372e Depends-On: If6d103b4f62c95b0fa76562a18e418e0d319e987 --- .gitignore | 7 + .testr.conf | 8 + HACKING.md | 10 + LICENSE | 202 +++++++++ Makefile | 11 + README.md | 19 + TODO.md | 57 +++ requirements.txt | 3 + src/README.md | 29 ++ src/config.yaml | 96 +++++ src/layer.yaml | 4 + src/lib/__init__.py | 13 + src/lib/charm/__init__.py | 13 + src/lib/charm/openstack/__init__.py | 13 + src/lib/charm/openstack/manila_generic.py | 342 ++++++++++++++++ src/metadata.yaml | 27 ++ src/reactive/__init__.py | 13 + src/reactive/manila_generic_handlers.py | 50 +++ src/test-requirements.txt | 22 + src/tests/README.md | 9 + src/tests/basic_deployment.py | 326 +++++++++++++++ src/tests/gate-basic-xenial-mitaka | 10 + src/tests/tests.yaml | 17 + src/tox.ini | 53 +++ test-requirements.txt | 7 + tox.ini | 53 +++ unit_tests/__init__.py | 45 +++ ...test_lib_charm_openstack_manila_generic.py | 382 ++++++++++++++++++ unit_tests/test_manila_generic_handlers.py | 77 ++++ 29 files changed, 1918 insertions(+) create mode 100644 .gitignore create mode 100644 .testr.conf create mode 100644 HACKING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 TODO.md create mode 100644 requirements.txt create mode 100644 src/README.md create mode 100644 src/config.yaml create mode 100644 src/layer.yaml create mode 100644 src/lib/__init__.py create mode 100644 src/lib/charm/__init__.py create mode 100644 src/lib/charm/openstack/__init__.py create mode 100644 src/lib/charm/openstack/manila_generic.py create mode 100644 src/metadata.yaml create mode 100644 src/reactive/__init__.py create mode 100644 src/reactive/manila_generic_handlers.py create mode 100644 src/test-requirements.txt create mode 100644 src/tests/README.md create mode 100644 src/tests/basic_deployment.py create mode 100755 src/tests/gate-basic-xenial-mitaka create mode 100644 src/tests/tests.yaml create mode 100644 src/tox.ini create mode 100644 test-requirements.txt create mode 100644 tox.ini create mode 100644 unit_tests/__init__.py create mode 100644 unit_tests/test_lib_charm_openstack_manila_generic.py create mode 100644 unit_tests/test_manila_generic_handlers.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a68c28d --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +build +.tox +layers +interfaces +trusty +.testrepository +__pycache__ diff --git a/.testr.conf b/.testr.conf new file mode 100644 index 0000000..801646b --- /dev/null +++ b/.testr.conf @@ -0,0 +1,8 @@ +[DEFAULT] +test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ + OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ + OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ + ${PYTHON:-python} -m subunit.run discover -t ./ ./unit_tests $LISTOPT $IDOPTION + +test_id_option=--load-list $IDFILE +test_list_option=--list diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 0000000..8a02dd6 --- /dev/null +++ b/HACKING.md @@ -0,0 +1,10 @@ +# Overview + +This charm is developed as part of the OpenStack Charms project, and as such you +should refer to the [OpenStack Charm Development Guide](https://github.com/openstack/charm-guide) for details on how +to contribute to this charm. + +You can find its source code here: . + + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..fd13171 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +#!/usr/bin/make + +clean: + find . -iname '*.pyc' -delete + find . -iname '__pycache__' -delete + +default: + echo "Doing nothing -- run 'make clean'" + +all: default + diff --git a/README.md b/README.md new file mode 100644 index 0000000..f6861df --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# Manila Generic Backend Source Charm + +THIS CHARM IS FOR EXPERIMENTAL USE AT PRESENT. This is a new charm for the +Manila service and it provides the generic backend plugin configuration. + +This repository is for the reactive, layered, _source_ charm. + +Please see the src/README.md for details on the built Manila-generic backend +charm and how to use it. + +## Building the charm + +To build the charm run the following command in the root of the repository: + +```bash +$ tox -e build +``` + +The resultant built charm will be in the builds directory. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..59dee11 --- /dev/null +++ b/TODO.md @@ -0,0 +1,57 @@ +TODO +==== + + * Add roles to the manila charm: api, scheduler, data, process, (all) + * Add a manila-backend-plugin interface + * Split the generic configuration into manila-generic-backend charm + * Add unit tests + * Add amulet tests + * Put the manual testing bits into charm-openstack-testing so that the bundles + are available + +## Add roles: + +It's necessary for the manila charm to be able to install itself as one of a +number of roles: + + 1. The manila-api: this provides the API to the rest of OpenStack. Until this + is HA aware, only ONE manila-api can be provisioned. Also, it may not make + sense to provision more than one manila-api server per OpenStack + installation. + 2. The manila-scheduler: TODO + 3. The manila-data process: TODO + 4. The manila-share process: TODO + + +## Split the generic backend configuration out into a separate charm + interface + +It's necessary to have the ability to configure a share backend independently +of the main charm. This means that plugin charms will be used to configure +each backend. + +Essentially, a plugin needs to be able to configure: + + - it's section in the manila.conf along with any network plugin's that it + needs (assuming that it's a share that manages it's own share-instance). + - ensure that the relevant bits are restarted. + +It's not clear whether, for example, the api bit needs to know if the backend +is a generic backend, rather than something else. + +Anyway, to start with: + + - charm-manila : the main charm that can be deployed as multiple roles + - interface-manila-backend-plugin : the interface for plugging in the generic + backend (and other interfaces) + - charm-manila-generic-backend : the plugin for configuring the generic backend. + +The backend needs to provide a piece of the manila.conf configuration file with +the bits necessary to configure the backend. This is mostly for the share, +rather than the api level. However, the issue is that parts of this file +actually need informatation from the principal charm (i.e. the manila service +user and password). And only the API charm should register with keystone +(particularly when the HA stuff is done with a floating VIP). + +So, to solve that particular problem, we need to 'half' do the template, OR +provide the keystone 'manila' user credentials across the interface. And I +prefer the latter! diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c3ebdd2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +#charm-tools +git+https://github.com/juju/charm-tools#egg=charm-tools +simplejson diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..1bc528d --- /dev/null +++ b/src/README.md @@ -0,0 +1,29 @@ +# Overview + +This is a _pre-release_ charm intended for *testing* only. + +This charm configures the generic backend in the related manila charm in an +OpenStack cloud. This provides NFS shares using Cinder as a backing store. It +should be used for testing and development purposes only. + +# Usage + +The charm relies on the prinical manila charm, and is a subordinate to it. It +provides configuration data to the manila-share service (which is provided by +the manila charm with a role that includes 'share'). + +If multiple, _different_, generic backend configurations are required then the +`share-backend-name` config option should be used to differentiate between the +configuration sections. + +_Note_: this subordinate charm requests that manila configure the nova, neutron +and cinder sections that the generic driver needs to launch NFS share instances +that provide NFS/CIFS services within their tenant networks. The manila charm +provides the _main_ manila service username/password to this charm to enable it +to provide those configuration sections. + +# Bugs + +Please report bugs on [Launchpad](https://bugs.launchpad.net/charm-manila-generic/+filebug). + +For general questions please refer to the OpenStack [Charm Guide](https://github.com/openstack/charm-guide). diff --git a/src/config.yaml b/src/config.yaml new file mode 100644 index 0000000..069c150 --- /dev/null +++ b/src/config.yaml @@ -0,0 +1,96 @@ +options: + openstack-origin: + default: distro + type: string + description: | + Repository from which to install. May be one of the following: + distro (default), ppa:somecustom/ppa, a deb url sources entry, + or a supported Cloud Archive release pocket. + + Supported Cloud Archive sources include: cloud:precise-folsom, + cloud:precise-folsom/updates, cloud:precise-folsom/staging, + cloud:precise-folsom/proposed. + + Note that updating this setting to a source that is known to + provide a later version of OpenStack will trigger a software + upgrade. + debug: + default: False + type: boolean + description: Enable debug logging + verbose: + default: False + type: boolean + description: Enable verbose logging + share-backend-name: + type: string + default: generic + description: | + The name given to the backend. This is used to generate the backend + configuration section and link it into the share server. If two + different configurations of the same backend type are needed, then this + config option can be used to separate them in the backend configuration. + share-protocols: + type: string + default: NFS CIFS + description: | + The share protocols that the backends will be able to provide. The + default is good for the generic backends. Other backends may not support + both NFS and CIFS. This is a space delimited list of protocols. + driver-service-image-name: + type: string + description: the image name to use for the generic instance + default: manila-service-image + driver-handles-share-servers: + type: boolean + description: Whether to generic driver should run up a share server. + default: True + driver-service-instance-flavor-id: + type: int + default: 0 + description: | + The ID for the flavor to launch images in. The driver blocks if this is + not set. + driver-connect-share-server-to-tenant-network: + type: boolean + default: True + description: Whether to connect the share server into the tenant network. + driver-service-instance-user: + type: string + description: The user to log into the share instance. + default: manila + driver-auth-type: + type: string + default: "" + description: | + One of 'password', 'ssh', 'both'. This determines how manila + authenticates against the service-instance; e.g. using password, ssh + keypair or both. + driver-service-instance-password: + type: string + default: "" + description: | + If the service user doesn't log in with a key-pair a password is needed + to allow manila to ssh into the service instance. If the password is set + then it is used and an SSH key is not configured. + driver-service-ssh-key: + type: string + default: "" + description: | + The key for the manila to inject into the instance. If set, manila will + inject it into OpenStack if the keypair name doesn't exist. + driver-service-ssh-key-public: + type: string + default: "" + description: | + The public key for the manila to inject into the instance. If set, + manila will inject it into OpenStack if the keypair name doesn't exist. + driver-keypair-name: + type: string + default: manila-service + description: | + This is the keypair name that will be provided to nova instances. Note + that manila uploads the keypair from the config settings + 'generic-driver-ssh-private-key' and 'generic-driver-ssh-public-key'. If + neither the ssh config vars are set nor the password then the charm will + block until they are set. diff --git a/src/layer.yaml b/src/layer.yaml new file mode 100644 index 0000000..b18b742 --- /dev/null +++ b/src/layer.yaml @@ -0,0 +1,4 @@ +includes: + - layer:openstack + - interface:manila-plugin +repo: https://github.com/openstack/charm-manila-generic diff --git a/src/lib/__init__.py b/src/lib/__init__.py new file mode 100644 index 0000000..9b088de --- /dev/null +++ b/src/lib/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/lib/charm/__init__.py b/src/lib/charm/__init__.py new file mode 100644 index 0000000..9b088de --- /dev/null +++ b/src/lib/charm/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/lib/charm/openstack/__init__.py b/src/lib/charm/openstack/__init__.py new file mode 100644 index 0000000..9b088de --- /dev/null +++ b/src/lib/charm/openstack/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/lib/charm/openstack/manila_generic.py b/src/lib/charm/openstack/manila_generic.py new file mode 100644 index 0000000..44b0545 --- /dev/null +++ b/src/lib/charm/openstack/manila_generic.py @@ -0,0 +1,342 @@ +# Copyright 2016 Canonical Ltd +# +# 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. +# The manila handlers class + +# bare functions are provided to the reactive handlers to perform the functions +# needed on the class. +from __future__ import absolute_import + +import os +import textwrap + +import charmhelpers.core.hookenv as hookenv +import charms_openstack.charm +import charms_openstack.adapters + +# There are no additional packages to install. +PACKAGES = [] +MANILA_DIR = '/etc/manila/' +MANILA_CONF = MANILA_DIR + "manila.conf" + +MANILA_SSH_KEY_PATH = '/etc/manila/ssh_image_key' +MANILA_SSH_KEY_PATH_PUBLIC = '/etc/manila/ssh_image_key.pub' + +# select the default release function and ssl feature +charms_openstack.charm.use_defaults('charm.default-select-release') + + +### +# Compute some options to help with template rendering +@charms_openstack.adapters.config_property +def computed_use_password(config): + """Return True if the generic driver should use a password rather than an + ssh key. + :returns: boolean + """ + return (bool(config.driver_service_instance_password) & + ((config.driver_auth_type or '').lower() + in ('password', 'both'))) + + +@charms_openstack.adapters.config_property +def computed_use_ssh(config): + """Return True if the generic driver should use a password rather than an + ssh key. + :returns: boolean + """ + return ((config.driver_auth_type or '').lower() in ('ssh', 'both')) + + +@charms_openstack.adapters.config_property +def computed_define_ssh(config): + """Return True if the generic driver should define the SSH keys + :returns: boolean + """ + return (bool(config.driver_service_ssh_key) & + bool(config.driver_service_ssh_key_public)) + + +@charms_openstack.adapters.config_property +def computed_debug_level(config): + """Return NONE, INFO, WARNING, DEBUG depending on the settings of + options.debug and options.level + :returns: string, NONE, WARNING, DEBUG + """ + if not config.debug: + return "NONE" + if config.verbose: + return "DEBUG" + return "WARNING" + + +### +# Implementation of the Manila Charm classes + +class ManilaGenericCharm(charms_openstack.charm.OpenStackCharm): + """Generic backend driver configuration charm. This configures a nominally + named "generic" section along with nova, cinder and neutron sections to + enable the generic NFS driver in the front end. + """ + + release = 'mitaka' + name = 'manila-generic' + packages = PACKAGES + version_package = 'manila-api' # need this for versioning the app + api_ports = {} + service_type = None + + default_service = None # There is no service for this charm. + services = [] + + required_relations = [] + + restart_map = {} + + # This is the command to sync the database + sync_cmd = [] + + def custom_assess_status_check(self): + """Validate that the driver configuration is at least complete, and + that it was valid when it used (either at configuration time or config + changed time) + + :returns (status: string, message: string): the status, and message if + there is a problem. Or (None, None) if there are no issues. + """ + options = self.options + if not options.driver_handles_share_servers: + # Nothing to check if the driver doesn't handle share servers + # directly. + return None, None + if not options.driver_service_image_name: + return 'blocked', "Missing 'driver-service-image-name'" + if not options.driver_service_instance_user: + return 'blocked', "Missing 'driver-service-instance-user'" + if not options.driver_service_instance_flavor_id: + return ('blocked', + "Missing 'driver-service-instance-flavor-id'") + # Need at least one of the password or the keypair + if not(bool(options.driver_service_instance_password) or + bool(options.driver_keypair_name)): + return ('blocked', + "Need at least one of instance password or keypair name") + return None, None + + def get_config_for_principal(self, auth_data): + """Assuming that the configuration data is valid, return the + configuration data for the principal charm. + + The format of the returned data is: + { + "complete": , + '': { + '
: ( + (key, value), + (key, value), + ) + } + + If the configuration is not complete, or we don't have auth data from + the principal charm, then we return: + { + "complete": false, + "reason": + } + + :param auth_data: the raw dictionary received from the principal charm + :returns: structure described above. + """ + if not auth_data: + return {"complete": False, "reason": "No authentication data"} + state, message = self.custom_assess_status_check() + if state: + return {"complete": False, "reason": message} + options = self.options # tiny optimisation for less typing. + # We have the auth data & the config is reasonably sensible. + if not options.share_backend_name: + return {"complete": False, + "reason": "Problem: share-backend-name is not set"} + + # if the driver is not going to handle the share servers then we only + # need a very simple config section + if not options.driver_handles_share_servers: + generic_section = self.process_lines(( + "# Set usage of Generic driver which uses cinder as backend.", + "share_driver = " + "manila.share.drivers.generic.GenericShareDriver", + "", + "# Generic driver supports both driver modes - " + "with and without handling", + "# of share servers. So, we need to define explicitly which " + "one we are", + "# enabling using this driver.", + "driver_handles_share_servers = False", + "# Custom name for share backend.", + ("share_backend_name", options.share_backend_name), + "# Generic driver seems to insist on 'service_instance_user' " + "even if it isn't using it", + ("service_instance_user", + options.driver_service_instance_user))) + return { + "complete": True, + MANILA_CONF: { + "[{}]".format(options.share_backend_name): generic_section, + }, + } + + # we use the same username/password/auth for each section as every + # service user has then same permissions as admin. + auth_section = self.process_lines(( + "# Only needed for the generic drivers as of Mitaka", + ('username', auth_data['username']), + ('password', auth_data['password']), + ('project_domain_id', auth_data['project_domain_id']), + ('project_name', auth_data['project_name']), + ('user_domain_id', auth_data['user_domain_id']), + ('auth_uri', auth_data['auth_uri']), + ('auth_url', auth_data['auth_url']), + ('auth_type', auth_data['auth_type']))) + + # Expression is True if the generic driver should use a password rather + # than an ssh key. + if options.computed_use_password: + service_instance_password = ( + "service_instance_password", + options.driver_service_instance_password) + else: + service_instance_password = "# No generic password section" + + # Expression is True if the generic driver should use a password rather + # than an ssh key. + if options.computed_use_ssh: + ssh_section = tuple(self.process_lines(( + ("path_to_private_key", MANILA_SSH_KEY_PATH), + ("path_to_public_key", MANILA_SSH_KEY_PATH_PUBLIC), + ("manila_service_keypair_name", + options.driver_keypair_name)))) + else: + ssh_section = ("# No ssh section", ) + + # And finally configure the generic section + generic_section = self.process_lines(( + "# Set usage of Generic driver which uses cinder as backend.", + "share_driver = manila.share.drivers.generic.GenericShareDriver", + "", + "# Generic driver supports both driver modes - " + "with and without handling", + "# of share servers. So, we need to define explicitly which one " + "we are", + "# enabling using this driver.", + ("driver_handles_share_servers", + options.driver_handles_share_servers), + "", + "# The flavor that Manila will use to launch the instance.", + ("service_instance_flavor_id", + options.driver_service_instance_flavor_id), + "", + "# Generic driver uses a glance image for building service VMs " + "in nova.", + "# The following options specify the image to use.", + "# We use the latest build of [1].", + "# [1] https://github.com/openstack/manila-image-elements", + ("service_instance_user", + options.driver_service_instance_user), + ("service_image_name", options.driver_service_image_name), + ("connect_share_server_to_tenant_network", + options.driver_connect_share_server_to_tenant_network), + "", + "# These will be used for keypair creation and inserted into", + "# service VMs.", + "# TODO: this presents a problem with HA and failover - as the" + "keys", + "# will no longer be the same -- need to be able to set these via", + "# a config option.", + service_instance_password, ) + + ssh_section + + ("", + "# Custom name for share backend.", + ("share_backend_name", options.share_backend_name))) + + return { + "complete": True, + MANILA_CONF: { + "[nova]": auth_section, + "[neutron]": auth_section, + "[cinder]": auth_section, + "[{}]".format(options.share_backend_name): generic_section, + }, + } + + @staticmethod + def process_lines(lines): + """Process each of the lines. If the line is a string, then just + passes it though; if the line is a tuple (and it must be a 2-tuple) + then the string is interpolated with an equals. + + :param lines: list of strings or 2-tuples of strings + :returns: list of strings + """ + out = [] + for line in lines: + if isinstance(line, str): + out.append(line) + elif isinstance(line, (list, tuple)): + if len(line) != 2: + raise TypeError("Line '{}' must be length 2" + .format(line)) + out.append("{} = {}".format(*line)) + # raise an error on other types + else: + raise TypeError("Line '{}' must be a string, tuple or list." + " Passed a {}" + .format(line, type(line))) + return out + + def maybe_write_ssh_keys(self): + """Maybe write the ssh keys from the options to the key files where + manila will be able to find them. The function only writes them if the + configuration is to use the SSH config. If they are not to be written + and they exist then they are deleted. + """ + if (self.options.computed_use_ssh and + self.options.computed_define_ssh): + write_file(self.options.driver_service_ssh_key, + MANILA_SSH_KEY_PATH) + write_file(self.options.driver_service_ssh_key_public, + MANILA_SSH_KEY_PATH_PUBLIC, 0o644) + else: + for f in (MANILA_SSH_KEY_PATH, MANILA_SSH_KEY_PATH_PUBLIC): + try: + os.remove(f) + except OSError: + pass + + +def write_file(contents, file, chown=0o600): + """Write the contents to the file. + + :param contents: the contents to write. This will be dedented, and striped + to ensure that it is just a set of lines. + :param file: the file to write + :param chown: the ownership for the file. + :raises OSError: If the file couldn't be written. + :returns None: + """ + try: + with os.fdopen(os.open(file, + os.O_WRONLY | os.O_CREAT, + chown), 'w') as f: + f.write(textwrap.dedent(contents)) + except OSError as e: + hookenv.log("Couldn't write file: {}".format(str(e))) diff --git a/src/metadata.yaml b/src/metadata.yaml new file mode 100644 index 0000000..8d9a009 --- /dev/null +++ b/src/metadata.yaml @@ -0,0 +1,27 @@ +name: manila-generic +summary: A generic backend configuration charm for manila. +maintainer: OpenStack Charmers +description: | + The Manil share file system service provides a set of services for management + of shared file systems in a multi-tenant cloud environment. The service + resembles OpenStack block-based storage management from the OpenStack Block + Storage service project. With the Shared File Systems service, you can create + a remote file system, mount the file system on your instances, and then read + and write data from your instances to and from your file system. + + The manila-generic plugin (using the manila-plugin relation) provides the + configuration information to the manila charm to configure the Manila + instance such that it can use the generic driver appropriately. +tags: + - openstack +series: + - xenial +subordinate: true +provides: + manila-plugin: + interface: manila-plugin + scope: container +requires: + juju-info: + interface: juju-info + scope: container diff --git a/src/reactive/__init__.py b/src/reactive/__init__.py new file mode 100644 index 0000000..9b088de --- /dev/null +++ b/src/reactive/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/src/reactive/manila_generic_handlers.py b/src/reactive/manila_generic_handlers.py new file mode 100644 index 0000000..fcdfa50 --- /dev/null +++ b/src/reactive/manila_generic_handlers.py @@ -0,0 +1,50 @@ +# Copyright 2016 Canonical Ltd +# +# 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 is just for the reactive handlers and calls into the charm. +from __future__ import absolute_import + +import charms.reactive +import charms_openstack.charm + +# This charm's library contains all of the handler code associated with +# manila -- we need to import it to get the definitions for the charm. +import charm.openstack.manila_generic # noqa + + +# Use the charms.openstack defaults for common states and hooks +charms_openstack.charm.use_defaults( + 'charm.installed', + 'update-status') + + +@charms.reactive.when('manila-plugin.available') +@charms.reactive.when_not('config.changed') +def send_config(manila_plugin): + """Send the configuration over to the prinicpal charm""" + with charms_openstack.charm.provide_charm_instance() as generic_charm: + # set the name of the backend using the configuration option + manila_plugin.name = generic_charm.options.share_backend_name + # Set the configuration data for the principal charm. + manila_plugin.configuration_data = ( + generic_charm.get_config_for_principal( + manila_plugin.authentication_data)) + generic_charm.maybe_write_ssh_keys() + generic_charm.assess_status() + + +@charms.reactive.when('manila-plugin.available', + 'config.changed') +def update_config(manila_plugin): + send_config(manila_plugin) diff --git a/src/test-requirements.txt b/src/test-requirements.txt new file mode 100644 index 0000000..ec50ece --- /dev/null +++ b/src/test-requirements.txt @@ -0,0 +1,22 @@ +# charm-proof +charm-tools>=2.0.0 +# amulet deployment helpers +bzr+lp:charm-helpers#egg=charmhelpers +# BEGIN: Amulet OpenStack Charm Helper Requirements +# Liberty client lower constraints +amulet>=1.14.3,<2.0 +bundletester>=0.6.1,<1.0 +python-keystoneclient>=1.7.1,<2.0 +python-barbicanclient>=4.0.1,<5.0 +python-designateclient>=1.5,<2.0 +python-cinderclient>=1.4.0,<2.0 +python-glanceclient>=1.1.0,<2.0 +python-heatclient>=0.8.0,<1.0 +python-neutronclient>=3.1.0,<4.0 +python-novaclient>=2.30.1,<3.0 +python-openstackclient>=1.7.0,<2.0 +python-swiftclient>=2.6.0,<3.0 +python-manilaclient>=1.8.1,<2.0 +pika>=0.10.0,<1.0 +distro-info +# END: Amulet OpenStack Charm Helper Requirements diff --git a/src/tests/README.md b/src/tests/README.md new file mode 100644 index 0000000..046be7f --- /dev/null +++ b/src/tests/README.md @@ -0,0 +1,9 @@ +# Overview + +This directory provides Amulet tests to verify basic deployment functionality +from the perspective of this charm, its requirements and its features, as +exercised in a subset of the full OpenStack deployment test bundle topology. + +For full details on functional testing of OpenStack charms please refer to +the [functional testing](http://docs.openstack.org/developer/charm-guide/testing.html#functional-testing) +section of the OpenStack Charm Guide. diff --git a/src/tests/basic_deployment.py b/src/tests/basic_deployment.py new file mode 100644 index 0000000..4aab63d --- /dev/null +++ b/src/tests/basic_deployment.py @@ -0,0 +1,326 @@ +import amulet + +from keystoneclient import session as keystone_session +from keystoneclient.auth import identity as keystone_identity +import keystoneclient.exceptions +from keystoneclient.v2_0 import client as keystone_v2_0_client +from keystoneclient.v3 import client as keystone_v3_client +from manilaclient.v1 import client as manila_client + +from charmhelpers.contrib.openstack.amulet.deployment import ( + OpenStackAmuletDeployment +) + +from charmhelpers.contrib.openstack.amulet.utils import ( + OpenStackAmuletUtils, + DEBUG, +) + +# Use DEBUG to turn on debug logging +u = OpenStackAmuletUtils(DEBUG) + + +class ManilaGenericBasicDeployment(OpenStackAmuletDeployment): + """Amulet tests on a basic Manila Generic deployment. + + Note that these tests don't attempt to do a functional test on Manila, + merely to demonstrate that the relations work and that they transfer the + correct information across them. It verifies that the configuration goes + across to the manila main charm. + + A functional test will be performed by a mojo or tempest test. + """ + + def __init__(self, series, openstack=None, source=None, stable=False, + keystone_version='2'): + """Deploy the entire test environment. + """ + super(ManilaGenericBasicDeployment, self).__init__( + series, openstack, source, stable) + self._keystone_version = keystone_version + self._add_services() + self._add_relations() + self._configure_services() + self._deploy() + + u.log.info('Waiting on extended status checks...') + exclude_services = ['mysql', ] + self._auto_wait_for_status(exclude_services=exclude_services) + + self._initialize_tests() + + def _add_services(self): + """Add services + + Add the services that we're testing, where manila is local, + and the rest of the service are from lp branches that are + compatible with the local charm (e.g. stable or next). + """ + this_service = {'name': 'manila-generic'} + other_services = [ + {'name': 'mysql', + 'location': 'cs:percona-cluster', + 'constraints': {'mem': '3072M'}}, + {'name': 'rabbitmq-server'}, + {'name': 'keystone'}, + {'name': 'manila', + 'location': 'cs:~openstack-charmers/xenial/manila'} + ] + super(ManilaGenericBasicDeployment, self)._add_services( + this_service, other_services) + + def _add_relations(self): + """Add all of the relations for the services.""" + relations = { + 'manila:shared-db': 'mysql:shared-db', + 'manila:amqp': 'rabbitmq-server:amqp', + 'manila:identity-service': 'keystone:identity-service', + 'manila:manila-plugin': 'manila-generic:manila-plugin', + 'keystone:shared-db': 'mysql:shared-db', + } + super(ManilaGenericBasicDeployment, self)._add_relations(relations) + + def _configure_services(self): + """Configure all of the services.""" + keystone_config = { + 'admin-password': 'openstack', + 'admin-token': 'ubuntutesting', + } + manila_config = { + 'default-share-backend': 'generic', + } + manila_generic_config = { + 'driver-handles-share-servers': False, + } + configs = { + 'keystone': keystone_config, + 'manila': manila_config, + 'manila-generic': manila_generic_config, + } + super(ManilaGenericBasicDeployment, self)._configure_services(configs) + + def _initialize_tests(self): + """Perform final initialization before tests get run.""" + # Access the sentries for inspecting service units + self.manila_sentry = self.d.sentry['manila'][0] + self.manila_generic_sentry = self.d.sentry['manila-generic'][0] + self.mysql_sentry = self.d.sentry['mysql'][0] + self.keystone_sentry = self.d.sentry['keystone'][0] + self.rabbitmq_sentry = self.d.sentry['rabbitmq-server'][0] + u.log.debug('openstack release val: {}'.format( + self._get_openstack_release())) + u.log.debug('openstack release str: {}'.format( + self._get_openstack_release_string())) + + keystone_ip = self.keystone_sentry.relation( + 'shared-db', 'mysql:shared-db')['private-address'] + + # We need to auth either to v2.0 or v3 keystone + if self._keystone_version == '2': + ep = ("http://{}:35357/v2.0" + .format(keystone_ip.strip().decode('utf-8'))) + auth = keystone_identity.v2.Password( + username='admin', + password='openstack', + tenant_name='admin', + auth_url=ep) + keystone_client_lib = keystone_v2_0_client + elif self._keystone_version == '3': + ep = ("http://{}:35357/v3" + .format(keystone_ip.strip().decode('utf-8'))) + auth = keystone_identity.v3.Password( + user_domain_name='admin_domain', + username='admin', + password='openstack', + domain_name='admin_domain', + auth_url=ep) + keystone_client_lib = keystone_v3_client + else: + raise RuntimeError("keystone version must be '2' or '3'") + + sess = keystone_session.Session(auth=auth) + self.keystone = keystone_client_lib.Client(session=sess) + # The service_catalog is missing from V3 keystone client when auth is + # done with session (via authenticate_keystone_admin() + # See https://bugs.launchpad.net/python-keystoneclient/+bug/1508374 + # using session construct client will miss service_catalog property + # workaround bug # 1508374 by forcing a pre-auth and therefore, getting + # the service-catalog -- + # see https://bugs.launchpad.net/python-keystoneclient/+bug/1547331 + self.keystone.auth_ref = auth.get_access(sess) + + def test_205_manila_to_manila_generic(self): + """Verify that the manila to manila-generic config is working""" + u.log.debug('Checking the manila:manila-generic relation data...') + manila = self.manila_sentry + relation = ['manila-plugin', 'manila-generic:manila-plugin'] + expected = { + 'private-address': u.valid_ip, + '_authentication_data': u.not_null, + } + ret = u.validate_relation_data(manila, relation, expected) + if ret: + message = u.relation_error('manila manila_generic', ret) + amulet.raise_status(amulet.FAIL, msg=message) + u.log.debug('OK') + + def test_206_manila_generic_to_manila(self): + """Verify that the manila-generic to manila config is working""" + u.log.debug('Checking the manila-generic:manila relation data...') + manila_generic = self.manila_generic_sentry + relation = ['manila-plugin', 'manila:manila-plugin'] + expected = { + 'private-address': u.valid_ip, + '_configuration_data': u.not_null, + '_name': 'generic' + } + ret = u.validate_relation_data(manila_generic, relation, expected) + if ret: + message = u.relation_error('manila manila_generic', ret) + amulet.raise_status(amulet.FAIL, msg=message) + u.log.debug('OK') + + @staticmethod + def _find_or_create(items, key, create): + """Find or create the thing in the items + + :param items: the items to search using the key + :param key: a function that key(item) -> boolean if found. + :param create: a function to call if the key() never was true. + :returns: the item that was either found or created. + """ + for i in items: + if key(i): + return i + return create() + + def test_400_api_connection(self): + """Simple api calls to check service is up and responding""" + u.log.debug('Checking api functionality...') + + # This handles both keystone v2 and v3. + # For keystone v2 we need a user: + # - 'demo' user + # - has a project 'demo' + # - in the 'demo' project + # - with an 'admin' role + # For keystone v3 we need a user: + # - 'default' domain + # - 'demo' user + # - 'demo' project + # - 'admin' role -- to be able to delete. + + # manila requires a user with creator or admin role on the project + # when creating a secret (which this test does). Therefore, we create + # a demo user, demo project, and then get a demo manila client and do + # the secret. ensure that the default domain is created. + + if self._keystone_version == '2': + # find or create the 'demo' tenant (project) + tenant = self._find_or_create( + items=self.keystone.tenants.list(), + key=lambda t: t.name == 'demo', + create=lambda: self.keystone.tenants.create( + tenant_name="demo", + description="Demo for testing manila", + enabled=True)) + # find or create the demo user + demo_user = self._find_or_create( + items=self.keystone.users.list(), + key=lambda u: u.name == 'demo', + create=lambda: self.keystone.users.create( + name='demo', + password='pass', + tenant_id=tenant.id)) + # find the admin role + # already be created - if not, then this will fail later. + admin_role = self._find_or_create( + items=self.keystone.roles.list(), + key=lambda r: r.name.lower() == 'admin', + create=lambda: None) + # grant the role if it isn't already created. + # now grant the creator role to the demo user. + self._find_or_create( + items=self.keystone.roles.roles_for_user( + demo_user, tenant=tenant), + key=lambda r: r.name.lower() == admin_role.name.lower(), + create=lambda: self.keystone.roles.add_user_role( + demo_user, admin_role, tenant=tenant)) + # now we can finally get the manila client and create the secret + keystone_ep = self.keystone.service_catalog.url_for( + service_type='identity', endpoint_type='publicURL') + auth = keystone_identity.v2.Password( + username=demo_user.name, + password='pass', + tenant_name=tenant.name, + auth_url=keystone_ep) + + else: + # find or create the 'default' domain + domain = self._find_or_create( + items=self.keystone.domains.list(), + key=lambda u: u.name == 'default', + create=lambda: self.keystone.domains.create( + "default", + description="domain for manila testing", + enabled=True)) + # find or create the 'demo' user + demo_user = self._find_or_create( + items=self.keystone.users.list(domain=domain.id), + key=lambda u: u.name == 'demo', + create=lambda: self.keystone.users.create( + 'demo', + domain=domain.id, + description="Demo user for manila tests", + enabled=True, + email="demo@example.com", + password="pass")) + # find or create the 'demo' project + demo_project = self._find_or_create( + items=self.keystone.projects.list(domain=domain.id), + key=lambda x: x.name == 'demo', + create=lambda: self.keystone.projects.create( + 'demo', + domain=domain.id, + description='manila testing project', + enabled=True)) + # create the role for the user - needs to be admin so that the + # secret can be deleted - note there is only one admin role, and it + # should already be created - if not, then this will fail later. + admin_role = self._find_or_create( + items=self.keystone.roles.list(), + key=lambda r: r.name.lower() == 'admin', + create=lambda: None) + # now grant the creator role to the demo user. + try: + self.keystone.roles.check( + role=admin_role, + user=demo_user, + project=demo_project) + except keystoneclient.exceptions.NotFound: + # create it if it isn't found + self.keystone.roles.grant( + role=admin_role, + user=demo_user, + project=demo_project) + # now we can finally get the manila client and create the secret + keystone_ep = self.keystone.service_catalog.url_for( + service_type='identity', endpoint_type='publicURL') + auth = keystone_identity.v3.Password( + user_domain_name=domain.name, + username=demo_user.name, + password='pass', + project_domain_name=domain.name, + project_name=demo_project.name, + auth_url=keystone_ep) + + # Now we carry on with common v2 and v3 code + sess = keystone_session.Session(auth=auth) + # Authenticate admin with manila endpoint + manila_ep = self.keystone.service_catalog.url_for( + service_type='share', endpoint_type='publicURL') + manila = manila_client.Client(session=sess, + endpoint=manila_ep) + # now just try a list the shares + manila.shares.list() + u.log.debug('OK') diff --git a/src/tests/gate-basic-xenial-mitaka b/src/tests/gate-basic-xenial-mitaka new file mode 100755 index 0000000..8919873 --- /dev/null +++ b/src/tests/gate-basic-xenial-mitaka @@ -0,0 +1,10 @@ +#!/usr/bin/env python + +"""Amulet tests on a basic barbican deployment on xenial-mitaka for keystone v2. +""" + +from basic_deployment import ManilaGenericBasicDeployment + +if __name__ == '__main__': + deployment = ManilaGenericBasicDeployment(series='xenial', keystone_version='2') + deployment.run_tests() diff --git a/src/tests/tests.yaml b/src/tests/tests.yaml new file mode 100644 index 0000000..e3185c6 --- /dev/null +++ b/src/tests/tests.yaml @@ -0,0 +1,17 @@ +# Bootstrap the model if necessary. +bootstrap: True +# Re-use bootstrap node instead of destroying/re-bootstrapping. +reset: True +# Use tox/requirements to drive the venv instead of bundletester's venv feature. +virtualenv: False +# Leave makefile empty, otherwise unit/lint tests will rerun ahead of amulet. +makefile: [] +# Do not specify juju PPA sources. Juju is presumed to be pre-installed +# and configured in all test runner environments. +#sources: +# Do not specify or rely on system packages. +#packages: +# Do not specify python packages here. Use test-requirements.txt +# and tox instead. ie. The venv is constructed before bundletester +# is invoked. +#python-packages: diff --git a/src/tox.ini b/src/tox.ini new file mode 100644 index 0000000..479d7bb --- /dev/null +++ b/src/tox.ini @@ -0,0 +1,53 @@ +# Source charm: ./src/tox.ini +# This file is managed centrally by release-tools and should not be modified +# within individual charm repos. +[tox] +envlist = pep8 +skipsdist = True + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 + AMULET_SETUP_TIMEOUT=2700 +whitelist_externals = juju +passenv = HOME TERM AMULET_* +deps = -r{toxinidir}/test-requirements.txt +install_command = + pip install --allow-unverified python-apt {opts} {packages} + +[testenv:pep8] +basepython = python2.7 +commands = charm-proof + +[testenv:func27-noop] +# DRY RUN - For Debug +basepython = python2.7 +commands = + bundletester -vl DEBUG -r json -o func-results.json --test-pattern "gate-*" -n --no-destroy + +[testenv:func27] +# Run all gate tests which are +x (expected to always pass) +basepython = python2.7 +commands = + bundletester -vl DEBUG -r json -o func-results.json --test-pattern "gate-*" --no-destroy + +[testenv:func27-smoke] +# Run a specific test as an Amulet smoke test (expected to always pass) +basepython = python2.7 +commands = + bundletester -vl DEBUG -r json -o func-results.json gate-basic-xenial-mitaka --no-destroy + +[testenv:func27-dfs] +# Run all deploy-from-source tests which are +x (may not always pass!) +basepython = python2.7 +commands = + bundletester -vl DEBUG -r json -o func-results.json --test-pattern "dfs-*" --no-destroy + +[testenv:func27-dev] +# Run all development test targets which are +x (may not always pass!) +basepython = python2.7 +commands = + bundletester -vl DEBUG -r json -o func-results.json --test-pattern "dev-*" --no-destroy + +[testenv:venv] +commands = {posargs} diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 0000000..368dbf2 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1,7 @@ +# Lint and unit test requirements +flake8 +os-testr>=0.4.1 +charms.reactive +mock>=1.2 +coverage>=3.6 +git+https://github.com/openstack/charms.openstack.git#egg=charms-openstack diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..0f8d053 --- /dev/null +++ b/tox.ini @@ -0,0 +1,53 @@ +[tox] +skipsdist = True +envlist = pep8,py34,py35 +skip_missing_interpreters = True + +[testenv] +setenv = VIRTUAL_ENV={envdir} + PYTHONHASHSEED=0 + TERM=linux + INTERFACE_PATH={toxinidir}/interfaces + LAYER_PATH={toxinidir}/layers + INTERFACE_PATH={toxinidir}/interfaces + JUJU_REPOSITORY={toxinidir}/build +passenv = http_proxy https_proxy +install_command = + pip install {opts} {packages} +deps = + -r{toxinidir}/requirements.txt + +[testenv:build] +basepython = python2.7 +commands = + charm-build --log-level DEBUG -o {toxinidir}/build src {posargs} + +[testenv:py27] +basepython = python2.7 +# Reactive source charms are Python3-only, but a py27 unit test target +# is required by OpenStack Governance. Remove this shim as soon as +# permitted. http://governance.openstack.org/reference/cti/python_cti.html +whitelist_externals = true +commands = true + +[testenv:py34] +basepython = python3.4 +deps = -r{toxinidir}/test-requirements.txt +commands = ostestr {posargs} + +[testenv:py35] +basepython = python3.5 +deps = -r{toxinidir}/test-requirements.txt +commands = ostestr {posargs} + +[testenv:pep8] +basepython = python2.7 +deps = -r{toxinidir}/test-requirements.txt +commands = flake8 {posargs} src unit_tests + +[testenv:venv] +commands = {posargs} + +[flake8] +# E402 ignore necessary for path append before sys module import in actions +ignore = E402 diff --git a/unit_tests/__init__.py b/unit_tests/__init__.py new file mode 100644 index 0000000..ad8caed --- /dev/null +++ b/unit_tests/__init__.py @@ -0,0 +1,45 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys +import mock + +sys.path.append('src') +sys.path.append('src/lib') + +# Mock out charmhelpers so that we can test without it. +# also stops sideeffects from occuring. +charmhelpers = mock.MagicMock() +apt_pkg = mock.MagicMock() +sys.modules['apt_pkg'] = apt_pkg +sys.modules['charmhelpers'] = charmhelpers +sys.modules['charmhelpers.core'] = charmhelpers.core +sys.modules['charmhelpers.core.hookenv'] = charmhelpers.core.hookenv +sys.modules['charmhelpers.core.host'] = charmhelpers.core.host +sys.modules['charmhelpers.core.unitdata'] = charmhelpers.core.unitdata +sys.modules['charmhelpers.core.templating'] = charmhelpers.core.templating +sys.modules['charmhelpers.contrib'] = charmhelpers.contrib +sys.modules['charmhelpers.contrib.openstack'] = charmhelpers.contrib.openstack +sys.modules['charmhelpers.contrib.openstack.utils'] = ( + charmhelpers.contrib.openstack.utils) +sys.modules['charmhelpers.contrib.openstack.templating'] = ( + charmhelpers.contrib.openstack.templating) +sys.modules['charmhelpers.contrib.network'] = charmhelpers.contrib.network +sys.modules['charmhelpers.contrib.network.ip'] = ( + charmhelpers.contrib.network.ip) +sys.modules['charmhelpers.fetch'] = charmhelpers.fetch +sys.modules['charmhelpers.cli'] = charmhelpers.cli +sys.modules['charmhelpers.contrib.hahelpers'] = charmhelpers.contrib.hahelpers +sys.modules['charmhelpers.contrib.hahelpers.cluster'] = ( + charmhelpers.contrib.hahelpers.cluster) diff --git a/unit_tests/test_lib_charm_openstack_manila_generic.py b/unit_tests/test_lib_charm_openstack_manila_generic.py new file mode 100644 index 0000000..e810ce9 --- /dev/null +++ b/unit_tests/test_lib_charm_openstack_manila_generic.py @@ -0,0 +1,382 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import print_function + +import mock + +import charm.openstack.manila_generic as manila_generic + +import charms_openstack.test_utils as test_utils + + +class Helper(test_utils.PatchHelper): + + def setUp(self): + super().setUp() + self.patch_release(manila_generic.ManilaGenericCharm.release) + + +class TestManilaGenericCharmConfigProperties(Helper): + + def test_computed_use_password(self): + config = mock.MagicMock() + # test no passowrd or driver_auth_type configured + config.driver_service_instance_password = None + config.driver_auth_type = None + self.assertFalse(manila_generic.computed_use_password(config)) + # test with the password but no auth type configured. + config.driver_service_instance_password = 'hello' + self.assertFalse(manila_generic.computed_use_password(config)) + # test with a driver password, and a configured string, but not + # password or both. + config.driver_auth_type = 'goodbye' + self.assertFalse(manila_generic.computed_use_password(config)) + # test with 'password' + config.driver_auth_type = 'Password' + self.assertTrue(manila_generic.computed_use_password(config)) + # test with 'BOTH' + config.driver_auth_type = 'BOTH' + self.assertTrue(manila_generic.computed_use_password(config)) + # now test without the password again. + config.driver_service_instance_password = None + self.assertFalse(manila_generic.computed_use_password(config)) + + def test_computed_use_ssh(self): + config = mock.MagicMock() + # test that not being configured returns false. + config.driver_auth_type = None + self.assertFalse(manila_generic.computed_use_ssh(config)) + # check that being either ssh or 'both' in upper/lower gives true + config.driver_auth_type = 'Ssh' + self.assertTrue(manila_generic.computed_use_ssh(config)) + config.driver_auth_type = 'BOTH' + self.assertTrue(manila_generic.computed_use_ssh(config)) + config.driver_auth_type = 'both' + self.assertTrue(manila_generic.computed_use_ssh(config)) + + def test_computed_define_ssh(self): + config = mock.MagicMock() + config.driver_service_ssh_key = None + config.driver_service_ssh_key_public = None + # test that function only returns true if both config items are set + self.assertFalse(manila_generic.computed_define_ssh(config)) + config.driver_service_ssh_key = "ssh key" + config.driver_service_ssh_key_public = None + self.assertFalse(manila_generic.computed_define_ssh(config)) + config.driver_service_ssh_key = None + config.driver_service_ssh_key_public = "ssh public key" + self.assertFalse(manila_generic.computed_define_ssh(config)) + config.driver_service_ssh_key = "ssh key" + config.driver_service_ssh_key_public = "ssh public key" + self.assertTrue(manila_generic.computed_define_ssh(config)) + + def test_computed_debug_level(self): + config = mock.MagicMock() + config.debug = False + config.verbose = False + self.assertEqual(manila_generic.computed_debug_level(config), "NONE") + config.verbose = True + self.assertEqual(manila_generic.computed_debug_level(config), "NONE") + config.debug = True + config.verbose = False + self.assertEqual( + manila_generic.computed_debug_level(config), "WARNING") + config.verbose = True + self.assertEqual(manila_generic.computed_debug_level(config), "DEBUG") + + +class TestManilaGenericCharm(Helper): + + def _patch_config_and_charm(self, config): + self.patch('charmhelpers.core.hookenv.config', name='config') + + def cf(key=None): + if key is not None: + return config[key] + return config + + self.config.side_effect = cf + + def test_custom_assess_status_check(self): + config = { + 'driver-handles-share-servers': False, + 'driver-service-image-name': '', + 'driver-service-instance-user': '', + 'driver-service-instance-flavor-id': '', + 'driver-service-instance-password': '', + 'driver-keypair-name': '', + } + self._patch_config_and_charm(config) + c = manila_generic.ManilaGenericCharm() + self.assertEqual(c.custom_assess_status_check(), (None, None)) + config['driver-handles-share-servers'] = True + c = manila_generic.ManilaGenericCharm() + self.assertEqual(c.custom_assess_status_check(), + ('blocked', "Missing 'driver-service-image-name'")) + config['driver-service-image-name'] = 'image-name' + c = manila_generic.ManilaGenericCharm() + self.assertEqual(c.custom_assess_status_check(), + ('blocked', "Missing 'driver-service-instance-user'")) + config['driver-service-instance-user'] = 'manila' + c = manila_generic.ManilaGenericCharm() + self.assertEqual( + c.custom_assess_status_check(), + ('blocked', "Missing 'driver-service-instance-flavor-id'")) + config['driver-service-instance-flavor-id'] = '100' + c = manila_generic.ManilaGenericCharm() + self.assertEqual( + c.custom_assess_status_check(), + ('blocked', + "Need at least one of instance password or keypair name")) + config['driver-service-instance-password'] = 'password' + c = manila_generic.ManilaGenericCharm() + self.assertEqual(c.custom_assess_status_check(), (None, None)) + config['driver-service-instance-password'] = '' + config['driver-keypair-name'] = 'keyname' + c = manila_generic.ManilaGenericCharm() + self.assertEqual(c.custom_assess_status_check(), (None, None)) + config['driver-service-instance-password'] = 'password' + config['driver-keypair-name'] = 'keyname' + c = manila_generic.ManilaGenericCharm() + self.assertEqual(c.custom_assess_status_check(), (None, None)) + + def test_get_config_for_principal(self): + # note that this indirectly tests 'process_lines' as well. + c = manila_generic.ManilaGenericCharm() + self.assertEqual( + c.get_config_for_principal(None), + {'complete': False, 'reason': 'No authentication data'}) + # we want to handle share servers to True to check for misconfig + config = { + 'driver-handles-share-servers': True, + 'driver-service-image-name': '', + 'driver-service-instance-user': '', + 'driver-service-instance-flavor-id': '', + 'driver-service-instance-password': '', + 'driver-keypair-name': '', + 'share-backend-name': '', + 'driver-auth-type': '', + 'driver-connect-share-server-to-tenant-network': False, + } + self._patch_config_and_charm(config) + c = manila_generic.ManilaGenericCharm() + state, message = c.custom_assess_status_check() + auth_data = { + 'username': 'user', + 'password': 'pass', + 'project_domain_id': 'pd1', + 'project_name': 'p1', + 'user_domain_id': 'ud1', + 'auth_uri': 'uri1', + 'auth_url': 'url1', + 'auth_type': 'type1', + } + self.maxDiff = None + self.assertEqual( + c.get_config_for_principal(auth_data), + {'complete': False, 'reason': message}) + # now set up the config to be okay to generate the sections + config['driver-handles-share-servers'] = True + config['driver-service-image-name'] = 'manila' + config['driver-service-instance-user'] = 'manila-user' + config['driver-service-instance-flavor-id'] = '103' + config['driver-service-instance-password'] = 'password' + config['driver-keypair-name'] = 'my-keyname' + # test that we've set the backend name + c = manila_generic.ManilaGenericCharm() + self.assertEqual( + c.get_config_for_principal(auth_data), + {'complete': False, 'reason': + 'Problem: share-backend-name is not set'}) + # now test that we actually generate some config data + config['share-backend-name'] = 'test-backend' + # simplify the output for the next test + config['driver-handles-share-servers'] = False + c = manila_generic.ManilaGenericCharm() + lines = c.get_config_for_principal(auth_data) + # verify that "# No generic password section" is in the lines + conf = manila_generic.MANILA_CONF + self.assertIn(conf, lines) + self.assertIn('[test-backend]', lines[conf]) + section = lines[conf]['[test-backend]'] + self.assertIn('share_driver = ' + 'manila.share.drivers.generic.GenericShareDriver', + section) + self.assertIn('driver_handles_share_servers = False', section) + self.assertIn('share_backend_name = test-backend', section) + + # Now verify that when we switch the driver handles shares on that the + # sections all appear + config['driver-handles-share-servers'] = True + c = manila_generic.ManilaGenericCharm() + lines = c.get_config_for_principal(auth_data) + self.assertIn(conf, lines) + self.assertIn('[test-backend]', lines[conf]) + self.assertIn('[nova]', lines[conf]) + self.assertIn('[neutron]', lines[conf]) + self.assertIn('[cinder]', lines[conf]) + # check each of the nova, neutron and cinder sections (which are all + # identical) + auth_lines = ['# Only needed for the generic drivers as of Mitaka', + 'username = user', + 'password = pass', + 'project_domain_id = pd1', + 'project_name = p1', + 'user_domain_id = ud1', + 'auth_uri = uri1', + 'auth_url = url1', + 'auth_type = type1'] + + for s in ('[nova]', '[neutron]', '[cinder]'): + section = lines[conf][s] + self._verify_section_contains(section, auth_lines) + + # now check the [test-backend] section + section = lines[conf]['[test-backend]'] + self.assertIn('share_driver = ' + 'manila.share.drivers.generic.GenericShareDriver', + section) + self.assertIn('driver_handles_share_servers = True', section) + self.assertIn('share_backend_name = test-backend', section) + self.assertIn('service_instance_flavor_id = 103', section) + self._verify_section_contains( + section, + ['service_instance_user = manila-user', + 'service_image_name = manila', + 'connect_share_server_to_tenant_network = False']) + self._verify_section_contains( + section, + ['# No generic password section', + '# No ssh section', ]) + + # Now switch on the password section + config['driver-auth-type'] = 'password' + c = manila_generic.ManilaGenericCharm() + lines = c.get_config_for_principal(auth_data) + section = lines[conf]['[test-backend]'] + self.assertNotIn('# No generic password section', section) + self.assertIn('service_instance_password = password', section) + + # Now switch on the SSH section + config['driver_service_ssh_key'] = 'ssh-key' + config['driver-service-ssh-key-public'] = 'ssh-key-public' + config['driver-auth-type'] = 'ssh' + c = manila_generic.ManilaGenericCharm() + lines = c.get_config_for_principal(auth_data) + section = lines[conf]['[test-backend]'] + self.assertNotIn('# No ssh section', section) + self.assertIn('# No generic password section', section) + # test for ssh lines + self._verify_section_contains( + section, + ['path_to_private_key = {}' + .format(manila_generic.MANILA_SSH_KEY_PATH), + 'path_to_public_key = {}' + .format(manila_generic.MANILA_SSH_KEY_PATH_PUBLIC), + 'manila_service_keypair_name = my-keyname', ]) + + # Enable the connect_share_to_tenant_network and both password and ssh + config['driver-auth-type'] = 'both' + config['driver-connect-share-server-to-tenant-network'] = True + c = manila_generic.ManilaGenericCharm() + lines = c.get_config_for_principal(auth_data) + section = lines[conf]['[test-backend]'] + self.assertNotIn('# No ssh section', section) + self.assertNotIn('# No generic password section', section) + self.assertIn('service_instance_password = password', section) + # test for ssh lines + self._verify_section_contains( + section, + ['path_to_private_key = {}' + .format(manila_generic.MANILA_SSH_KEY_PATH), + 'path_to_public_key = {}' + .format(manila_generic.MANILA_SSH_KEY_PATH_PUBLIC), + 'manila_service_keypair_name = my-keyname', ]) + self.assertIn('connect_share_server_to_tenant_network = True', section) + + def _verify_section_contains(self, section, lines): + index = section.index(lines[0]) + for i, line in enumerate(lines): + self.assertEqual(section[index + i], line) + + def test_maybe_write_ssh_keys(self): + config = { + 'driver-keypair-name': '', + 'driver-auth-type': '', + 'driver-service-ssh-key': '', + 'driver-service-ssh-key-public': '' + } + self._patch_config_and_charm(config) + c = manila_generic.ManilaGenericCharm() + # The 'maybe_write_ssh_keys' should attempt to delete two files + self.patch_object(manila_generic.os, 'remove') + c.maybe_write_ssh_keys() + self.assertEqual(self.remove.call_count, 2) + print(self.remove.call_args_list) + self.assertEqual(self.remove.call_args_list, [ + mock.call(manila_generic.MANILA_SSH_KEY_PATH), + mock.call(manila_generic.MANILA_SSH_KEY_PATH_PUBLIC)]) + # now configure it up and check the writes happen + config['driver-keypair-name'] = 'mykeypair' + config['driver-auth-type'] = 'both' + config['driver-service-ssh-key'] = 'this is my key' + config['driver-service-ssh-key-public'] = 'my public key' + c = manila_generic.ManilaGenericCharm() + self.patch_object(manila_generic, 'write_file') + c.maybe_write_ssh_keys() + self.assertEqual(self.write_file.call_count, 2) + self.write_file.assert_has_calls( + [mock.call('this is my key', manila_generic.MANILA_SSH_KEY_PATH), + mock.call('my public key', + manila_generic.MANILA_SSH_KEY_PATH_PUBLIC, + 0o644)]) + + +class TestAuxilaryFunctions(Helper): + + def test_write_file(self): + f = mock.MagicMock() + self.patch_object(manila_generic.os, 'fdopen', return_value=f) + self.patch_object(manila_generic.os, 'open', return_value='opener') + text = """ + This + One""" + # strip the first new line off when passing the test string through + # this is to test dedenting strings + manila_generic.write_file(text[1:], 'file1') + self.open.assert_called_once_with( + 'file1', + manila_generic.os.O_WRONLY | manila_generic.os.O_CREAT, + 0o600) + self.fdopen.assert_called_once_with('opener', 'w') + f.__enter__().write.assert_called_once_with("This\nOne") + + def test_write_file_private(self): + f = mock.MagicMock() + self.patch_object(manila_generic.os, 'fdopen', return_value=f) + self.patch_object(manila_generic.os, 'open', return_value='opener') + text = """ + This + Two""" + # strip the first new line off when passing the test string through + # this is to test dedenting strings + manila_generic.write_file(text[1:], 'file1', chown=0o644) + self.open.assert_called_once_with( + 'file1', + manila_generic.os.O_WRONLY | manila_generic.os.O_CREAT, + 0o644) + self.fdopen.assert_called_once_with('opener', 'w') + f.__enter__().write.assert_called_once_with("This\nTwo") diff --git a/unit_tests/test_manila_generic_handlers.py b/unit_tests/test_manila_generic_handlers.py new file mode 100644 index 0000000..fbaab9f --- /dev/null +++ b/unit_tests/test_manila_generic_handlers.py @@ -0,0 +1,77 @@ +# Copyright 2016 Canonical Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import absolute_import +from __future__ import print_function + +import mock + +import reactive.manila_generic_handlers as handlers + +import charms_openstack.test_utils as test_utils + + +class TestRegisteredHooks(test_utils.TestRegisteredHooks): + + def test_hooks(self): + defaults = [ + 'charm.installed', + 'update-status'] + hook_set = { + 'when': { + 'send_config': ('manila-plugin.available', ), + 'update_config': ('manila-plugin.available', + 'config.changed', ), + }, + 'when_not': { + 'send_config': ('config.changed', ), + }, + } + # test that the hooks were registered via the + # reactive.barbican_handlers + self.registered_hooks_test_helper(handlers, hook_set, defaults) + + +class TestHandlerFunctions(test_utils.PatchHelper): + + def _patch_provide_charm_instance(self): + manila_generic_charm = mock.MagicMock() + self.patch('charms_openstack.charm.provide_charm_instance', + name='provide_charm_instance', + new=mock.MagicMock()) + self.provide_charm_instance().__enter__.return_value = \ + manila_generic_charm + self.provide_charm_instance().__exit__.return_value = None + return manila_generic_charm + + def test_send_config(self): + generic = self._patch_provide_charm_instance() + + class FakeManilaPlugin(object): + + name = None + configuration_data = None + authentication_data = 'auth data' + + generic.get_config_for_principal.return_value = "some data" + manila_plugin = FakeManilaPlugin() + handlers.send_config(manila_plugin) + + # test for expecations + self.assertEqual(manila_plugin.name, + generic.options.share_backend_name) + self.assertEqual(manila_plugin.configuration_data, "some data") + generic.get_config_for_principal.assert_called_once_with('auth data') + generic.assess_status.assert_called_once_with() + generic.maybe_write_ssh_keys.assert_called_once_with()