Initial commite
This commit is contained in:
commit
7c188a3d59
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal 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
3
.stestr.conf
Normal file
@ -0,0 +1,3 @@
|
||||
[DEFAULT]
|
||||
test_path=./unit_tests
|
||||
top_dir=./
|
38
README.md
Normal file
38
README.md
Normal 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
16
copyright
Normal 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
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
# Requirements to build the charm
|
||||
charm-tools
|
38
src/README.md
Normal file
38
src/README.md
Normal 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
52
src/config.yaml
Normal 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
16
src/copyright
Normal 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.
|
13
src/files/trilio/tvault-datamover-api.service
Normal file
13
src/files/trilio/tvault-datamover-api.service
Normal 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
23
src/icon.svg
Normal 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
5
src/layer.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
includes: ['layer:openstack-api']
|
||||
options:
|
||||
basic:
|
||||
use_venv: True
|
||||
include_system_packages: True
|
166
src/lib/charm/openstack/dmapi.py
Normal file
166
src/lib/charm/openstack/dmapi.py
Normal 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
24
src/metadata.yaml
Normal 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
|
195
src/reactive/dmapi_handlers.py
Normal file
195
src/reactive/dmapi_handlers.py
Normal 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
60
src/templates/dmapi.conf
Normal 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
9
test-requirements.txt
Normal 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
40
tox.ini
Normal 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
8
unit_tests/__init__.py
Normal 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()
|
109
unit_tests/test_charm_openstack_dmapi.py
Normal file
109
unit_tests/test_charm_openstack_dmapi.py
Normal 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()
|
172
unit_tests/test_dmapi_handlers.py
Normal file
172
unit_tests/test_dmapi_handlers.py
Normal 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()
|
Loading…
Reference in New Issue
Block a user