summaryrefslogtreecommitdiff
diff options
authorPeter Sabaini <peter.sabaini@canonical.com>2020-04-15 22:24:49 +0200
committerPeter Sabaini <peter.sabaini@canonical.com>2020-04-15 22:24:49 +0200
commitb8435c56af49deead8f87151b68991e498051f39 (patch)
treeaf81ce376031ba226cbe8276ef9c3932a890ae2e
parent67a8a1bf09526bccf7bff692b2dd418c1a27fe65 (diff)
Functional testing update
- Excise amulet tests, add zaza based functional testing - Setup tox for functests - Remove some python2-isms - Increase mongoclient timeout to make install more robust under load
-rw-r--r--.gitignore3
-rw-r--r--Makefile16
-rw-r--r--actions/backup.py8
-rwxr-xr-xhooks/hooks.py36
-rwxr-xr-xtests/00_setup.sh9
-rw-r--r--tests/base_deploy.py90
-rw-r--r--tests/bundles/bionic-shard.yaml27
-rw-r--r--tests/bundles/bionic.yaml9
-rw-r--r--tests/bundles/xenial.yaml9
-rwxr-xr-xtests/deploy_replicaset-trusty6
-rwxr-xr-xtests/deploy_replicaset-xenial6
-rw-r--r--tests/deploy_replicaset.py150
-rwxr-xr-xtests/deploy_shard-trusty6
-rwxr-xr-xtests/deploy_shard-xenial6
-rw-r--r--tests/deploy_shard.py80
-rwxr-xr-xtests/deploy_single-trusty8
-rwxr-xr-xtests/deploy_single-xenial8
-rw-r--r--tests/deploy_single.py23
-rwxr-xr-xtests/deploy_with_ceilometer-trusty6
-rwxr-xr-xtests/deploy_with_ceilometer-xenial7
-rw-r--r--tests/deploy_with_ceilometer.py36
-rwxr-xr-xtests/deploy_with_storage-trusty6
-rwxr-xr-xtests/deploy_with_storage-xenial8
-rw-r--r--tests/deploy_with_storage.py74
-rw-r--r--tests/tests.yaml17
-rw-r--r--tests/tests_mongodb.py142
-rw-r--r--tox.ini34
-rw-r--r--unit_tests/test_hooks.py8
28 files changed, 276 insertions, 562 deletions
diff --git a/.gitignore b/.gitignore
index a936365..e6ff298 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,8 +3,9 @@
.pydevproject
.coverage
.settings
+.idea/
*.pyc
-.venv/
+.tox/
bin/*
scripts/charm-helpers-sync.py
exec.d/*
diff --git a/Makefile b/Makefile
index c23418b..68db95f 100644
--- a/Makefile
+++ b/Makefile
@@ -20,6 +20,7 @@ clean:
rm -f .coverage
find . -name '*.pyc' -delete
rm -rf .venv
+ rm -rf .tox
(which dh_clean && dh_clean) || true
.venv:
@@ -31,14 +32,14 @@ lint: .venv
.venv/bin/flake8 --exclude hooks/charmhelpers actions $(ACTIONS) hooks tests unit_tests
.venv/bin/charm-proof
-test: .venv
+unit:
@echo Starting unit tests...
- .venv/bin/nosetests -s --nologcapture --with-coverage $(EXTRA) unit_tests/
- .venv/bin/nosetests -s --nologcapture --with-coverage $(EXTRA) actions/
+ @tox -e unit
-functional_test:
- @echo Starting amulet tests...
- @juju test -v -p AMULET_HTTP_PROXY --timeout 900
+functional:
+ @echo Starting functional tests...
+ rm -rf .venv
+ @tox -e functional
sync:
@mkdir -p bin
@@ -48,3 +49,6 @@ sync:
publish: lint unit_test
bzr push lp:charms/mongodb
bzr push lp:charms/trusty/mongodb
+
+# The targets below don't depend on a file
+.PHONY: lint test unittest functional publish sync
diff --git a/actions/backup.py b/actions/backup.py
index ac7c4d9..cac42cd 100644
--- a/actions/backup.py
+++ b/actions/backup.py
@@ -35,9 +35,9 @@ def restore():
def backup_command(cmd, args, dir):
try:
mkdir(dir)
- except OSError, e:
+ except OSError as e:
pass # Ignoring, the directory already exists
- except Exception, e:
+ except Exception as e:
action_set({"directory creation exception": e})
action_fail(str(e))
return
@@ -48,10 +48,10 @@ def backup_command(cmd, args, dir):
try:
output = execute(command, dir)
action_set({"output": output})
- except subprocess.CalledProcessError, e:
+ except subprocess.CalledProcessError as e:
action_set({"error_code": e.returncode,
"exception": e, "output": e.output})
action_fail(str(e))
- except Exception, e:
+ except Exception as e:
action_set({"exception": e})
action_fail(str(e))
diff --git a/hooks/hooks.py b/hooks/hooks.py
index e2bc2b5..a4a3ee5 100755
--- a/hooks/hooks.py
+++ b/hooks/hooks.py
@@ -150,7 +150,7 @@ def port_check(host=None, port=None, protocol='TCP'):
s.shutdown(socket.SHUT_RDWR)
juju_log("port_check: %s:%s/%s is open" % (host, port, protocol))
return(True)
- except Exception, e:
+ except Exception as e:
juju_log("port_check: Unable to connect to %s:%s/%s." %
(host, port, protocol))
juju_log("port_check: Exception: %s" % str(e))
@@ -195,7 +195,7 @@ def update_file(filename=None, new_data=None, old_data=None):
with open(filename, 'w') as f:
f.write(new_data)
retVal = True
- except Exception, e:
+ except Exception as e:
juju_log(str(e))
retVal = False
finally:
@@ -211,7 +211,7 @@ def process_check(pid=None):
else:
juju_log("process_check: pid not defined.")
retVal = (None, None)
- except Exception, e:
+ except Exception as e:
juju_log("process_check exception: %s" % str(e))
retVal = (None, None)
finally:
@@ -490,7 +490,7 @@ def mongo_client_smart(host='localhost', command=None):
'--eval', 'printjson(%s)' % command]
juju_log("mongo_client_smart executing: %s" % str(cmd_line), level=DEBUG)
- for i in xrange(MONGO_CLIENT_RETRIES):
+ for i in range(MONGO_CLIENT_RETRIES):
try:
cmd_output = subprocess.check_output(cmd_line)
juju_log('mongo_client_smart executed, output: %s' %
@@ -614,7 +614,7 @@ def enable_replset(replicaset_name=None):
juju_log('enable_replset will return: %s' % str(retVal), level=DEBUG)
- except Exception, e:
+ except Exception as e:
juju_log(str(e), level=WARNING)
retVal = False
finally:
@@ -632,7 +632,7 @@ def remove_replset_from_upstart():
mongodb_init_config = re.sub(r' --replSet .\w+', '',
mongodb_init_config)
retVal = update_file(default_mongodb_init_config, mongodb_init_config)
- except Exception, e:
+ except Exception as e:
juju_log(str(e))
retVal = False
finally:
@@ -643,7 +643,7 @@ def step_down_replset_primary():
"""Steps down the primary
"""
retVal = mongo_client('localhost', 'rs.stepDown()')
- for i in xrange(MONGO_CLIENT_RETRIES):
+ for i in range(MONGO_CLIENT_RETRIES):
if not am_i_primary():
juju_log("step_down_replset_primary returns: %s" % retVal,
level=DEBUG)
@@ -665,7 +665,7 @@ def remove_rest_from_upstart():
mongodb_init_config = regex_sub([(' --rest ', ' ')],
mongodb_init_config)
retVal = update_file(default_mongodb_init_config, mongodb_init_config)
- except Exception, e:
+ except Exception as e:
juju_log(str(e))
retVal = False
finally:
@@ -743,7 +743,7 @@ def disable_configsvr(port=None):
os.kill(int(pid), signal.SIGTERM)
os.unlink('/var/run/mongodb/configsvr.pid')
retVal = True
- except Exception, e:
+ except Exception as e:
juju_log('no config server running ...')
juju_log("Exception: %s" % str(e))
retVal = False
@@ -835,7 +835,7 @@ def disable_mongos(port=None):
os.kill(int(pid), signal.SIGTERM)
os.unlink('/var/run/mongodb/mongos.pid')
retVal = True
- except Exception, e:
+ except Exception as e:
juju_log('no mongo router running ...')
juju_log("Exception: %s" % str(e))
retVal = False
@@ -948,7 +948,7 @@ def backup_cronjob(disable=False):
with open(script_filename, 'w') as output:
output.writelines(rendered)
- chmod(script_filename, 0755)
+ chmod(script_filename, 0o755)
juju_log('Installing cron.d/mongodb')
@@ -1085,7 +1085,7 @@ def config_changed():
# update config-server information and port
try:
(configsvr_pid, configsvr_cmd_line) = configsvr_status()
- except Exception, e:
+ except Exception as e:
configsvr_pid = None
configsvr_cmd_line = None
juju_log("config_changed: configsvr_status failed.")
@@ -1101,7 +1101,7 @@ def config_changed():
# update mongos information and port
try:
(mongos_pid, mongos_cmd_line) = mongos_status()
- except Exception, e:
+ except Exception as e:
mongos_pid = None
mongos_cmd_line = None
juju_log("config_changed: mongos_status failed.")
@@ -1137,7 +1137,7 @@ def stop_hook():
retVal = service('stop', 'mongodb')
os.remove('/var/lib/mongodb/mongod.lock')
# FIXME Need to check if this is still needed
- except Exception, e:
+ except Exception as e:
juju_log(str(e))
retVal = False
finally:
@@ -1227,7 +1227,7 @@ def rs_add(host):
juju_log("Executing: %s" % cmd_line, level=DEBUG)
run(cmd_line)
- for i in xrange(MONGO_CLIENT_RETRIES):
+ for i in range(MONGO_CLIENT_RETRIES):
c = MongoClient('localhost')
subprocess.check_output(cmd_line)
r = run_admin_command(c, 'replSetGetStatus')
@@ -1243,7 +1243,7 @@ def rs_add(host):
def am_i_primary():
c = MongoClient('localhost')
- for i in xrange(10):
+ for i in range(10):
try:
r = run_admin_command(c, 'replSetGetStatus')
pretty_r = pprint.pformat(r)
@@ -1310,7 +1310,7 @@ def get_mongod_version():
Mainly used for application_set_version in config-changed hook
"""
- c = MongoClient('localhost')
+ c = MongoClient('localhost', serverSelectionTimeoutMS=60000)
return c.server_info()['version']
@@ -1612,7 +1612,7 @@ def run(command, exit_on_error=True):
juju_log(command)
return subprocess.check_output(
command, stderr=subprocess.STDOUT, shell=True)
- except subprocess.CalledProcessError, e:
+ except subprocess.CalledProcessError as e:
juju_log("status=%d, output=%s" % (e.returncode, e.output))
if exit_on_error:
sys.exit(e.returncode)
diff --git a/tests/00_setup.sh b/tests/00_setup.sh
deleted file mode 100755
index 4f58709..0000000
--- a/tests/00_setup.sh
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/bash
-
-set -e
-
-sudo apt-get install python-setuptools -y
-sudo add-apt-repository ppa:juju/stable -y
-
-sudo apt-get update
-sudo apt-get install amulet python3 python3-requests python3-pymongo juju-core charm-tools python-mock python-pymongo -y
diff --git a/tests/base_deploy.py b/tests/base_deploy.py
deleted file mode 100644
index b136cca..0000000
--- a/tests/base_deploy.py
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env python3
-
-import amulet
-import requests
-import sys
-import time
-import traceback
-from pymongo import MongoClient
-
-
-class BasicMongo(object):
- def __init__(self, units, series, deploy_timeout):
- self.units = units
- self.series = series
- self.deploy_timeout = deploy_timeout
- self.d = amulet.Deployment(series=self.series)
- self.addy = None
-
- def deploy(self):
- try:
- self.d.setup(self.deploy_timeout)
- self.d.sentry.wait(self.deploy_timeout)
- except amulet.helpers.TimeoutError:
- message = 'The environment did not setup in %d seconds.',
- self.deploy_timeout
- amulet.raise_status(amulet.SKIP, msg=message)
-
- self.sentry_dict = {svc: self.d.sentry[svc]
- for svc in list(self.d.sentry.unit)}
-
- def validate_status_interface(self):
- addy = self.addy
- fmt = "http://{}:28017"
- if ":" in addy:
- fmt = "http://[{}]:28017"
-
- time_between = 10
- tries = self.deploy_timeout / time_between
-
- try:
- r = requests.get(fmt.format(addy), verify=False)
- r.raise_for_status()
- except requests.exception.ConnectionError as ex:
- sys.stderr.write(
- 'Connection error, sleep and retry... to {}: {}\n'.
- format(addy, ex))
- tb_lines = traceback.format_exception(ex.__class__,
- ex, ex.__traceback__)
- tb_text = ''.join(tb_lines)
- sys.stderr.write(tb_text)
- tries = tries - 1
- if tries < 0:
- sys.stderr.write('retry limit caught, failing...\n')
- time.sleep(time_between)
-
- def validate_world_connectivity(self):
- addy = self.addy
- # ipv6 proper formating
- if ":" in addy:
- addy = "[{}]".format(addy)
-
- client = MongoClient(addy)
- db = client['test']
-
- # Can we successfully insert?
- insert_id = db.amulet.insert({'assert': True})
- if insert_id is None:
- amulet.raise_status(amulet.FAIL, msg="Failed to insert test data")
-
- # Can we delete from a shard using the Mongos hub?
- result = db.amulet.remove(insert_id)
- if 'err' in result and result['err'] is not None:
- amulet.raise_status(amulet.FAIL, msg="Failed to remove test data")
-
- def validate_running_services(self):
- for service in self.sentry_dict:
- grep_command = 'grep RELEASE /etc/lsb-release'
- release = self.sentry_dict[service].run(grep_command)
- release = str(release).split('=')[1]
- if release >= '15.10':
- status_string = 'active (running)' # systemd
- else:
- status_string = 'mongodb start/running' # upstart
-
- output = self.sentry_dict[service].run('service mongodb status')
- service_active = str(output).find(status_string)
- if service_active == -1:
- message = "Failed to find running MongoDB on host {}".format(
- service)
- amulet.raise_status(amulet.SKIP, msg=message)
diff --git a/tests/bundles/bionic-shard.yaml b/tests/bundles/bionic-shard.yaml
new file mode 100644
index 0000000..b7ee354
--- /dev/null
+++ b/tests/bundles/bionic-shard.yaml
@@ -0,0 +1,27 @@
+series: bionic
+description: "mongodb-charm test bundle"
+applications:
+ configsvr:
+ charm: "../../."
+ num_units: 1
+ options:
+ replicaset: configsvr
+ mongodb:
+ charm: "../../."
+ num_units: 1
+ options:
+ replicaset: testset
+ shard1:
+ charm: "../../."
+ num_units: 1
+ options:
+ replicaset: shard1
+ shard2:
+ charm: "../../."
+ num_units: 1
+ options:
+ replicaset: shard2
+relations:
+ - [ "configsvr:configsvr", "mongodb:mongos-cfg" ]
+ - [ "mongodb:mongos", "shard1:database" ]
+ - [ "mongodb:mongos", "shard2:database" ]
diff --git a/tests/bundles/bionic.yaml b/tests/bundles/bionic.yaml
new file mode 100644
index 0000000..ebfb98e
--- /dev/null
+++ b/tests/bundles/bionic.yaml
@@ -0,0 +1,9 @@
+series: bionic
+description: "mongodb-charm test bundle"
+applications:
+ mongodb:
+ charm: "../../."
+ num_units: 3
+ options:
+ replicaset: testset
+ backup_directory: /var/backups
diff --git a/tests/bundles/xenial.yaml b/tests/bundles/xenial.yaml
new file mode 100644
index 0000000..9d29342
--- /dev/null
+++ b/tests/bundles/xenial.yaml
@@ -0,0 +1,9 @@
+series: xenial
+description: "mongodb-charm test bundle"
+applications:
+ mongodb:
+ charm: "../../."
+ num_units: 3
+ options:
+ replicaset: testset
+ backup_directory: /var/backups
diff --git a/tests/deploy_replicaset-trusty b/tests/deploy_replicaset-trusty
deleted file mode 100755
index 9ed77a8..0000000
--- a/tests/deploy_replicaset-trusty
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_replicaset
-
-t = deploy_replicaset.Replicaset('trusty')
-t.run() \ No newline at end of file
diff --git a/tests/deploy_replicaset-xenial b/tests/deploy_replicaset-xenial
deleted file mode 100755
index 4f142dd..0000000
--- a/tests/deploy_replicaset-xenial
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_replicaset
-
-t = deploy_replicaset.Replicaset('xenial')
-t.run()
diff --git a/tests/deploy_replicaset.py b/tests/deploy_replicaset.py
deleted file mode 100644
index 6fa0290..0000000
--- a/tests/deploy_replicaset.py
+++ /dev/null
@@ -1,150 +0,0 @@
-#!/usr/bin/env python3
-
-import amulet
-import logging
-import re
-import sys
-import time
-import traceback
-from pymongo import MongoClient
-from pymongo.errors import OperationFailure
-from collections import Counter
-
-from base_deploy import BasicMongo
-
-# max amount of time to wait before testing for replicaset status
-wait_for_replicaset = 600
-logger = logging.getLogger(__name__)
-
-
-class Replicaset(BasicMongo):
- def __init__(self, series):
- super(Replicaset, self).__init__(units=3,
- series=series,
- deploy_timeout=1800)
-
- def _expect_replicaset_counts(self,
- primaries_count,
- secondaries_count,
- time_between=10):
- unit_status = []
- tries = wait_for_replicaset / time_between
-
- for service in self.sentry_dict:
- addy = self.sentry_dict[service].info['public-address']
- if ":" in addy:
- addy = "[{}]".format(addy)
- while True:
- try:
- client = MongoClient(addy)
- r = client.admin.command('replSetGetStatus')
- break
- except OperationFailure as ex:
- sys.stderr.write(
- 'OperationFailure, sleep and retry... to {}: {}\n'.
- format(addy, ex))
- tb_lines = traceback.format_exception(ex.__class__,
- ex, ex.__traceback__)
- tb_text = ''.join(tb_lines)
- sys.stderr.write(tb_text)
- tries = tries - 1
- if tries < 0:
- sys.stderr.write('retry limit caught, failing...\n')
- break
- time.sleep(time_between)
- unit_status.append(r['myState'])
- client.close()
-
- primaries = Counter(unit_status)[1]
- if primaries != primaries_count:
- message = "Expected %d PRIMARY unit(s)! Found: %s %s" % (
- primaries_count,
- primaries,
- unit_status)
- amulet.raise_status(amulet.FAIL, message)
-
- secondrs = Counter(unit_status)[2]
- if secondrs != secondaries_count:
- message = ("Expected %d secondary units! (Found %s) %s" %
- (secondaries_count, secondrs, unit_status))
- amulet.raise_status(amulet.FAIL, message)
-
- def deploy(self):
- self.d.add('mongodb', charm='mongodb', units=self.units)
- self.d.expose('mongodb')
- super(Replicaset, self).deploy()
- self.wait_for_replicaset = 600
-
- def validate_status_interface(self):
- self.addy = self.d.sentry['mongodb'][0].info['public-address']
- super(Replicaset, self).validate_status_interface()
-
- def validate_replicaset_setup(self):
- self.d.sentry.wait(self.deploy_timeout)
- self._expect_replicaset_counts(1, 2)
-
- def validate_replicaset_relation_joined(self):
- self.d.add_unit('mongodb', units=2)
- self.d.sentry.wait(wait_for_replicaset)
- self.sentry_dict = {svc: self.d.sentry[svc]
- for svc in list(self.d.sentry.unit)}
- self._expect_replicaset_counts(1, 4)
-
- def validate_world_connectivity(self):
- # figuring out which unit is primary
- primary = False
- while not primary:
- for unit in self.sentry_dict:
- unit_address = self.sentry_dict[unit].info['public-address']
- if ":" in unit_address:
- unit_address = "[{}]".format(unit_address)
- c = MongoClient(unit_address)
- r = c.admin.command('replSetGetStatus')
- if r['myState'] == 1:
- # reusing address without possible brackets []
- primary = self.sentry_dict[unit].info['public-address']
- break
- time.sleep(.1)
-
- self.addy = primary
- super(Replicaset, self).validate_world_connectivity()
-
- def validate_running_services(self):
- super(Replicaset, self).validate_running_services()
-
- def validate_workload_status(self):
- primaries = 0
- secondaries = 0
- regex = re.compile('^Unit is ready as (PRIMARY|SECONDARY)$')
- self.d.sentry.wait_for_messages({'mongodb': regex})
-
- # count how many primaries and secondaries were reported in the
- # workload status
- for unit_name, unit in self.d.sentry.get_status()['mongodb'].items():
- workload_msg = unit['workload-status']['message']
- matched = re.match(regex, workload_msg)
-
- if not matched:
- msg = "'{}' does not match '{}'".format(workload_msg, regex)
- amulet.raise_status(amulet.FAIL, msg=msg)
- elif matched.group(1) == 'PRIMARY':
- primaries += 1
- elif matched.group(1) == 'SECONDARY':
- secondaries += 1
- else:
- amulet.raise_status(amulet.FAIL,
- msg='Unknown state: %s' % matched.group(1))
-
- logger.debug('Secondary units found: %d' % secondaries)
- if primaries > 1:
- msg = "Found %d primaries, expected 1" % primaries
- amulet.raise_status(amulet.FAIL, msg=msg)
-
- def run(self):
- self.deploy()
- self.validate_status_interface()
- self.validate_running_services()
- self.validate_replicaset_setup()
- self.validate_replicaset_relation_joined()
- self.validate_world_connectivity()
- self.validate_workload_status()
diff --git a/tests/deploy_shard-trusty b/tests/deploy_shard-trusty
deleted file mode 100755
index 200110f..0000000
--- a/tests/deploy_shard-trusty
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_shard
-
-t = deploy_shard.ShardNode('trusty')
-t.run() \ No newline at end of file
diff --git a/tests/deploy_shard-xenial b/tests/deploy_shard-xenial
deleted file mode 100755
index cb00363..0000000
--- a/tests/deploy_shard-xenial
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_shard
-
-t = deploy_shard.ShardNode('xenial')
-t.run()
diff --git a/tests/deploy_shard.py b/tests/deploy_shard.py
deleted file mode 100644
index d384ef4..0000000
--- a/tests/deploy_shard.py
+++ /dev/null
@@ -1,80 +0,0 @@
-#!/usr/bin/env python3
-
-import amulet
-
-from base_deploy import BasicMongo
-
-
-class ShardNode(BasicMongo):
- def __init__(self, series):
- super(ShardNode, self).__init__(units=1,
- series=series,
- deploy_timeout=900)
-
- def deploy(self):
- self.d.add('configsvr', charm='mongodb', units=self.units)
- self.d.add('mongos', charm='mongodb', units=self.units)
- self.d.add('shard1', charm='mongodb', units=self.units)
- self.d.add('shard2', charm='mongodb', units=self.units)
-
- # Setup the config svr
- self.d.configure('configsvr', {'replicaset': 'configsvr'})
-
- # define each shardset
- self.d.configure('shard1', {'replicaset': 'shard1'})
- self.d.configure('shard2', {'replicaset': 'shard2'})
-
- self.d.configure('mongos', {})
-
- # Connect the config servers to mongo shell
- self.d.relate('configsvr:configsvr', 'mongos:mongos-cfg')
-
- # connect each shard to the mongo shell
- self.d.relate('mongos:mongos', 'shard1:database')
- self.d.relate('mongos:mongos', 'shard2:database')
- self.d.expose('configsvr')
- self.d.expose('mongos')
- super(ShardNode, self).deploy()
-
- self.sentry_dict = {
- 'config-sentry': self.d.sentry['configsvr'][0],
- 'mongos-sentry': self.d.sentry['mongos'][0],
- 'shard1-sentry': self.d.sentry['shard1'][0],
- 'shard2-sentry': self.d.sentry['shard2'][0]
- }
-
- def validate_world_connectivity(self):
- self.addy = self.d.sentry['mongos'][0].info['public-address']
- super(ShardNode, self).validate_world_connectivity()
-
- def validate_running_services(self):
- super(ShardNode, self).validate_running_services()
-
- def validate_status_interface(self):
- self.addy = self.sentry_dict['config-sentry'].info['public-address']
- super(ShardNode, self).validate_status_interface()
-
- def validate_manual_connection(self):
- fmt = "mongo {}"
- addy = self.d.sentry['mongos'][0].info['public-address']
- if ":" in addy:
- fmt = "mongo --ipv6 {}:27017"
- jujuruncmd = fmt.format(addy)
- output, code = self.d.sentry['shard1'][0].run(jujuruncmd)
- if code != 0:
- msg = ("Manual Connection failed, unit shard1:{} code:{} cmd:{}"
- .format(output, code, jujuruncmd))
- amulet.raise_status(amulet.SKIP, msg=msg)
-
- output, code = self.d.sentry['shard2'][0].run(jujuruncmd)
- if code != 0:
- msg = ("Manual Connection failed, unit shard2:{} code:{} cmd:{}"
- .format(output, code, jujuruncmd))
- amulet.raise_status(amulet.SKIP, msg=msg)
-
- def run(self):
- self.deploy()
- self.validate_world_connectivity()
- self.validate_status_interface()
- self.validate_running_services()
- self.validate_manual_connection()
diff --git a/tests/deploy_single-trusty b/tests/deploy_single-trusty
deleted file mode 100755
index 59a2231..0000000
--- a/tests/deploy_single-trusty
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_single
-
-t = deploy_single.SingleNode('trusty')
-t.run()
-
-
diff --git a/tests/deploy_single-xenial b/tests/deploy_single-xenial
deleted file mode 100755
index 3f718d8..0000000
--- a/tests/deploy_single-xenial
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_single
-
-t = deploy_single.SingleNode('xenial')
-t.run()
-
-
diff --git a/tests/deploy_single.py b/tests/deploy_single.py
deleted file mode 100644
index c3181b1..0000000
--- a/tests/deploy_single.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env python3
-
-from base_deploy import BasicMongo
-
-
-class SingleNode(BasicMongo):
- def __init__(self, series):
- super(SingleNode, self).__init__(units=1,
- series=series,
- deploy_timeout=900)
-
- def deploy(self):
- self.d.add('mongodb', charm='mongodb', units=self.units)
- self.d.expose('mongodb')
- super(SingleNode, self).deploy()
-
- def validate_world_connectivity(self):
- self.addy = self.d.sentry['mongodb'][0].info['public-address']
- super(SingleNode, self).validate_world_connectivity()
-
- def run(self):
- self.deploy()
- self.validate_world_connectivity()
diff --git a/tests/deploy_with_ceilometer-trusty b/tests/deploy_with_ceilometer-trusty
deleted file mode 100755
index b73d870..0000000
--- a/tests/deploy_with_ceilometer-trusty
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_with_ceilometer
-
-t = deploy_with_ceilometer.TestCeilometer('trusty')
-t.run()
diff --git a/tests/deploy_with_ceilometer-xenial b/tests/deploy_with_ceilometer-xenial
deleted file mode 100755
index 4678457..0000000
--- a/tests/deploy_with_ceilometer-xenial
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_with_ceilometer
-
-#Not running this because of: https://launchpad.net/bugs/1656651
-#t = deploy_with_ceilometer.TestCeilometer('xenial')
-#t.run()
diff --git a/tests/deploy_with_ceilometer.py b/tests/deploy_with_ceilometer.py
deleted file mode 100644
index 133eee3..0000000
--- a/tests/deploy_with_ceilometer.py
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env python3
-
-import amulet
-
-from base_deploy import BasicMongo
-
-
-class TestCeilometer(BasicMongo):
- def __init__(self, series):
- super(TestCeilometer, self).__init__(units=1, series=series,
- deploy_timeout=900)
-
- def deploy(self):
- self.d.add('mongodb', charm='mongodb', units=self.units)
- self.d.add('ceilometer', 'cs:{}/ceilometer'.format(self.series))
- self.d.relate('mongodb:database', 'ceilometer:shared-db')
- self.d.expose('mongodb')
- super(TestCeilometer, self).deploy()
-
- def validate_world_connectivity(self):
- self.addy = self.d.sentry['mongodb'][0].info['public-address']
- super(TestCeilometer, self).validate_world_connectivity()
-
- def validate_mongo_relation(self):
- unit = self.d.sentry['ceilometer'][0]
- mongo = self.d.sentry['mongodb'][0].info['public-address']
- mongo_reladdr = self.d.sentry['mongodb'][0].relation(
- 'database', 'ceilometer:shared-db')
- cont = unit.file_contents('/etc/ceilometer/ceilometer.conf')
- if (mongo not in cont and mongo_reladdr.get(
- 'hostname', 'I SURE HOPE NOT') not in cont):
- amulet.raise_status(amulet.FAIL, "Unable to verify ceilometer cfg")
-
- def run(self):
- self.deploy()
- self.validate_world_connectivity()
diff --git a/tests/deploy_with_storage-trusty b/tests/deploy_with_storage-trusty
deleted file mode 100755
index f7e8314..0000000
--- a/tests/deploy_with_storage-trusty
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_with_storage
-
-t = deploy_with_storage.WithStorage('trusty')
-t.run()
diff --git a/tests/deploy_with_storage-xenial b/tests/deploy_with_storage-xenial
deleted file mode 100755
index 6b0d660..0000000
--- a/tests/deploy_with_storage-xenial
+++ /dev/null
@@ -1,8 +0,0 @@
-#!/usr/bin/env python3
-
-import deploy_with_storage
-
-# We are not testing this against xenial yet, because
-# cs:~chris-gondolin/trusty/storage-5 does not exist for xenial (yet)
-#t = deploy_with_storage.WithStorage('xenial')
-#t.run()
diff --git a/tests/deploy_with_storage.py b/tests/deploy_with_storage.py
deleted file mode 100644
index 92a5fe6..0000000
--- a/tests/deploy_with_storage.py
+++ /dev/null
@@ -1,74 +0,0 @@
-#!/usr/bin/env python3
-
-from base_deploy import BasicMongo
-
-import amulet
-from pymongo import MongoClient
-from collections import Counter
-
-
-class WithStorage(BasicMongo):
- def __init__(self, series):
- super(WithStorage, self).__init__(units=2,
- series=series,
- deploy_timeout=1800)
-
- def deploy(self):
- self.d.add('mongodb',
- charm='mongodb',
- units=self.units,
- constraints={'root-disk': '20480M'})
-
- storage_charm = 'cs:~chris-gondolin/{}/storage-5'.format(self.series)
- self.d.add('storage', charm=storage_charm, series=self.series)
- self.d.configure('storage', {'provider': 'local'})
- super(WithStorage, self).deploy()
- self.d.expose('mongodb')
-
- ordered_units = sorted(self.d.sentry['mongodb'],
- key=lambda u: u.info['unit'])
- self.sentry_dict = {
- 'mongodb0-sentry': ordered_units[0],
- 'mongodb1-sentry': ordered_units[1]
- }
-
- def validate_status(self):
- self.d.sentry.wait_for_status(self.d.juju_env, ['mongodb'])
-
- def validate_replicaset_setup(self):
- self.d.sentry.wait(self.deploy_timeout)
-
- unit_status = []
- for service in self.sentry_dict:
- addy = self.sentry_dict[service].info['public-address']
- if ":" in addy:
- addy = "[{}]".format(addy)
- client = MongoClient(addy)
- r = client.admin.command('replSetGetStatus')
- unit_status.append(r['myState'])
- client.close()
-
- prims = Counter(unit_status)[1]
- if prims != 1:
- message = "Only one PRIMARY unit allowed! Found: %s" % (prims)
- amulet.raise_status(amulet.FAIL, message)
-
- secnds = Counter(unit_status)[2]
- if secnds != 1:
- message = "Only one SECONDARY unit allowed! (Found %s)" % (secnds)
- amulet.raise_status(amulet.FAIL, message)
-
- def run(self):
- self.deploy()
- self.validate_status()
- self.validate_replicaset_setup()
-
- print("Adding storage relation, and sleeping for 2 min.")
- try:
- self.d.relate('mongodb:data', 'storage:data')
- except OSError as e:
- print("Ignoring error: {}", e)
- self.d.sentry.wait(120) # 2 minute
-
- self.validate_status()
- self.validate_replicaset_setup()
diff --git a/tests/tests.yaml b/tests/tests.yaml
new file mode 100644
index 0000000..dcbd01c
--- /dev/null
+++ b/tests/tests.yaml
@@ -0,0 +1,17 @@
+charm_name: mongodb-charm
+tests:
+ - model_alias_xenial:
+ - tests.tests_mongodb.BasicMongodbCharmTest
+ - tests.tests_mongodb.ReplicatedMongodbCharmTest
+ - tests.tests_mongodb.XenialMongodbCharmTest
+ - model_alias_bionic:
+ - tests.tests_mongodb.BasicMongodbCharmTest
+ - tests.tests_mongodb.ReplicatedMongodbCharmTest
+ - model_alias_shard:
+ - tests.tests_mongodb.ShardedMongodbCharmTest
+gate_bundles:
+ - model_alias_xenial: xenial
+ - model_alias_bionic: bionic
+ - model_alias_shard: bionic-shard
+smoke_bundles:
+ - model_alias_bionic: bionic
diff --git a/tests/tests_mongodb.py b/tests/tests_mongodb.py
new file mode 100644
index 0000000..2e763d7
--- /dev/null
+++ b/tests/tests_mongodb.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+import requests
+import unittest
+
+from pymongo import MongoClient
+from requests.adapters import HTTPAdapter
+from requests.packages.urllib3.util.retry import Retry
+from zaza import model
+from zaza.charm_lifecycle import utils as lifecycle_utils
+
+
+MONGO_STARTUP = 0
+MONGO_PRIMARY = 1
+MONGO_SECONDARY = 2
+MONGO_RECOVERING = 3
+MONGO_FATAL = 4
+MONGO_STARTUP2 = 5
+MONGO_UNKNOWN = 6
+MONGO_ARBITER = 7
+MONGO_DOWN = 8
+MONGO_ROLLBACK = 9
+MONGO_REMOVED = 10
+
+
+def requests_retry_session(
+ retries=3, backoff_factor=2, status_forcelist=(500, 502, 504), session=None
+):
+ """Create a http session with retry"""
+ session = session or requests.Session()
+ retry = Retry(
+ total=retries,
+ read=retries,
+ connect=retries,
+ backoff_factor=backoff_factor,
+ status_forcelist=status_forcelist,
+ )
+ adapter = HTTPAdapter(max_retries=retry)
+ session.mount("http://", adapter)
+ session.mount("https://", adapter)
+ return session
+
+
+class MongodbCharmTestBase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.model_name = model.get_juju_model()
+ cls.test_config = lifecycle_utils.get_charm_config()
+ model.block_until_all_units_idle()
+ addr = model.get_lead_unit_ip("mongodb")
+ if ":" in addr: # ipv6 formatting
+ cls.leader_address = "[{}]".format(addr)
+ else:
+ cls.leader_address = addr
+ cls.db_client = MongoClient(cls.leader_address)
+
+ @classmethod
+ def tearDownClass(cls):
+ # shutil.rmtree(cls.tmp)
+ pass
+
+ def cat_unit(self, unit, path):
+ unit_res = model.run_on_unit(unit, "sudo cat {}".format(path))
+ return unit_res["Stdout"]
+
+ def web_admin_interface(self, ipaddr, port=28017):
+ url = "http://{}:{}".format(ipaddr, port)
+ resp = requests_retry_session(retries=10).get(url)
+ return resp
+
+
+class BasicMongodbCharmTest(MongodbCharmTestBase):
+ def test_db_insert(self):
+ """Test if we can insert and remove a value"""
+ test_db = self.db_client.test_db
+ insert_id = test_db.testcoll.insert({"assert": True})
+ self.assertTrue(insert_id is not None, "Failed to insert test data")
+ result = test_db.testcoll.remove(insert_id)
+ self.assertTrue("err" not in result, "Failed to remove test data")
+
+ def test_service_running(self):
+ """Test if we have a mongod running on all units"""
+ for i in (0, 1, 2):
+ running_for = model.get_unit_service_start_time(
+ "mongodb/{}".format(i), "mongod", timeout=20
+ )
+ self.assertGreater(running_for, 0)
+
+
+class XenialMongodbCharmTest(MongodbCharmTestBase):
+ def test_status_interface(self):
+ """Check if we can access the web admin port -- xenial only"""
+ resp = self.web_admin_interface(self.leader_address)
+ resp.raise_for_status()
+
+
+class ReplicatedMongodbCharmTest(MongodbCharmTestBase):
+ def get_set_status(self):
+ unit_status = []
+ for addr in model.get_app_ips("mongodb"):
+ unit_client = MongoClient(addr)
+ unit_status.append(unit_client.admin.command("replSetGetStatus"))
+ return unit_status
+
+ def test_replset_numbers(self):
+ """Test if we have 1 primary and 2 secondary mongodbs"""
+ unit_status = self.get_set_status()
+ primaries = [u for u in unit_status if u["myState"] == MONGO_PRIMARY]
+ secondaries = [u for u in unit_status if u["myState"] == MONGO_SECONDARY]
+ self.assertEqual(len(primaries), 1)
+ self.assertEqual(len(secondaries), 2)
+
+ def test_replset_consistent_members(self):
+ """Test if all units have the same view on membership"""
+ unit_status = self.get_set_status()
+ prim_members = [u for u in unit_status if u["myState"] == MONGO_PRIMARY][0][
+ "members"
+ ]
+ secondary_members = [
+ u["members"] for u in unit_status if u["myState"] == MONGO_SECONDARY
+ ]
+
+ def extract(member_dict):
+ # Extract a subset of membership info as a frozenset. Name is ipaddr:port, health a float and stateStr a str
+ return frozenset(
+ v for k, v in member_dict.items() if k in ["name", "health", "stateStr"]
+ )
+
+ ref = set(
+ map(extract, prim_members)
+ ) # Our reference view on membership comes from the primary
+ secondaries = [set(map(extract, sec)) for sec in secondary_members]
+ for sec in secondaries:
+ self.assertEqual(ref, sec)
+
+
+class ShardedMongodbCharmTest(MongodbCharmTestBase):
+ def test_mongos_running(self):
+ """Test if the mongos service is running"""
+ running_for = model.get_unit_service_start_time(
+ "mongodb/0", "mongos", timeout=20
+ )
+ self.assertGreater(running_for, 0)
diff --git a/tox.ini b/tox.ini
new file mode 100644
index 0000000..013abf0
--- /dev/null
+++ b/tox.ini
@@ -0,0 +1,34 @@
+[tox]
+skipsdist=True
+envlist = unit, functional, lint
+skip_missing_interpreters = True
+
+[testenv]
+setenv =
+ PYTHONPATH = .
+passenv =
+ HOME
+ JUJU_REPOSITORY
+ MODEL_SETTINGS
+
+[testenv:unit]
+basepython = python2
+commands =
+ nosetests -s --nologcapture --with-coverage unit_tests/ actions/
+deps = -r{toxinidir}/test_requirements.txt
+
+[testenv:functional]
+basepython = python3
+commands =
+ functest-run-suite --keep-model
+deps =
+ git+https://github.com/openstack-charmers/zaza.git#egg=zaza
+ pymongo
+
+[testenv:func-smoke]
+basepython = python3
+commands =
+ functest-run-suite --keep-model --smoke
+deps =
+ git+https://github.com/openstack-charmers/zaza.git#egg=zaza
+ pymongo
diff --git a/unit_tests/test_hooks.py b/unit_tests/test_hooks.py
index b44d11f..c6e3c22 100644
--- a/unit_tests/test_hooks.py
+++ b/unit_tests/test_hooks.py
@@ -187,8 +187,8 @@ class MongoHooksTest(CharmTestCase):
@patch('time.sleep')
def test_am_i_primary(self, mock_sleep, mock_mongo_client,
mock_run_admin_cmd):
- mock_run_admin_cmd.side_effect = [{'myState': x} for x in xrange(5)]
- expected_results = [True if x == 1 else False for x in xrange(5)]
+ mock_run_admin_cmd.side_effect = [{'myState': x} for x in range(5)]
+ expected_results = [True if x == 1 else False for x in range(5)]
# Check expected return values each time...
for exp in expected_results:
@@ -203,7 +203,7 @@ class MongoHooksTest(CharmTestCase):
mock_run_admin_cmd):
msg = 'replSetInitiate - should come online shortly'
mock_run_admin_cmd.side_effect = [OperationFailure(msg)
- for x in xrange(10)]
+ for x in range(10)]
try:
hooks.am_i_primary()
@@ -262,7 +262,7 @@ class MongoHooksTest(CharmTestCase):
def test_mongo_client_smart_error_cases(self, mock_ck_output, mock_sleep):
mock_ck_output.side_effect = [CalledProcessError(1, 'cmd',
output='fake-error')
- for x in xrange(11)]
+ for x in range(11)]
rv = hooks.mongo_client_smart(command='fake-cmd')
self.assertFalse(rv)