MySQL Router Charm
The charm intelligently proxies database requests from clients to MySQL InnoDB Clusters.
This commit is contained in:
commit
67d9b4483c
|
@ -0,0 +1,14 @@
|
|||
.tox
|
||||
.stestr
|
||||
*__pycache__*
|
||||
*.pyc
|
||||
build
|
||||
interfaces
|
||||
layers
|
||||
README.ex
|
||||
|
||||
# Remove these
|
||||
src/tests/mysqlsh.snap
|
||||
src/tests/bundles/overlays/local-charm-overlay.yaml.j2
|
||||
src/files
|
||||
manual-attach.sh
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 15 KiB |
|
@ -0,0 +1,7 @@
|
|||
# This file is managed centrally. If you find the need to modify this as a
|
||||
# one-off, please don't. Intead, consult #openstack-charms and ask about
|
||||
# requirements management in charms via bot-control. Thank you.
|
||||
#
|
||||
# Build requirements
|
||||
charm-tools>=2.4.4
|
||||
simplejson
|
|
@ -0,0 +1,27 @@
|
|||
options:
|
||||
source:
|
||||
type: string
|
||||
default: distro
|
||||
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 e.g.
|
||||
.
|
||||
cloud:<series>-<openstack-release>
|
||||
cloud:<series>-<openstack-release>/updates
|
||||
cloud:<series>-<openstack-release>/staging
|
||||
cloud:<series>-<openstack-release>/proposed
|
||||
.
|
||||
See https://wiki.ubuntu.com/OpenStack/CloudArchive for info on which
|
||||
cloud archives are available and supported.
|
||||
system-user:
|
||||
# TODO What user? No mysql user exists. Create one?
|
||||
type: string
|
||||
description: System user to run mysqlrouter
|
||||
default: ubuntu
|
||||
base-port:
|
||||
type: int
|
||||
default: 3306
|
||||
description: |
|
||||
Base port number for RW interface. RO, xRW and xRO will
|
||||
increment from base_port.
|
|
@ -0,0 +1,15 @@
|
|||
includes:
|
||||
- layer:openstack
|
||||
- interface:mysql-shared
|
||||
- interface:mysql-router
|
||||
options:
|
||||
basic:
|
||||
use_venv: True
|
||||
packages: [ 'libmysqlclient-dev']
|
||||
repo: https://github.com/openstack-charmers/charm-mysql-router
|
||||
config:
|
||||
deletes:
|
||||
- verbose
|
||||
- openstack-origin
|
||||
- use-internal-endpoints
|
||||
- debug
|
|
@ -0,0 +1,290 @@
|
|||
# Copyright 2019 Canonicauh 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 json
|
||||
import subprocess
|
||||
|
||||
import charms_openstack.charm
|
||||
import charms_openstack.adapters
|
||||
|
||||
import charms.reactive as reactive
|
||||
|
||||
import charmhelpers.core as ch_core
|
||||
import charmhelpers.contrib.network.ip as ch_net_ip
|
||||
|
||||
import charmhelpers.contrib.database.mysql as mysql
|
||||
|
||||
|
||||
MYSQLD_CNF = "/etc/mysql/mysql.conf.d/mysqld.cnf"
|
||||
|
||||
# Flag Strings
|
||||
MYSQL_ROUTER_BOOTSTRAPPED = "charm.mysqlrouter.bootstrapped"
|
||||
MYSQL_ROUTER_STARTED = "charm.mysqlrouter.started"
|
||||
DB_ROUTER_AVAILABLE = "db-router.available"
|
||||
DB_ROUTER_PROXY_AVAILABLE = "db-router.available.proxy"
|
||||
|
||||
|
||||
@charms_openstack.adapters.config_property
|
||||
def db_router_address(cls):
|
||||
return ch_net_ip.get_relation_ip("db-router")
|
||||
|
||||
|
||||
@charms_openstack.adapters.config_property
|
||||
def shared_db_address(cls):
|
||||
# This is is a subordinate relation, we want mysql communication
|
||||
# to run over localhost
|
||||
# Alternatively: ch_net_ip.get_relation_ip("shared-db")
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
class MySQLRouterCharm(charms_openstack.charm.OpenStackCharm):
|
||||
"""Charm class for the MySQLRouter charm."""
|
||||
name = "mysqlrouter"
|
||||
packages = ["mysql-router"]
|
||||
release = "stein"
|
||||
release_pkg = "mysql-router"
|
||||
required_relations = ["db-router", "shared-db"]
|
||||
source_config_key = "source"
|
||||
|
||||
# FIXME Can we have a non-systemd services?
|
||||
# Create a systemd shim?
|
||||
# services = ["mysqlrouter"]
|
||||
services = []
|
||||
# TODO Post bootstrap config management and restarts
|
||||
restart_map = {}
|
||||
# TODO Pick group owner
|
||||
group = "mysql"
|
||||
|
||||
# For internal use with mysql.get_db_data
|
||||
_unprefixed = "MRUP"
|
||||
|
||||
@property
|
||||
def mysqlrouter_bin(self):
|
||||
return "/usr/bin/mysqlrouter"
|
||||
|
||||
@property
|
||||
def db_router_endpoint(self):
|
||||
return reactive.relations.endpoint_from_flag("db-router.available")
|
||||
|
||||
@property
|
||||
def db_prefix(self):
|
||||
return "mysqlrouter"
|
||||
|
||||
@property
|
||||
def db_router_user(self):
|
||||
# The prefix will be prepended
|
||||
return "{}user".format(self.db_prefix)
|
||||
|
||||
@property
|
||||
def db_router_password(self):
|
||||
return json.loads(
|
||||
self.db_router_endpoint.password(prefix=self.db_prefix))
|
||||
|
||||
@property
|
||||
def db_router_address(self):
|
||||
""" My address """
|
||||
return self.options.db_router_address
|
||||
|
||||
@property
|
||||
def cluster_address(self):
|
||||
""" Database Cluster Addresss """
|
||||
return json.loads(self.db_router_endpoint.db_host())
|
||||
|
||||
@property
|
||||
def shared_db_address(self):
|
||||
""" My address """
|
||||
return self.options.shared_db_address
|
||||
|
||||
@property
|
||||
def mysqlrouter_dir(self):
|
||||
return "/home/{}/mysqlrouter".format(self.options.system_user)
|
||||
|
||||
def install(self):
|
||||
"""Custom install function.
|
||||
"""
|
||||
|
||||
# TODO: charms.openstack should probably do this
|
||||
# Need to configure source first
|
||||
self.configure_source()
|
||||
super().install()
|
||||
|
||||
def get_db_helper(self):
|
||||
|
||||
db_helper = mysql.MySQL8Helper(
|
||||
rpasswdf_template="/var/lib/charm/{}/mysql.passwd"
|
||||
.format(ch_core.hookenv.service_name()),
|
||||
upasswdf_template="/var/lib/charm/{}/mysql-{{}}.passwd"
|
||||
.format(ch_core.hookenv.service_name()),
|
||||
user=self.db_router_user,
|
||||
password=self.db_router_password,
|
||||
host=self.cluster_address)
|
||||
return db_helper
|
||||
|
||||
def states_to_check(self, required_relations=None):
|
||||
"""Custom state check function for charm specific state check needs.
|
||||
|
||||
"""
|
||||
states_to_check = super().states_to_check(required_relations)
|
||||
states_to_check["charm"] = [
|
||||
(MYSQL_ROUTER_BOOTSTRAPPED,
|
||||
"waiting",
|
||||
"MySQL-Router not yet bootstrapped"),
|
||||
(MYSQL_ROUTER_STARTED,
|
||||
"waiting",
|
||||
"MySQL-Router not yet started"),
|
||||
(DB_ROUTER_PROXY_AVAILABLE,
|
||||
"waiting",
|
||||
"Waiting for proxied DB creation from cluster")]
|
||||
|
||||
return states_to_check
|
||||
|
||||
def check_mysql_connection(self):
|
||||
"""Check if local instance of mysql is accessible.
|
||||
|
||||
Attempt a connection to the local instance of mysql to determine
|
||||
if it is running and accessible.
|
||||
|
||||
:side effect: Uses get_db_helper to execute a connection to the DB.
|
||||
:returns: boolean
|
||||
"""
|
||||
|
||||
m_helper = self.get_db_helper()
|
||||
try:
|
||||
m_helper.connect(self.db_router_user,
|
||||
self.db_router_password,
|
||||
self.shared_db_address)
|
||||
return True
|
||||
except mysql.MySQLdb._exceptions.OperationalError:
|
||||
ch_core.hookenv.log("Could not connect to db", "DEBUG")
|
||||
return False
|
||||
|
||||
def custom_assess_status_check(self):
|
||||
|
||||
# Start with default checks
|
||||
for f in [self.check_if_paused,
|
||||
self.check_interfaces,
|
||||
self.check_mandatory_config]:
|
||||
state, message = f()
|
||||
if state is not None:
|
||||
ch_core.hookenv.status_set(state, message)
|
||||
return state, message
|
||||
|
||||
# We should not get here until there is a connection to the
|
||||
# cluster (db-router available)
|
||||
if not self.check_mysql_connection():
|
||||
return "blocked", "Failed to connect to MySQL"
|
||||
|
||||
return None, None
|
||||
|
||||
def bootstrap_mysqlrouter(self):
|
||||
|
||||
cmd = [self.mysqlrouter_bin,
|
||||
"--user", self.options.system_user,
|
||||
"--bootstrap",
|
||||
"{}:{}@{}".format(self.db_router_user,
|
||||
self.db_router_password,
|
||||
self.cluster_address),
|
||||
"--directory", self.mysqlrouter_dir,
|
||||
"--conf-use-sockets",
|
||||
"--conf-base-port", str(self.options.base_port)]
|
||||
try:
|
||||
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
|
||||
ch_core.hookenv.log(output, "DEBUG")
|
||||
except subprocess.CalledProcessError as e:
|
||||
ch_core.hookenv.log(
|
||||
"Failed to bootstrap mysqlrouter: {}"
|
||||
.format(e.output.decode("UTF-8")), "ERROR")
|
||||
return
|
||||
reactive.flags.set_flag(MYSQL_ROUTER_BOOTSTRAPPED)
|
||||
|
||||
def start_mysqlrouter(self):
|
||||
cmd = ["{}/start.sh".format(self.mysqlrouter_dir)]
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, bufsize=1,
|
||||
universal_newlines=True)
|
||||
proc.wait()
|
||||
ch_core.hookenv.log("MySQL router started", "DEBUG")
|
||||
except subprocess.CalledProcessError as e:
|
||||
ch_core.hookenv.log(
|
||||
"Failed to start mysqlrouter: {}"
|
||||
.format(e.output.decode("UTF-8")), "ERROR")
|
||||
return
|
||||
reactive.flags.set_flag(MYSQL_ROUTER_STARTED)
|
||||
|
||||
def stop_mysqlrouter(self):
|
||||
cmd = ["{}/stop.sh".format(self.mysqlrouter_dir)]
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, bufsize=1,
|
||||
universal_newlines=True)
|
||||
proc.wait()
|
||||
ch_core.hookenv.log("MySQL router stopped", "DEBUG")
|
||||
except subprocess.CalledProcessError as e:
|
||||
ch_core.hookenv.log(
|
||||
"Failed to start mysqlrouter: {}"
|
||||
.format(e.output.decode("UTF-8")), "ERROR")
|
||||
return
|
||||
reactive.flags.clear_flag(MYSQL_ROUTER_STARTED)
|
||||
|
||||
def restart_mysqlrouter(self):
|
||||
self.stop_mysqlrouter()
|
||||
self.start_mysqlrouter()
|
||||
|
||||
def proxy_db_and_user_requests(
|
||||
self, receiving_interface, sending_interface):
|
||||
|
||||
# We can use receiving_interface.all_joined_units.received
|
||||
# as this is a subordiante and there is only one unit related.
|
||||
db_data = mysql.get_db_data(
|
||||
dict(receiving_interface.all_joined_units.received),
|
||||
unprefixed=self._unprefixed)
|
||||
|
||||
for prefix in db_data:
|
||||
sending_interface.configure_proxy_db(
|
||||
db_data[prefix].get("database"),
|
||||
db_data[prefix].get("username"),
|
||||
db_data[prefix].get("hostname"),
|
||||
prefix=prefix)
|
||||
|
||||
def proxy_db_and_user_responses(
|
||||
self, receiving_interface, sending_interface):
|
||||
|
||||
# This is a suborndinate relationship there is only ever one
|
||||
unit = sending_interface.all_joined_units[0]
|
||||
|
||||
for prefix in receiving_interface.get_prefixes():
|
||||
|
||||
if prefix in self.db_prefix:
|
||||
# Do not send the mysqlrouter credentials to the client
|
||||
continue
|
||||
|
||||
_password = json.loads(
|
||||
receiving_interface.password(prefix=prefix))
|
||||
if ch_core.hookenv.local_unit() in (json.loads(
|
||||
receiving_interface.allowed_units(prefix=prefix))):
|
||||
_allowed_hosts = unit.unit_name
|
||||
else:
|
||||
_allowed_hosts = None
|
||||
if prefix in self._unprefixed:
|
||||
prefix = None
|
||||
|
||||
sending_interface.set_db_connection_info(
|
||||
unit.relation.relation_id,
|
||||
self.shared_db_address,
|
||||
_password,
|
||||
_allowed_hosts,
|
||||
prefix=prefix)
|
|
@ -0,0 +1,20 @@
|
|||
name: mysql-router
|
||||
summary: MySQL Router
|
||||
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
|
||||
description: |
|
||||
MySQL Router proxying communication between application clients and MySQL InnoDB Clusters.
|
||||
tags:
|
||||
- databases
|
||||
subordinate: true
|
||||
series:
|
||||
- eoan
|
||||
provides:
|
||||
shared-db:
|
||||
interface: mysql-shared
|
||||
scope: container
|
||||
requires:
|
||||
juju-info:
|
||||
interface: juju-info
|
||||
scope: container
|
||||
db-router:
|
||||
interface: mysql-router
|
|
@ -0,0 +1,64 @@
|
|||
import charms.reactive as reactive
|
||||
|
||||
import charms_openstack.bus
|
||||
import charms_openstack.charm as charm
|
||||
|
||||
import charm.mysql_router as mysql_router # noqa
|
||||
|
||||
charms_openstack.bus.discover()
|
||||
|
||||
|
||||
charm.use_defaults(
|
||||
'charm.installed',
|
||||
'config.changed',
|
||||
'update-status',
|
||||
'upgrade-charm')
|
||||
|
||||
|
||||
@reactive.when('charm.installed')
|
||||
@reactive.when('db-router.connected')
|
||||
def db_router_request(db_router):
|
||||
with charm.provide_charm_instance() as instance:
|
||||
db_router.set_prefix(instance.db_prefix)
|
||||
db_router.configure_db_router(
|
||||
instance.db_router_user,
|
||||
instance.db_router_address,
|
||||
prefix=instance.db_prefix)
|
||||
instance.assess_status()
|
||||
|
||||
|
||||
@reactive.when('charm.installed')
|
||||
@reactive.when(mysql_router.DB_ROUTER_AVAILABLE)
|
||||
@reactive.when_not(mysql_router.MYSQL_ROUTER_BOOTSTRAPPED)
|
||||
def bootstrap_mysqlrouter(db_router):
|
||||
with charm.provide_charm_instance() as instance:
|
||||
instance.bootstrap_mysqlrouter()
|
||||
instance.assess_status()
|
||||
|
||||
|
||||
@reactive.when('charm.installed')
|
||||
@reactive.when(mysql_router.DB_ROUTER_AVAILABLE)
|
||||
@reactive.when(mysql_router.MYSQL_ROUTER_BOOTSTRAPPED)
|
||||
@reactive.when_not(mysql_router.MYSQL_ROUTER_STARTED)
|
||||
def start_mysqlrouter(db_router):
|
||||
with charm.provide_charm_instance() as instance:
|
||||
instance.start_mysqlrouter()
|
||||
instance.assess_status()
|
||||
|
||||
|
||||
@reactive.when(mysql_router.MYSQL_ROUTER_STARTED)
|
||||
@reactive.when(mysql_router.DB_ROUTER_AVAILABLE)
|
||||
@reactive.when('shared-db.available')
|
||||
def proxy_shared_db_requests(shared_db, db_router):
|
||||
with charm.provide_charm_instance() as instance:
|
||||
instance.proxy_db_and_user_requests(shared_db, db_router)
|
||||
instance.assess_status()
|
||||
|
||||
|
||||
@reactive.when(mysql_router.MYSQL_ROUTER_STARTED)
|
||||
@reactive.when(mysql_router.DB_ROUTER_PROXY_AVAILABLE)
|
||||
@reactive.when('shared-db.available')
|
||||
def proxy_shared_db_responses(shared_db, db_router):
|
||||
with charm.provide_charm_instance() as instance:
|
||||
instance.proxy_db_and_user_responses(db_router, shared_db)
|
||||
instance.assess_status()
|
|
@ -0,0 +1,3 @@
|
|||
# zaza
|
||||
git+https://github.com/openstack-charmers/zaza.git#egg=zaza
|
||||
git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack
|
|
@ -0,0 +1,17 @@
|
|||
series: eoan
|
||||
relations:
|
||||
- ["keystone:shared-db", "mysql-router:shared-db"]
|
||||
- ["mysql-router:db-router", "mysql-innodb-cluster:db-router"]
|
||||
applications:
|
||||
mysql-router:
|
||||
charm: ../../../mysql-router
|
||||
mysql-innodb-cluster:
|
||||
series: eoan
|
||||
charm: cs:~thedac/mysql-innodb-cluster
|
||||
num_units: 3
|
||||
options:
|
||||
source: distro-proposed
|
||||
keystone:
|
||||
series: eoan
|
||||
charm: cs:~openstack-charmers-next/keystone
|
||||
num_units: 1
|
|
@ -0,0 +1,11 @@
|
|||
charm_name: mysql-router
|
||||
configure:
|
||||
- zaza.openstack.charm_tests.keystone.setup.add_demo_user
|
||||
tests:
|
||||
# Validates DB connectivity
|
||||
- zaza.openstack.charm_tests.keystone.tests.AuthenticationAuthorizationTest
|
||||
dev_bundles:
|
||||
gate_bundles:
|
||||
- eoan
|
||||
smoke_bundles:
|
||||
- eoan
|
|
@ -0,0 +1,35 @@
|
|||
[tox]
|
||||
envlist = pep8
|
||||
skipsdist = True
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
PYTHONHASHSEED=0
|
||||
whitelist_externals = juju
|
||||
passenv = HOME TERM CS_API_* OS_* AMULET_*
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
install_command =
|
||||
pip install {opts} {packages}
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python3
|
||||
deps=charm-tools
|
||||
commands = charm-proof
|
||||
|
||||
[testenv:func-noop]
|
||||
basepython = python3
|
||||
commands =
|
||||
true
|
||||
|
||||
[testenv:func]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model
|
||||
|
||||
[testenv:func-smoke]
|
||||
basepython = python3
|
||||
commands =
|
||||
functest-run-suite --keep-model --smoke
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
|
@ -0,0 +1,3 @@
|
|||
jinja2
|
||||
psutil
|
||||
mysqlclient
|
|
@ -0,0 +1,13 @@
|
|||
# This file is managed centrally. If you find the need to modify this as a
|
||||
# one-off, please don't. Intead, consult #openstack-charms and ask about
|
||||
# requirements management in charms via bot-control. Thank you.
|
||||
#
|
||||
# Lint and unit test requirements
|
||||
flake8>=2.2.4,<=2.4.1
|
||||
stestr>=2.2.0
|
||||
requests>=2.18.4
|
||||
charms.reactive
|
||||
mock>=1.2
|
||||
nose>=1.3.7
|
||||
coverage>=3.6
|
||||
git+https://github.com/openstack/charms.openstack.git#egg=charms.openstack
|
|
@ -0,0 +1,80 @@
|
|||
# Source charm: ./tox.ini
|
||||
# This file is managed centrally by release-tools and should not be modified
|
||||
# within individual charm repos.
|
||||
[tox]
|
||||
skipsdist = True
|
||||
envlist = pep8,py3
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUAL_ENV={envdir}
|
||||
PYTHONHASHSEED=0
|
||||
TERM=linux
|
||||
CHARM_LAYER_PATH={toxinidir}/layers
|
||||
CHARM_INTERFACES_DIR={toxinidir}/interfaces
|
||||
JUJU_REPOSITORY={toxinidir}/build
|
||||
passenv = http_proxy https_proxy OS_*
|
||||
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:py3]
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = stestr run {posargs}
|
||||
|
||||
[testenv:py35]
|
||||
basepython = python3.5
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = stestr run {posargs}
|
||||
|
||||
[testenv:py36]
|
||||
basepython = python3.6
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = stestr run {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/test-requirements.txt
|
||||
commands = flake8 {posargs} src unit_tests
|
||||
|
||||
[testenv:cover]
|
||||
# Technique based heavily upon
|
||||
# https://github.com/openstack/nova/blob/master/tox.ini
|
||||
basepython = python3
|
||||
deps = -r{toxinidir}/requirements.txt
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
setenv =
|
||||
{[testenv]setenv}
|
||||
PYTHON=coverage run
|
||||
commands =
|
||||
coverage erase
|
||||
stestr run {posargs}
|
||||
coverage combine
|
||||
coverage html -d cover
|
||||
coverage xml -o cover/coverage.xml
|
||||
coverage report
|
||||
|
||||
[coverage:run]
|
||||
branch = True
|
||||
concurrency = multiprocessing
|
||||
parallel = True
|
||||
source =
|
||||
.
|
||||
omit =
|
||||
.tox/*
|
||||
*/charmhelpers/*
|
||||
unit_tests/*
|
||||
|
||||
[testenv:venv]
|
||||
basepython = python3
|
||||
commands = {posargs}
|
||||
|
||||
[flake8]
|
||||
# E402 ignore necessary for path append before sys module import in actions
|
||||
ignore = E402
|
|
@ -0,0 +1,32 @@
|
|||
# Copyright 2019 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 os
|
||||
import sys
|
||||
|
||||
# Mock out charmhelpers so that we can test without it.
|
||||
import charms_openstack.test_mocks # noqa
|
||||
charms_openstack.test_mocks.mock_charmhelpers()
|
||||
|
||||
_path = os.path.dirname(os.path.realpath(__file__))
|
||||
_src = os.path.abspath(os.path.join(_path, 'src'))
|
||||
_lib = os.path.abspath(os.path.join(_path, 'src/lib'))
|
||||
|
||||
|
||||
def _add_path(path):
|
||||
if path not in sys.path:
|
||||
sys.path.insert(1, path)
|
||||
|
||||
_add_path(_src)
|
||||
_add_path(_lib)
|
Loading…
Reference in New Issue