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()