Initial commite

This commit is contained in:
James Page 2020-03-04 10:11:16 +00:00
commit 7c188a3d59
20 changed files with 1000 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
bin
.coverage
.testrepository
.tox
*.sw[nop]
*.pyc
.unit-state.db
.stestr
__pycache__
func-results.json
tests/id_rsa_zaza

3
.stestr.conf Normal file
View File

@ -0,0 +1,3 @@
[DEFAULT]
test_path=./unit_tests
top_dir=./

38
README.md Normal file
View File

@ -0,0 +1,38 @@
# Overview
TrilioVault Data Mover API provides API service for TrilioVault Datamover
# Usage
TrilioVault Data Mover API relies on services from mysql, rabbitmq-server
and keystone charms. Steps to deploy the charm:
juju deploy trilio-dm-api
juju deploy keystone
juju deploy mysql
juju deploy rabbitmq-server
juju add-relation trilio-dm-api rabbitmq-server
juju add-relation trilio-dm-api mysql
juju add-relation trilio-dm-api keystone
# Configuration
python-version: "Openstack base python version(2 or 3)"
NOTE - Default value is set to "3". Please ensure to update this based on python version since installing
python3 packages on python2 based setup might have unexpected impact.
TrilioVault Packages are downloaded from the repository added in below config parameter. Please change this only if you wish to download
TrilioVault Packages from a different source.
triliovault-pkg-source: Repository address of triliovault packages
# Contact Information
Trilio Support <support@trilio.com>

16
copyright Normal file
View File

@ -0,0 +1,16 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0
Files: *
Copyright: 2018, Trilio
License: Apache-2.0
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.

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
# Requirements to build the charm
charm-tools

38
src/README.md Normal file
View File

@ -0,0 +1,38 @@
# Overview
TrilioVault Data Mover API provides API service for TrilioVault Datamover
# Usage
TrilioVault Data Mover API relies on services from mysql, rabbitmq-server
and keystone charms. Steps to deploy the charm:
juju deploy trilio-dm-api
juju deploy keystone
juju deploy mysql
juju deploy rabbitmq-server
juju add-relation trilio-dm-api rabbitmq-server
juju add-relation trilio-dm-api mysql
juju add-relation trilio-dm-api keystone
# Configuration
python-version: "Openstack base python version(2 or 3)"
NOTE - Default value is set to "3". Please ensure to update this based on python version since installing
python3 packages on python2 based setup might have unexpected impact.
TrilioVault Packages are downloaded from the repository added in below config parameter. Please change this only if you wish to download
TrilioVault Packages from a different source.
triliovault-pkg-source: Repository address of triliovault packages
# Contact Information
Trilio Support <support@trilio.com>

52
src/config.yaml Normal file
View File

@ -0,0 +1,52 @@
---
options:
openstack-origin:
type: string
default: bionic-stein
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 Ubuntu Cloud Archive (UCA) release pocket.
.
Supported UCA sources include:
.
cloud:<series>-<openstack-release>
cloud:<series>-<openstack-release>/updates
cloud:<series>-<openstack-release>/staging
cloud:<series>-<openstack-release>/proposed
.
For series=Precise we support UCA for openstack-release=
* icehouse
.
For series=Trusty we support UCA for openstack-release=
* juno
* kilo
* ...
.
NOTE: updating this setting to a source that is known to provide
a later version of OpenStack will trigger a software upgrade.
.
python-version:
type: int
default: 3
description: Openstack base python version(2 or 3)
triliovault-pkg-source:
type: string
default: "deb [trusted=yes] https://apt.fury.io/triliodata-3-4/ /"
description: Repository address of triliovault packages
openstack-pkg-source:
type: string
default: "cloud-archive:queens"
description: Repository address of openstack packages
public-port:
type: int
default: 8784
description: DataMover API public endpoint port
internal-port:
type: int
default: 8784
description: DataMover API internal endpoint port
admin-port:
type: int
default: 8784
description: DataMover API admin endpoint port

16
src/copyright Normal file
View File

@ -0,0 +1,16 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0
Files: *
Copyright: 2018, Trilio
License: Apache-2.0
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 @@
[Unit]
Description=Datamover API service
[Service]
User = dmapi
Group = dmapi
Type = Simple
ExecStart=/usr/bin/dmapi-api
KillMode=process
Restart=on-failure
[Install]
WantedBy=multi-user.target

23
src/icon.svg Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000" style="enable-background:new 0 0 1000 1000;" xml:space="preserve">
<style type="text/css">
.st0{fill:#77BC1F;}
.st1{fill:#FFFFFF;}
</style>
<circle class="st0" cx="500" cy="500.9" r="492.5"/>
<g>
<path class="st1" d="M500.1,799.9c79.9,0,153.9-30.2,208.3-85c55-55.4,85-131.1,84.5-213.1c0-167.8-128.6-299.2-292.7-299.2h-33.6
v23.1h33.6c151.2,0,269.5,121.3,269.5,276.2c0.6,75.8-27,145.7-77.7,196.8c-50.1,50.3-118.2,78-191.9,78
c-153.7,0-269.5-118.3-269.5-275.1c0-68.6,23.7-133.8,66.9-183.7l-16.6-16.5c-47.4,54.4-73.5,125.4-73.5,200.3
C207.4,671.7,333.2,799.9,500.1,799.9"/>
<path class="st1" d="M500.1,846c92.2,0,177.5-34.8,240.4-98.1c63.4-63.9,98-151.3,97.3-246c0-192.8-147.7-344.4-336.1-345.4h-12.5
V48.3L332.2,215l157.1,166.7v-110h10.8c125.8,0,224.4,101,224.4,230c0.4,63.2-22.5,121.5-64.7,163.9c-41.6,41.9-98.4,65-159.7,65
c-128,0-224.4-98.4-224.4-229c0-56.2,19.2-109.8,54.2-151.3L313.3,334c-39.3,45.8-60.8,105.3-60.8,167.7
c0,143.7,106.4,252.1,247.6,252.1c67.6,0,130.2-25.5,176.1-71.9c46.5-46.9,71.9-110.8,71.4-180.2c0-141.9-108.7-253-247.5-253
h-33.9v74.9L364,215l102.2-108.5v73.1h33.9c176.5,0,314.7,141.6,314.7,322.4c0.7,88.6-31.6,170.1-90.7,229.7
c-58.4,58.8-137.9,91.2-224,91.2c-179.4,0-314.7-138-314.7-321c0-80.9,28.2-157.6,79.5-216.3l-16.5-16.5
c-55.6,63-86.2,145.6-86.2,232.8C162.2,698.1,307.5,846,500.1,846"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

5
src/layer.yaml Normal file
View File

@ -0,0 +1,5 @@
includes: ['layer:openstack-api']
options:
basic:
use_venv: True
include_system_packages: True

View File

@ -0,0 +1,166 @@
import os
import charmhelpers.contrib.openstack.utils as ch_utils
import charms_openstack.charm
import charms_openstack.adapters
import charms_openstack.ip as os_ip
from charmhelpers.core.hookenv import (
config
)
DMAPI_DIR = '/etc/dmapi'
DMAPI_CONF = os.path.join(DMAPI_DIR, 'dmapi.conf')
class DmapiDBAdapter(charms_openstack.adapters.DatabaseRelationAdapter):
"""Get database URIs for the two nova databases"""
@property
def dmapi_nova_uri(self):
"""URI for nova DB"""
return self.get_uri(prefix='dmapinova')
@property
def dmapi_nova_api_uri(self):
"""URI for nova_api DB"""
return self.get_uri(prefix='dmapinovaapi')
class DmapiAdapters(charms_openstack.adapters.OpenStackAPIRelationAdapters):
"""
Adapters class for the Data Mover API charm.
"""
relation_adapters = {
'shared_db': DmapiDBAdapter,
}
class DmapiCharm(charms_openstack.charm.HAOpenStackCharm):
# Internal name of charm + keystone endpoint
service_name = 'dmapi'
name = 'trilio-dm-api'
# First release supported
release = 'queens'
# Packages the service needs installed
if config('python-version') == 3:
packages = ['python3-nova', 'python3-dmapi']
else:
packages = ['python-nova', 'dmapi']
# Init services the charm manages
services = ['tvault-datamover-api']
# Ports that need exposing.
default_service = 'dmapi-api'
api_ports = {
'dmapi-api': {
os_ip.PUBLIC: config('public-port'),
os_ip.ADMIN: config('admin-port'),
os_ip.INTERNAL: config('internal-port'),
}
}
# Database sync command used to initalise the schema.
sync_cmd = []
# The restart map defines which services should be restarted when a given
# file changes
restart_map = {
DMAPI_CONF: services,
}
adapters_class = DmapiAdapters
# Resource when in HA mode
ha_resources = ['vips', 'haproxy']
# DataMover requires a message queue, database and keystone to work,
# so these are the 'required' relationships for the service to
# have an 'active' workload status. 'required_relations' is used in
# the assess_status() functionality to determine what the current
# workload status of the charm is.
required_relations = ['amqp', 'shared-db', 'identity-service']
def __init__(self, release=None, **kwargs):
"""Custom initialiser for class
If no release is passed, then the charm determines the release from the
ch_utils.os_release() function.
"""
if release is None:
release = ch_utils.os_release('python-keystonemiddleware')
super(DmapiCharm, self).__init__(release=release, **kwargs)
def install(self):
"""Customise the installation, configure the source and then call the
parent install() method to install the packages
"""
self.configure_source()
# and do the actual install
super(DmapiCharm, self).install()
@property
def public_url(self):
return super().public_url + "/v2"
@property
def admin_url(self):
return super().admin_url + "/v2"
@property
def internal_url(self):
return super().internal_url + "/v2"
def install():
"""Use the singleton from the DmapiCharm to install the packages on the
unit
"""
DmapiCharm.singleton.install()
def restart_all():
"""Use the singleton from the DmapiCharm to restart services on the
unit
"""
DmapiCharm.singleton.restart_all()
def setup_endpoint(keystone):
"""When the keystone interface connects, register this unit in the keystone
catalogue.
"""
charm = DmapiCharm.singleton
keystone.register_endpoints(charm.service_name,
charm.region,
charm.public_url,
charm.internal_url,
charm.admin_url)
def render_configs(interfaces_list):
"""Using a list of interfaces, render the configs and, if they have
changes, restart the services on the unit.
"""
DmapiCharm.singleton.render_with_interfaces(interfaces_list)
def assess_status():
"""Just call the DmapiCharm.singleton.assess_status() command to update
status on the unit.
"""
DmapiCharm.singleton.assess_status()
def configure_ha_resources(hacluster):
"""Use the singleton from the DmapiCharm to run configure_ha_resources
"""
DmapiCharm.singleton.configure_ha_resources(hacluster)
def configure_ssl():
"""Use the singleton from the DmapiCharm to run configure_ssl
"""
DmapiCharm.singleton.configure_ssl()

24
src/metadata.yaml Normal file
View File

@ -0,0 +1,24 @@
---
name: trilio-dm-api
maintainer: Trilio Support <support@trilio.io>
summary: TrilioVault Data Mover API
description: |
API service of TrilioVault Datamover
tags:
- openstack
- storage
- backup
- TVMv3.4
requires:
shared-db:
interface: mysql-shared
amqp:
interface: rabbitmq
identity-service:
interface: keystone
provides:
dm-api:
interface: dm-api
series:
- xenial
- bionic

View File

@ -0,0 +1,195 @@
import charms.reactive as reactive
import os
import re
# This charm's library contains all of the handler code associated with
# dmapi
import charm.openstack.dmapi as dmapi
from subprocess import (
check_output,
check_call,
)
from charmhelpers.core.hookenv import (
config,
log,
application_version_set,
)
from charmhelpers.fetch import (
apt_update,
apt_upgrade,
)
from charmhelpers.contrib.openstack.utils import (
configure_installation_source,
)
from charmhelpers.core.host import (
service_restart,
adduser,
add_group,
add_user_to_group,
chownr,
mkdir,
)
# Minimal inferfaces required for operation
MINIMAL_INTERFACES = [
'shared-db.available',
'identity-service.available',
'amqp.available',
]
DMAPI_USR = 'dmapi'
DMAPI_GRP = 'dmapi'
def get_new_version(pkg_name):
"""
Get the latest version available on the TrilioVault node.
"""
apt_cmd = "apt list {}".format(pkg_name)
pkg = check_output(apt_cmd.split()).decode('utf-8')
new_ver = re.search(r'\s([\d.]+)', pkg).group().strip()
return new_ver
def add_user():
"""
Adding passwordless sudo access to nova user and adding to required groups
"""
try:
add_group(DMAPI_GRP, system_group=True)
adduser(DMAPI_USR, password=None, shell='/bin/bash', system_user=True)
add_user_to_group(DMAPI_USR, DMAPI_GRP)
except Exception as e:
log("Failed while adding user with msg: {}".format(e))
return False
return True
# use a synthetic state to ensure that it get it to be installed independent of
# the install hook.
@reactive.when_not('charm.installed')
def install_packages():
# Add TrilioVault repository to install required package
# and add queens repo to install nova libraries
if not add_user():
log("Adding dmapi user failed!")
return
os.system('sudo echo "{}" > '
'/etc/apt/sources.list.d/trilio-gemfury-sources.list'.format(
config('triliovault-pkg-source')))
new_src = config('openstack-origin')
configure_installation_source(new_src)
if config('python-version') == 2:
dmapi_pkg = 'dmapi'
else:
dmapi_pkg = 'python3-dmapi'
apt_update()
dmapi.install()
# Placing the service file
os.system('sudo cp files/trilio/tvault-datamover-api.service '
'/etc/systemd/system/')
chownr('/var/log/dmapi', DMAPI_USR, DMAPI_GRP)
mkdir('/var/cache/dmapi', DMAPI_USR, DMAPI_GRP, perms=493)
os.system('sudo systemctl enable tvault-datamover-api')
service_restart('tvault-datamover-api')
application_version_set(get_new_version(dmapi_pkg))
reactive.set_state('charm.installed')
@reactive.when('amqp.connected')
def setup_amqp_req(amqp):
"""Use the amqp interface to request access to the amqp broker using our
local configuration.
"""
amqp.request_access(username='dmapi',
vhost='openstack')
dmapi.assess_status()
@reactive.when('shared-db.connected')
def setup_database(database):
"""On receiving database credentials, configure the database on the
interface.
"""
database.configure('nova', 'nova', prefix='dmapinova')
database.configure('nova_api', 'nova', prefix='dmapinovaapi')
dmapi.assess_status()
@reactive.when('identity-service.connected')
def setup_endpoint(keystone):
dmapi.configure_ssl()
dmapi.setup_endpoint(keystone)
dmapi.assess_status()
def render(*args):
dmapi.render_configs(args)
reactive.set_state('config.complete')
# change the ownership to 'dmapi'
chownr('/etc/dmapi', DMAPI_USR, DMAPI_GRP)
dmapi.assess_status()
@reactive.when('charm.installed')
@reactive.when_not('cluster.available')
@reactive.when(*MINIMAL_INTERFACES)
def render_unclustered(*args):
dmapi.configure_ssl()
render(*args)
@reactive.when('charm.installed')
@reactive.when('cluster.available',
*MINIMAL_INTERFACES)
def render_clustered(*args):
render(*args)
@reactive.when('charm.installed')
@reactive.when('config.complete')
@reactive.when_not('db.synced')
def run_db_migration():
dmapi.restart_all()
reactive.set_state('db.synced')
dmapi.assess_status()
@reactive.when('ha.connected')
def cluster_connected(hacluster):
dmapi.configure_ha_resources(hacluster)
@reactive.hook('upgrade-charm')
def upgrade_charm():
os.system('sudo echo "{}" > '
'/etc/apt/sources.list.d/trilio-gemfury-sources.list'.format(
config('triliovault-pkg-source')))
new_src = config('openstack-origin')
configure_installation_source(new_src)
apt_update()
apt_upgrade(fatal=True, dist=True)
chownr('/var/log/dmapi', DMAPI_USR, DMAPI_GRP)
check_call(['systemctl', 'daemon-reload'])
service_restart('tvault-datamover-api')
if config('python-version') == 2:
dmapi_pkg = 'dmapi'
else:
dmapi_pkg = 'python3-dmapi'
application_version_set(get_new_version(dmapi_pkg))

60
src/templates/dmapi.conf Normal file
View File

@ -0,0 +1,60 @@
[DEFAULT]
dmapi_workers = {{ options.workers }}
{% if amqp.ssl_port %}
transport_url = rabbit://{{amqp.username}}:{{amqp.password}}@{{amqp.host}}:{{amqp.ssl_port}}/{{amqp.vhost}}
{% else %}
transport_url = rabbit://{{amqp.username}}:{{amqp.password}}@{{amqp.host}}:5672/{{amqp.vhost}}
{% endif %}
dmapi_link_prefix = {{ options.service_listen_info.dmapi_api.ip }}:{{ options.service_listen_info.dmapi_api.port }}
dmapi_listen_port = {{ options.service_listen_info.dmapi_api.port }}
dmapi_enabled_apis = dmapi
dmapi_enabled_ssl_apis =
bindir = /usr/bin
instance_name_template = instance-%08x
dmapi_listen = 0.0.0.0
my_ip = {{ options.service_listen_info.dmapi_api.ip }}
rootwrap_config = /etc/dmapi/rootwrap.conf
debug = {{ options.debug }}
log_file = /var/log/dmapi/dmapi.log
log_dir = /var/log/dmapi
[wsgi]
ssl_cert_file = {{ amqp.ssl_cert_file }}
ssl_key_file = {{ amqp.ssl_key_file }}
api_paste_config = /etc/dmapi/api-paste.ini
[database]
connection = {{ shared_db.dmapi_nova_uri }}
[api_database]
connection = {{ shared_db.dmapi_nova_api_uri }}
{% include "parts/section-keystone-authtoken" %}
region_name = {{ options.region }}
{% if options.ssl_ca %}
insecure = False
{% else %}
insecure = True
{% endif %}
{% if options.use_internal_endpoints -%}
interface = internalURL
{%- endif %}
[oslo_messaging_notifications]
driver = messagingv2
{% if amqp.ssl_port %}
transport_url = rabbit://{{amqp.username}}:{{amqp.password}}@{{amqp.host}}:{{amqp.ssl_port}}/{{amqp.vhost}}
{% else %}
transport_url = rabbit://{{amqp.username}}:{{amqp.password}}@{{amqp.host}}:5672/{{amqp.vhost}}
{% endif %}
[oslo_middleware]
enable_proxy_headers_parsing = false
[conductor]
use_local = True
{% include "parts/section-oslo-messaging-rabbit" %}

9
test-requirements.txt Normal file
View File

@ -0,0 +1,9 @@
# Unit test requirements
netifaces
hvac
flake8>=2.2.4,<=2.4.1
os-testr>=0.4.1
charms.reactive
mock>=1.2
coverage>=3.6
git+https://github.com/openstack/charms.openstack#egg=charms.openstack

40
tox.ini Normal file
View File

@ -0,0 +1,40 @@
# tox (https://tox.readthedocs.io/) is a tool for running tests
# in multiple virtualenvs. This configuration file will run the
# test suite on all supported python versions. To use it, "pip install tox"
# and then run "tox" from this directory.
[tox]
skipsdist = True
envlist = pep8, py27, py3
[testenv]
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
TERM=linux
INTERFACE_PATH={toxinidir}/interfaces
LAYER_PATH={toxinidir}/layers
JUJU_REPOSITORY={toxinidir}/build
install_command =
pip install {opts} {packages}
deps =
-r{toxinidir}/requirements.txt
[testenv:build]
basepython = python3
commands =
charm-build --log-level DEBUG -o {toxinidir}/build src {posargs}
[testenv:py27]
basepython = python2.7
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run {posargs}
[testenv:py3]
basepython = python3
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run {posargs}
[testenv:pep8]
basepython = python3
deps = -r{toxinidir}/test-requirements.txt
commands = flake8 {posargs} src

8
unit_tests/__init__.py Normal file
View File

@ -0,0 +1,8 @@
import sys
sys.path.append('src')
sys.path.append('src/lib')
# Mock out charmhelpers so that we can test without it.
import charms_openstack.test_mocks # noqa
charms_openstack.test_mocks.mock_charmhelpers()

View File

@ -0,0 +1,109 @@
from __future__ import absolute_import
from __future__ import print_function
import mock
import sys
import charm.openstack.dmapi as dmapi
import charms_openstack.test_utils as test_utils
class Helper(test_utils.PatchHelper):
def setUp(self):
super().setUp()
self.patch_release(dmapi.DmapiCharm.release)
class TestOpenStackDmapi(Helper):
def test_install(self):
self.patch_object(dmapi.DmapiCharm.singleton, 'install')
dmapi.install()
self.install.assert_called_once_with()
def test_setup_endpoint(self):
self.patch_object(dmapi.DmapiCharm, 'service_name',
new_callable=mock.PropertyMock)
self.patch_object(dmapi.DmapiCharm, 'region',
new_callable=mock.PropertyMock)
self.patch_object(dmapi.DmapiCharm, 'public_url',
new_callable=mock.PropertyMock)
self.patch_object(dmapi.DmapiCharm, 'internal_url',
new_callable=mock.PropertyMock)
self.patch_object(dmapi.DmapiCharm, 'admin_url',
new_callable=mock.PropertyMock)
self.service_name.return_value = 'type1'
self.region.return_value = 'region1'
self.public_url.return_value = 'public_url'
self.internal_url.return_value = 'internal_url'
self.admin_url.return_value = 'admin_url'
keystone = mock.MagicMock()
dmapi.setup_endpoint(keystone)
keystone.register_endpoints.assert_called_once_with(
'type1', 'region1', 'public_url', 'internal_url', 'admin_url')
def test_render_configs(self):
self.patch_object(dmapi.DmapiCharm.singleton, 'render_with_interfaces')
dmapi.render_configs('interfaces-list')
self.render_with_interfaces.assert_called_once_with(
'interfaces-list')
class TestDmapiDBAdapter(Helper):
def fake_get_uri(self, prefix):
return 'mysql://uri/{}-database'.format(prefix)
def test_dmapi_uri(self):
relation = mock.MagicMock()
a = dmapi.DmapiDBAdapter(relation)
self.patch_object(dmapi.DmapiDBAdapter, 'get_uri')
self.get_uri.side_effect = self.fake_get_uri
self.assertEqual(a.dmapi_nova_uri, 'mysql://uri/dmapinova-database')
self.assertEqual(a.dmapi_nova_api_uri, 'mysql://uri/dmapinovaapi-database')
class TestDmapiAdapters(Helper):
@mock.patch('charmhelpers.core.hookenv.config')
def test_dmapi_adapters(self, config):
reply = {
'keystone-api-version': '3',
}
config.side_effect = lambda: reply
self.patch_object(
dmapi.charms_openstack.adapters.APIConfigurationAdapter,
'get_network_addresses')
cluster_relation = mock.MagicMock()
cluster_relation.endpoint_name = 'cluster'
amqp_relation = mock.MagicMock()
amqp_relation.endpoint_name = 'amqp'
shared_db_relation = mock.MagicMock()
shared_db_relation.endpoint_name = 'shared_db'
other_relation = mock.MagicMock()
other_relation.endpoint_name = 'other'
other_relation.thingy = 'help'
# verify that the class is created with a DmapiConfigurationAdapter
b = dmapi.DmapiAdapters([amqp_relation,
cluster_relation,
shared_db_relation,
other_relation])
# ensure that the relevant things got put on.
self.assertTrue(
isinstance(
b.other,
dmapi.charms_openstack.adapters.OpenStackRelationAdapter))
class TestDmapiCharm(Helper):
def test_install(self):
b = dmapi.DmapiCharm()
self.patch_object(dmapi.charms_openstack.charm.OpenStackCharm,
'configure_source')
self.patch_object(dmapi.charms_openstack.charm.OpenStackCharm,
'install')
b.install()
self.configure_source.assert_called_with()
self.install.assert_called_once_with()

View File

@ -0,0 +1,172 @@
from __future__ import absolute_import
from __future__ import print_function
import unittest
import mock
import sys
import reactive.dmapi_handlers as handlers
_when_args = {}
_when_not_args = {}
def mock_hook_factory(d):
def mock_hook(*args, **kwargs):
def inner(f):
# remember what we were passed. Note that we can't actually
# determine the class we're attached to, as the decorator only gets
# the function.
try:
d[f.__name__].append(dict(args=args, kwargs=kwargs))
except KeyError:
d[f.__name__] = [dict(args=args, kwargs=kwargs)]
return f
return inner
return mock_hook
class TestDmapiHandlers(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls._patched_when = mock.patch('charms.reactive.when',
mock_hook_factory(_when_args))
cls._patched_when_started = cls._patched_when.start()
cls._patched_when_not = mock.patch('charms.reactive.when_not',
mock_hook_factory(_when_not_args))
cls._patched_when_not_started = cls._patched_when_not.start()
# force requires to rerun the mock_hook decorator:
# try except is Python2/Python3 compatibility as Python3 has moved
# reload to importlib.
try:
reload(handlers)
except NameError:
import importlib
importlib.reload(handlers)
@classmethod
def tearDownClass(cls):
cls._patched_when.stop()
cls._patched_when_started = None
cls._patched_when = None
cls._patched_when_not.stop()
cls._patched_when_not_started = None
cls._patched_when_not = None
# and fix any breakage we did to the module
try:
reload(handlers)
except NameError:
import importlib
importlib.reload(handlers)
def setUp(self):
self._patches = {}
self._patches_start = {}
def tearDown(self):
for k, v in self._patches.items():
v.stop()
setattr(self, k, None)
self._patches = None
self._patches_start = None
def patch(self, obj, attr, return_value=None, side_effect=None):
mocked = mock.patch.object(obj, attr)
self._patches[attr] = mocked
started = mocked.start()
started.return_value = return_value
started.side_effect = side_effect
self._patches_start[attr] = started
setattr(self, attr, started)
def test_registered_hooks(self):
# test that the hooks actually registered the relation expressions that
# are meaningful for this interface: this is to handle regressions.
# The keys are the function names that the hook attaches to.
when_patterns = {
'setup_amqp_req': ('amqp.connected', ),
'setup_database': ('shared-db.connected', ),
'setup_endpoint': ('identity-service.connected', ),
'render_unclustered': ('charm.installed',
'shared-db.available',
'identity-service.available',
'amqp.available',),
'render_clustered': ('charm.installed',
'shared-db.available',
'identity-service.available',
'amqp.available',
'cluster.available',),
'run_db_migration': ('charm.installed',
'config.complete', ),
'cluster_connected': ('ha.connected', ),
}
when_not_patterns = {
'install_packages': ('charm.installed', ),
'render_unclustered': ('cluster.available', ),
'run_db_migration': ('db.synced', ),
}
# check the when hooks are attached to the expected functions
for t, p in [(_when_args, when_patterns),
(_when_not_args, when_not_patterns)]:
for f, args in t.items():
# check that function is in patterns
self.assertTrue(f in p.keys(),
"{} not found".format(f))
# check that the lists are equal
l = []
for a in args:
l += a['args'][:]
self.assertEqual(sorted(l), sorted(p[f]),
"{}: incorrect state registration".format(f))
def test_install_packages(self):
self.patch(handlers.dmapi, 'install')
self.patch(handlers.reactive, 'set_state')
self.patch(handlers, 'add_user')
self.add_user.return_value = True
self.patch(handlers.os, 'system')
self.patch(handlers, 'apt_update')
self.patch(handlers, 'get_new_version')
self.patch(handlers, 'service_restart')
handlers.install_packages()
self.install.assert_called_once_with()
self.set_state.assert_called_once_with('charm.installed')
def test_setup_amqp_req(self):
self.patch(handlers.dmapi, 'assess_status')
amqp = mock.MagicMock()
handlers.setup_amqp_req(amqp)
amqp.request_access.assert_called_once_with(
username='dmapi', vhost='openstack')
def test_database(self):
database = mock.MagicMock()
self.patch(handlers.dmapi, 'assess_status')
handlers.setup_database(database)
database.configure.assert_has_calls([
mock.call('nova', 'nova', prefix='dmapinova'),
mock.call('nova_api', 'nova', prefix='dmapinovaapi'),
])
def test_setup_endpoint(self):
self.patch(handlers.dmapi, 'setup_endpoint')
self.patch(handlers.dmapi, 'assess_status')
self.patch(handlers.dmapi, 'configure_ssl')
handlers.setup_endpoint('keystone')
self.setup_endpoint.assert_called_once_with('keystone')
def test_render(self):
self.patch(handlers.dmapi, 'render_configs')
self.patch(handlers.dmapi, 'assess_status')
self.patch(handlers.dmapi, 'configure_ssl')
handlers.render_unclustered('args')
self.render_configs.assert_called_once_with(('args', ))
self.assess_status.assert_called_once()
self.configure_ssl.assert_called_once()