Migrate charm to charmhub

Summary of changes:

- Add charmcraft.yaml to build the charm using charmcraft-2.2
- Add osci.yaml to run CI jobs on zosci
- Align tox.ini and src/tox.ini with the rest of the charms
- Add testing bundles jammy-zed, jammy-antelope and lunar-antelope. The
  other bundles are dropped
- Clean up requirements.txt removing unneeded dependencies
- Update metadata.yaml to declare jammy and lunar only
- Use generic zuul job templates
- Add src/wheelhouse.txt
- Install python3-apt instead of python-apt
- Drop pip.sh
- Drop unneeded pining from test-requirements.txt

Change-Id: I28df31910c869f813682c3b436e5481dc607772e
This commit is contained in:
Felipe Reyes 2023-05-22 21:32:37 -04:00
parent 7b62f18eb6
commit 631f0034a1
22 changed files with 279 additions and 376 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ build
.venv
cover
.coverage
*.charm

View File

@ -1,4 +1,4 @@
- project:
templates:
- openstack-python3-ussuri-jobs
- openstack-python3-charm-jobs
- openstack-cover-jobs

28
charmcraft.yaml Normal file
View File

@ -0,0 +1,28 @@
type: charm
parts:
charm:
plugin: reactive
build-packages:
- tox
- git
source: src/
build-packages:
- python3-dev
build-snaps:
- charm
build-environment:
- CHARM_INTERFACES_DIR: $CRAFT_PROJECT_DIR/interfaces/
- CHARM_LAYERS_DIR: $CRAFT_PROJECT_DIR/layers/
bases:
- build-on:
- name: ubuntu
channel: "22.04"
architectures: [amd64]
run-on:
- name: ubuntu
channel: "22.04"
architectures: [amd64, arm64, ppc64el, s390x]
- name: ubuntu
channel: "23.04"
architectures: [amd64, arm64, ppc64el, s390x]

1
metadata.yaml Symbolic link
View File

@ -0,0 +1 @@
src/metadata.yaml

9
osci.yaml Normal file
View File

@ -0,0 +1,9 @@
- project:
templates:
- charm-unit-jobs
- charm-functional-jobs
vars:
needs_charm_build: true
charm_build_name: watcher
build_type: charmcraft
charmcraft_channel: "2.2/stable"

18
pip.sh
View File

@ -1,18 +0,0 @@
#!/usr/bin/env bash
#
# This file is managed centrally by release-tools and should not be modified
# within individual charm repos. See the 'global' dir contents for available
# choices of tox.ini for OpenStack Charms:
# https://github.com/openstack-charmers/release-tools
#
# setuptools 58.0 dropped the support for use_2to3=true which is needed to
# install blessings (an indirect dependency of charm-tools).
#
# More details on the beahvior of tox and virtualenv creation can be found at
# https://github.com/tox-dev/tox/issues/448
#
# This script is wrapper to force the use of the pinned versions early in the
# process when the virtualenv was created and upgraded before installing the
# depedencies declared in the target.
pip install 'pip<20.3' 'setuptools<50.0.0'
pip "$@"

13
rename.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}')
echo "renaming ${charm}_*.charm to ${charm}.charm"
echo -n "pwd: "
pwd
ls -al
echo "Removing bad downloaded charm maybe?"
if [[ -e "${charm}.charm" ]];
then
rm "${charm}.charm"
fi
echo "Renaming charm here."
mv ${charm}_*.charm ${charm}.charm

View File

@ -1,23 +1 @@
# This file is managed centrally by release-tools and should not be modified
# within individual charm repos. See the 'global' dir contents for available
# choices of *requirements.txt files for OpenStack Charms:
# https://github.com/openstack-charmers/release-tools
#
# NOTE(lourot): This might look like a duplication of test-requirements.txt but
# some tox targets use only test-requirements.txt whereas charm-build uses only
# requirements.txt
setuptools<50.0.0 # https://github.com/pypa/setuptools/commit/04e3df22df840c6bb244e9b27bc56750c44b7c85
# Build requirements
cffi==1.14.6; python_version < '3.6' # cffi 1.15.0 drops support for py35.
charm-tools==2.8.3
simplejson
# Newer versions use keywords that didn't exist in python 3.5 yet (e.g.
# "ModuleNotFoundError")
# NOTE(lourot): This might look like a duplication of test-requirements.txt but
# some tox targets use only test-requirements.txt whereas charm-build uses only
# requirements.txt
importlib-metadata<3.0.0; python_version < '3.6'
importlib-resources<3.0.0; python_version < '3.6'
charm-tools

View File

@ -91,7 +91,7 @@ class WatcherCharm(openstack_charm.HAOpenStackCharm):
packages = [
'watcher-common', 'watcher-api', 'watcher-decision-engine',
'watcher-applier', 'python3-watcher', 'libapache2-mod-wsgi-py3',
'python-apt', # NOTE: workaround for hacluster subordinate
'python3-apt', # NOTE: workaround for hacluster subordinate
]
python_version = 3

View File

@ -10,9 +10,8 @@ description: |
tags:
- openstack
series:
- bionic
- eoan
- focal
- jammy
- lunar
subordinate: false
requires:
shared-db:

View File

@ -1,36 +0,0 @@
series: bionic
relations:
- - mysql:shared-db
- keystone:shared-db
- - mysql:shared-db
- watcher:shared-db
- - keystone:identity-service
- watcher:identity-service
- - rabbitmq-server:amqp
- watcher:amqp
applications:
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: cloud:bionic-train
mysql:
constraints: mem=3072M
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
watcher:
series: bionic
charm: ../../../watcher
num_units: 1
options:
openstack-origin: cloud:bionic-train
datasources: gnocchi
planner: weight
planner-config: >
{
"weights": "change_node_power_state:9,change_nova_service_state:50,migrate:30,nop:70,resize:20,sleep:40,turn_host_to_acpi_s3_state:10,volume_migrate:60",
"parallelization": "change_node_power_state:2,change_nova_service_state:1,migrate:2,nop:1,resize:2,sleep:1,turn_host_to_acpi_s3_state:2,volume_migrate:2"
}

View File

@ -1,36 +0,0 @@
series: bionic
relations:
- - mysql:shared-db
- keystone:shared-db
- - mysql:shared-db
- watcher:shared-db
- - keystone:identity-service
- watcher:identity-service
- - rabbitmq-server:amqp
- watcher:amqp
applications:
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: cloud:bionic-ussuri
mysql:
constraints: mem=3072M
charm: cs:~openstack-charmers-next/percona-cluster
num_units: 1
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
watcher:
series: bionic
charm: ../../../watcher
num_units: 1
options:
openstack-origin: cloud:bionic-ussuri
datasources: gnocchi
planner: weight
planner-config: >
{
"weights": "change_node_power_state:9,change_nova_service_state:50,migrate:30,nop:70,resize:20,sleep:40,turn_host_to_acpi_s3_state:10,volume_migrate:60",
"parallelization": "change_node_power_state:2,change_nova_service_state:1,migrate:2,nop:1,resize:2,sleep:1,turn_host_to_acpi_s3_state:2,volume_migrate:2"
}

View File

@ -1,61 +0,0 @@
variables:
openstack-origin: &openstack-origin cloud:focal-victoria
series: &series focal
applications:
keystone-mysql-router:
charm: cs:~openstack-charmers-next/mysql-router
watcher-mysql-router:
charm: cs:~openstack-charmers-next/mysql-router
mysql-innodb-cluster:
charm: cs:~openstack-charmers-next/mysql-innodb-cluster
num_units: 3
options:
source: *openstack-origin
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: *openstack-origin
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
options:
source: *openstack-origin
watcher:
series: *series
charm: ../../../watcher
num_units: 1
options:
openstack-origin: *openstack-origin
datasources: gnocchi
planner: weight
planner-config: >
{
"weights": "change_node_power_state:9,change_nova_service_state:50,migrate:30,nop:70,resize:20,sleep:40,turn_host_to_acpi_s3_state:10,volume_migrate:60",
"parallelization": "change_node_power_state:2,change_nova_service_state:1,migrate:2,nop:1,resize:2,sleep:1,turn_host_to_acpi_s3_state:2,volume_migrate:2"
}
relations:
- - 'keystone:shared-db'
- 'keystone-mysql-router:shared-db'
- - 'keystone-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'watcher:shared-db'
- 'watcher-mysql-router:shared-db'
- - 'watcher-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'keystone:identity-service'
- 'watcher:identity-service'
- - 'rabbitmq-server:amqp'
- 'watcher:amqp'

View File

@ -1,61 +0,0 @@
variables:
openstack-origin: &openstack-origin distro
series: &series groovy
applications:
keystone-mysql-router:
charm: cs:~openstack-charmers-next/mysql-router
watcher-mysql-router:
charm: cs:~openstack-charmers-next/mysql-router
mysql-innodb-cluster:
charm: cs:~openstack-charmers-next/mysql-innodb-cluster
num_units: 3
options:
source: *openstack-origin
keystone:
charm: cs:~openstack-charmers-next/keystone
num_units: 1
options:
openstack-origin: *openstack-origin
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
num_units: 1
options:
source: *openstack-origin
watcher:
series: *series
charm: ../../../watcher
num_units: 1
options:
openstack-origin: *openstack-origin
datasources: gnocchi
planner: weight
planner-config: >
{
"weights": "change_node_power_state:9,change_nova_service_state:50,migrate:30,nop:70,resize:20,sleep:40,turn_host_to_acpi_s3_state:10,volume_migrate:60",
"parallelization": "change_node_power_state:2,change_nova_service_state:1,migrate:2,nop:1,resize:2,sleep:1,turn_host_to_acpi_s3_state:2,volume_migrate:2"
}
relations:
- - 'keystone:shared-db'
- 'keystone-mysql-router:shared-db'
- - 'keystone-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'watcher:shared-db'
- 'watcher-mysql-router:shared-db'
- - 'watcher-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'keystone:identity-service'
- 'watcher:identity-service'
- - 'rabbitmq-server:amqp'
- 'watcher:amqp'

View File

@ -0,0 +1,62 @@
variables:
openstack-origin: &openstack-origin cloud:jammy-antelope
series: &series jammy
applications:
keystone-mysql-router:
charm: ch:mysql-router
channel: latest/edge
watcher-mysql-router:
charm: ch:mysql-router
channel: latest/edge
mysql-innodb-cluster:
charm: ch:mysql-innodb-cluster
channel: latest/edge
num_units: 3
options:
source: distro
keystone:
charm: ch:keystone
channel: latest/edge
num_units: 1
options:
openstack-origin: *openstack-origin
rabbitmq-server:
charm: ch:rabbitmq-server
channel: latest/edge
num_units: 1
options:
source: distro
watcher:
charm: ../../../watcher.charm
num_units: 1
options:
openstack-origin: *openstack-origin
debug: True
datasources: gnocchi
planner: weight
planner-config: >
{
"weights": "change_node_power_state:9,change_nova_service_state:50,migrate:30,nop:70,resize:20,sleep:40,turn_host_to_acpi_s3_state:10,volume_migrate:60",
"parallelization": "change_node_power_state:2,change_nova_service_state:1,migrate:2,nop:1,resize:2,sleep:1,turn_host_to_acpi_s3_state:2,volume_migrate:2"
}
relations:
- - 'keystone:shared-db'
- 'keystone-mysql-router:shared-db'
- - 'keystone-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'watcher:shared-db'
- 'watcher-mysql-router:shared-db'
- - 'watcher-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'keystone:identity-service'
- 'watcher:identity-service'
- - 'rabbitmq-server:amqp'
- 'watcher:amqp'

View File

@ -0,0 +1,62 @@
variables:
openstack-origin: &openstack-origin cloud:jammy-zed
series: &series jammy
applications:
keystone-mysql-router:
charm: ch:mysql-router
channel: latest/edge
watcher-mysql-router:
charm: ch:mysql-router
channel: latest/edge
mysql-innodb-cluster:
charm: ch:mysql-innodb-cluster
channel: latest/edge
num_units: 3
options:
source: distro
keystone:
charm: ch:keystone
channel: latest/edge
num_units: 1
options:
openstack-origin: *openstack-origin
rabbitmq-server:
charm: ch:rabbitmq-server
channel: latest/edge
num_units: 1
options:
source: distro
watcher:
charm: ../../../watcher.charm
num_units: 1
options:
openstack-origin: *openstack-origin
debug: True
datasources: gnocchi
planner: weight
planner-config: >
{
"weights": "change_node_power_state:9,change_nova_service_state:50,migrate:30,nop:70,resize:20,sleep:40,turn_host_to_acpi_s3_state:10,volume_migrate:60",
"parallelization": "change_node_power_state:2,change_nova_service_state:1,migrate:2,nop:1,resize:2,sleep:1,turn_host_to_acpi_s3_state:2,volume_migrate:2"
}
relations:
- - 'keystone:shared-db'
- 'keystone-mysql-router:shared-db'
- - 'keystone-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'watcher:shared-db'
- 'watcher-mysql-router:shared-db'
- - 'watcher-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'keystone:identity-service'
- 'watcher:identity-service'
- - 'rabbitmq-server:amqp'
- 'watcher:amqp'

View File

@ -1,39 +1,44 @@
variables:
openstack-origin: &openstack-origin distro
series: &series focal
series: &series lunar
applications:
keystone-mysql-router:
charm: cs:~openstack-charmers-next/mysql-router
charm: ch:mysql-router
channel: latest/edge
watcher-mysql-router:
charm: cs:~openstack-charmers-next/mysql-router
charm: ch:mysql-router
channel: latest/edge
mysql-innodb-cluster:
charm: cs:~openstack-charmers-next/mysql-innodb-cluster
charm: ch:mysql-innodb-cluster
channel: latest/edge
num_units: 3
options:
source: *openstack-origin
source: distro
keystone:
charm: cs:~openstack-charmers-next/keystone
charm: ch:keystone
channel: latest/edge
num_units: 1
options:
openstack-origin: *openstack-origin
rabbitmq-server:
charm: cs:~openstack-charmers-next/rabbitmq-server
charm: ch:rabbitmq-server
channel: latest/edge
num_units: 1
options:
source: *openstack-origin
source: distro
watcher:
series: *series
charm: ../../../watcher
charm: ../../../watcher.charm
num_units: 1
options:
openstack-origin: *openstack-origin
debug: True
datasources: gnocchi
planner: weight
planner-config: >
@ -43,19 +48,15 @@ applications:
}
relations:
- - 'keystone:shared-db'
- 'keystone-mysql-router:shared-db'
- - 'keystone-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'watcher:shared-db'
- 'watcher-mysql-router:shared-db'
- - 'watcher-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'keystone:identity-service'
- 'watcher:identity-service'
- - 'rabbitmq-server:amqp'
- 'watcher:amqp'
- - 'keystone:shared-db'
- 'keystone-mysql-router:shared-db'
- - 'keystone-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'watcher:shared-db'
- 'watcher-mysql-router:shared-db'
- - 'watcher-mysql-router:db-router'
- 'mysql-innodb-cluster:db-router'
- - 'keystone:identity-service'
- 'watcher:identity-service'
- - 'rabbitmq-server:amqp'
- 'watcher:amqp'

View File

@ -1,17 +1,17 @@
charm_name: watcher
gate_bundles:
- bionic-train
- bionic-ussuri
- focal-ussuri
- focal-victoria
- jammy-zed
- jammy-antelope
- lunar-antelope
smoke_bundles:
- focal-ussuri
- jammy-antelope
configure:
- zaza.charm_tests.noop.setup.basic_setup
tests:
- zaza.charm_tests.noop.tests.NoopTest
dev_bundles:
- groovy-victoria
tests_options:
force_deploy:
- groovy-victoria
force_deploy: []

View File

@ -1,29 +1,34 @@
[tox]
envlist = pep8
skipsdist = True
# NOTE(beisner): Avoid build/test env pollution by not enabling sitepackages.
# NOTE: Avoid build/test env pollution by not enabling sitepackages.
sitepackages = False
# NOTE(beisner): Avoid false positives by not skipping missing interpreters.
# NOTE: Avoid false positives by not skipping missing interpreters.
skip_missing_interpreters = False
[testenv]
# We use tox mainly for virtual environment management for test requirements
# and do not install the charm code as a Python package into that environment.
# Ref: https://tox.wiki/en/latest/config.html#skip_install
skip_install = True
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
whitelist_externals = juju
passenv = HOME TERM CS_API_* OS_*
allowlist_externals = juju
passenv =
HOME
TERM
CS_*
OS_*
TEST_*
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
functest-run-suite --help
[testenv:func]
basepython = python3

2
src/wheelhouse.txt Normal file
View File

@ -0,0 +1,2 @@
git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
git+https://github.com/openstack/charms.openstack.git#egg=charms.openstack

View File

@ -1,49 +1,5 @@
# This file is managed centrally by release-tools and should not be modified
# within individual charm repos. See the 'global' dir contents for available
# choices of *requirements.txt files for OpenStack Charms:
# https://github.com/openstack-charmers/release-tools
#
pyparsing<3.0.0 # aodhclient is pinned in zaza and needs pyparsing < 3.0.0, but cffi also needs it, so pin here.
cffi==1.14.6; python_version < '3.6' # cffi 1.15.0 drops support for py35.
setuptools<50.0.0 # https://github.com/pypa/setuptools/commit/04e3df22df840c6bb244e9b27bc56750c44b7c85
stestr>=2.2.0
# Dependency of stestr. Workaround for
# https://github.com/mtreinish/stestr/issues/145
cliff<3.0.0
# Dependencies of stestr. Newer versions use keywords that didn't exist in
# python 3.5 yet (e.g. "ModuleNotFoundError")
importlib-metadata<3.0.0; python_version < '3.6'
importlib-resources<3.0.0; python_version < '3.6'
# Some Zuul nodes sometimes pull newer versions of these dependencies which
# dropped support for python 3.5:
osprofiler<2.7.0;python_version<'3.6'
stevedore<1.31.0;python_version<'3.6'
debtcollector<1.22.0;python_version<'3.6'
oslo.utils<=3.41.0;python_version<'3.6'
requests>=2.18.4
charms.reactive
nose>=1.3.7
coverage>=3.6
charms.reactive
git+https://github.com/openstack/charms.openstack.git#egg=charms.openstack
#
# Revisit for removal / mock improvement:
#
# NOTE(lourot): newer versions of cryptography require a Rust compiler to build,
# see
# * https://github.com/openstack-charmers/zaza/issues/421
# * https://mail.python.org/pipermail/cryptography-dev/2021-January/001003.html
#
netifaces # vault
psycopg2-binary # vault
tenacity # vault
pbr==5.6.0 # vault
cryptography<3.4 # vault, keystone-saml-mellon
lxml # keystone-saml-mellon
hvac # vault, barbican-vault
psutil # cinder-lvm

90
tox.ini
View File

@ -1,47 +1,47 @@
# Source charm: ./tox.ini
# This file is managed centrally by release-tools and should not be modified
# within individual charm repos. See the 'global' dir contents for available
# choices of tox.ini for OpenStack Charms:
# https://github.com/openstack-charmers/release-tools
[tox]
skipsdist = True
envlist = pep8,py3
# NOTE: Avoid build/test env pollution by not enabling sitepackages.
sitepackages = False
# NOTE: Avoid false positives by not skipping missing interpreters.
skip_missing_interpreters = False
# NOTES:
# * We avoid the new dependency resolver by pinning pip < 20.3, see
# https://github.com/pypa/pip/issues/9187
# * Pinning dependencies requires tox >= 3.2.0, see
# https://tox.readthedocs.io/en/latest/config.html#conf-requires
# * It is also necessary to pin virtualenv as a newer virtualenv would still
# lead to fetching the latest pip in the func* tox targets, see
# https://stackoverflow.com/a/38133283
requires =
pip < 20.3
virtualenv < 20.0
setuptools<50.0.0
# NOTE: https://wiki.canonical.com/engineering/OpenStack/InstallLatestToxOnOsci
minversion = 3.18.0
[testenv]
skip_install = True
setenv = VIRTUAL_ENV={envdir}
PYTHONHASHSEED=0
TERM=linux
LAYER_PATH={toxinidir}/layers
INTERFACE_PATH={toxinidir}/interfaces
CHARM_LAYERS_DIR={toxinidir}/layers
CHARM_INTERFACES_DIR={toxinidir}/interfaces
JUJU_REPOSITORY={toxinidir}/build
passenv = http_proxy https_proxy INTERFACE_PATH LAYER_PATH JUJU_REPOSITORY
install_command =
{toxinidir}/pip.sh install {opts} {packages}
passenv =
no_proxy
http_proxy
https_proxy
CHARM_INTERFACES_DIR
CHARM_LAYERS_DIR
JUJU_REPOSITORY
allowlist_externals =
charmcraft
bash
tox
{toxinidir}/rename.sh
deps =
-r{toxinidir}/requirements.txt
[testenv:build]
basepython = python3
# charmcraft clean is done to ensure that
# `tox -e build` always performs a clean, repeatable build.
# For faster rebuilds during development,
# directly run `charmcraft -v pack && ./rename.sh`.
commands =
charmcraft clean
charmcraft -v pack
{toxinidir}/rename.sh
charmcraft clean
[testenv:build-reactive]
basepython = python3
commands =
charm-build --log-level DEBUG --use-lock-file-branches -o {toxinidir}/build/builds src {posargs}
@ -55,37 +55,35 @@ basepython = python3
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py35]
basepython = python3.5
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py36]
basepython = python3.6
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py37]
basepython = python3.7
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py38]
basepython = python3.8
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py39]
basepython = python3.9
[testenv:py310]
basepython = python3.10
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:py311]
basepython = python3.11
deps = -r{toxinidir}/test-requirements.txt
commands = stestr run --slowest {posargs}
[testenv:pep8]
basepython = python3
deps = flake8==3.9.2
charm-tools==2.8.3
deps = flake8
charm-tools
commands = flake8 {posargs} src unit_tests
[testenv:func-target]
basepython = python3
changedir = {toxinidir}/src
deps =
-r{toxinidir}/test-requirements.txt
-r{toxinidir}/src/test-requirements.txt
commands = functest-run-suite --keep-model --bundle {posargs}
[testenv:cover]
# Technique based heavily upon
# https://github.com/openstack/nova/blob/master/tox.ini