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
This commit is contained in:
Alex Kavanagh 2016-11-16 10:59:53 +00:00
parent 1142d53f3b
commit 9b039d3981
29 changed files with 1918 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
build
.tox
layers
interfaces
trusty
.testrepository
__pycache__

8
.testr.conf Normal file
View File

@ -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

10
HACKING.md Normal file
View File

@ -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: <https://github.com/openstack/charm-manila-generic>.

202
LICENSE Normal file
View File

@ -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.

11
Makefile Normal file
View File

@ -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

19
README.md Normal file
View File

@ -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.

57
TODO.md Normal file
View File

@ -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!

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
#charm-tools
git+https://github.com/juju/charm-tools#egg=charm-tools
simplejson

29
src/README.md Normal file
View File

@ -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).

96
src/config.yaml Normal file
View File

@ -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.

4
src/layer.yaml Normal file
View File

@ -0,0 +1,4 @@
includes:
- layer:openstack
- interface:manila-plugin
repo: https://github.com/openstack/charm-manila-generic

13
src/lib/__init__.py Normal file
View File

@ -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.

13
src/lib/charm/__init__.py Normal file
View File

@ -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.

View File

@ -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.

View File

@ -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": <boolean>,
'<config file>': {
'<section>: (
(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": <message>
}
: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)))

27
src/metadata.yaml Normal file
View File

@ -0,0 +1,27 @@
name: manila-generic
summary: A generic backend configuration charm for manila.
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
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

13
src/reactive/__init__.py Normal file
View File

@ -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.

View File

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

22
src/test-requirements.txt Normal file
View File

@ -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

9
src/tests/README.md Normal file
View File

@ -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.

View File

@ -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')

View File

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

17
src/tests/tests.yaml Normal file
View File

@ -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:

53
src/tox.ini Normal file
View File

@ -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}

7
test-requirements.txt Normal file
View File

@ -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

53
tox.ini Normal file
View File

@ -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

45
unit_tests/__init__.py Normal file
View File

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

View File

@ -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")

View File

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