6 minute read

Migrating from Amulet to Zaza can seem like harrowing experience, but it doesn’t have to be! In this post, we will take a charm written with Amulet tests and convert it to a Zaza charm. This will be a multiple step process, starting with adding the tests in-tree, and then later migrating them to a shared library. While migrating the tests to a shared library is wonderfully useful if you are writing tests that may be useful to other services, it is an optional step!

Our example will work through the changes we might make to the Ubuntu charm

Where we came from

In the Ubuntu charm’s tests directory, we see several things:

$ tree tests
tests
├── 010_basic_precise
├── 015_basic_trusty
├── 030_basic_xenial
├── 035_basic_artful
├── 040_basic_bionic
├── basic_deployment.py
├── charmhelpers
│   ├── contrib
│   │   ├── amulet
│   │   │   ├── deployment.py
│   │   │   ├── __init__.py
│   │   │   └── utils.py
│   │   └── __init__.py
│   └── __init__.py
├── setup
│   └── 00-setup.sh
└── tests.yaml

Reading a basic Amulet test

The important things to note in the file tree showing the tests directory in the Ubuntu charm are the structure of one of the ##_basic_series files, and the basic_deployment.py.

First, we’ll see the executable, 040_basic_bionic:

#!/usr/bin/python
"""Amulet tests on a basic ubuntu charm deployment on bionic."""

from basic_deployment import ubuntu_basic_deployment

if __name__ == '__main__':
    ubuntu_basic_deployment(series='bionic')

The other ##_basic_series files are almost identical, excepting the series='bionic' line

We can see that we’re importing the function ubuntu_basic_deployment from basic_deployment, so lets look at that next:

basic_deployment.py

First, we have some basic documentation and a couple of imports:

#!/usr/bin/python3
"""
Ubuntu charm functional test.  Take note that the Ubuntu
charm does not have any relations or config options
to exercise.
"""

import amulet
from charmhelpers.contrib.amulet.utils import AmuletUtils
import logging

Now we come to the meat of this test, ubuntu_basic_deployment, which starts with some basic Amulet configuration, and a command to deploy the ubuntu charm:

def ubuntu_basic_deployment(series):
    """ Common test routines to run per-series. """

    # Initialize
    seconds = 900
    u = AmuletUtils(logging.DEBUG)
    d = amulet.Deployment(series=series)
    d.add('ubuntu')

After we deploy ubuntu, we want to ensure that the deployment completed and the units are ready:

    # Deploy services, wait for started state.  Fail or skip on timeout.
    try:
        d.setup(timeout=seconds)
        sentry_unit = d.sentry['ubuntu'][0]
    except amulet.helpers.TimeoutError:
        message = 'Deployment timed out ({}s)'.format(seconds)
        amulet.raise_status(amulet.FAIL, msg=message)
    except:
        raise

Now we’re ready to actually run our test! In this test, what we’re doing is getting the application version from the deployed unit and ensuring that the series equals the series we were configured to run before!

    # Confirm Ubuntu release name from the unit.
    release, ret = u.get_ubuntu_release_from_sentry(sentry_unit)
    if ret:
        # Something went wrong trying to query the unit, or it is an
        # unknown/alien release name based on distro-info validation.
        amulet.raise_status(amulet.FAIL, msg=ret)

    if release == series:
        u.log.info('Release/series check:  OK')
    else:
        msg = 'Release/series check:  FAIL ({} != {})'.format(release, series)
        u.log.error(msg)
        amulet.raise_status(amulet.FAIL, msg=msg)

In summary, the Amulet tests for the Ubuntu charm deploys a charm on a particular series, waits for the Juju model to stabilize, and then ensures that the Ubuntu charm sets it’s application version to the correct value!

Writing a Zaza test

Lets look at how we could reproduce the above behaviour in Zaza!

First, we’d want a slightly different file structure:

$ tree tests
tests
├── bundles
│   ├── artful.yaml
│   ├── bionic.yaml
│   ├── precise.yaml
│   ├── trusty.yaml
│   └── xenial.yaml
└── tests.yaml
└── basic_deployment.py

Taking this one step at a time, the tests.yaml might look like:

gate_bundles:
  - precise
  - trusty
  - xenial
  - artful
  - bionic
smoke_bundles:
  - bionic
tests:
  - tests.basic_deployment.BasicDeployment

Breaking this down a little bit, the gate_bundles and smoke_bundles keys are used to configure Zaza. When calling Zaza’s functest-run-suite, by default the gate_bundles will be run and tested, but it’s possible to pass --smoke, at which point only the smoke_bundles will be run.

The tests key refers th unittest.TestCase instances. In this case, I’ve started by modifying the basic_deployment.py into:

#!/usr/bin/python3
"""
Ubuntu charm functional test using Zaza. Take note that the Ubuntu
charm does not have any relations or config options to exercise.
"""

import unittest
import zaza.model as model

class BasicDeployment(unittest.TestCase):
    def test_ubuntu_series(self):
        pass

Given our example using bionic earlier, let’s look at that bundles/bionic.yaml next:

series: bionic
applications:
  ubuntu:
    charm: cs:ubuntu
    num_units: 1

This bundle is just a plan Juju bundle. As you can see, we configure the series at the top level and deploy the ubuntu charm.

Given the above configuration, We should have enough to proove that our Zaza setup is workable, although we have no useful tests yet! Just to confirm, I’ve setup a virtualenv, installed Zaza, and run functest-run-suite --keep-model --smoke which results in:

2019-05-21 11:30:02 [INFO] Deploying bundle './tests/bundles/bionic.yaml' on to 'zaza-637dd5f09105' model
Resolving charm: cs:ubuntu
Executing changes:
- upload charm cs:ubuntu-12 for series bionic
- deploy application ubuntu on bionic using cs:ubuntu-12
- add unit ubuntu/0 to new machine 0
Deploy of bundle completed.
2019-05-21 11:30:07 [INFO] Waiting for environment to settle
2019-05-21 11:30:07 [INFO] Waiting for a unit to appear
2019-05-21 11:30:07 [INFO] Waiting for all units to be idle
2019-05-21 11:31:53 [INFO] Checking workload status of ubuntu/0
2019-05-21 11:31:53 [INFO] Checking workload status message of ubuntu/0
2019-05-21 11:31:53 [INFO] ## Running Test tests.basic_deployment.BasicDeployment ##
test_ubuntu_series (tests.basic_deployment.BasicDeployment) ... ok

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
____________________________________________________________________________________ summary _____________________________________________________________________________________
  func-smoke: commands succeeded
  congratulations :)

Don’t loose functional coverage!

Now, we should ensure that we still maintain our existing functional test! This shouldn’t be too hard. Let’s update our basic_depoloyment.py like this:

#!/usr/bin/python3
"""
Ubuntu charm functional test using Zaza. Take note that the Ubuntu
charm does not have any relations or config options to exercise.
"""

import unittest
import zaza.model as model

class BasicDeployment(unittest.TestCase):

    def test_ubuntu_series(self):
-        pass
+        first_unit = model.get_units('ubuntu')[0]
+        result = model.run_on_leader('ubuntu', 'lsb_release -cs')
+        self.assertEqual(result['Code'], '0')
+        self.assertEqual(result['Stdout'].strip(), first_unit.series)

If we run the same command as above, we should get the same output, but this time, we’ve actually validated the same thing that the Amulet test validated before, success!

Summary

In this change, we have accomplished the following changes:

 test-requirements.txt                           |   1 +
 tests/010_basic_precise                         |   7 --
 tests/015_basic_trusty                          |   7 --
 tests/030_basic_xenial                          |   7 --
 tests/035_basic_artful                          |   7 --
 tests/040_basic_bionic                          |   7 --
 tests/basic_deployment.py                       |  48 +++--------
 tests/bundles/artful.yaml                       |   5 ++
 tests/bundles/bionic.yaml                       |   5 ++
 tests/bundles/precise.yaml                      |   5 ++
 tests/bundles/trusty.yaml                       |   5 ++
 tests/bundles/xenial.yaml                       |   5 ++
 tests/charmhelpers/__init__.py                  |  38 ---------
 tests/charmhelpers/contrib/__init__.py          |  15 ----
 tests/charmhelpers/contrib/amulet/__init__.py   |  15 ----
 tests/charmhelpers/contrib/amulet/deployment.py |  93 ----------------------
 tests/charmhelpers/contrib/amulet/utils.py      | 533 --------------------------------------------------------------------------------------------------------------------------
 tests/setup/00-setup.sh                         |   9 ---
 tests/tests.yaml                                |  22 +++--
 tox.ini                                         |  35 ++++++++
 20 files changed, 81 insertions(+), 788 deletions(-)

An additional, small, takeaway for the readers who made it all the way to the bottom, an example tox.ini that the OpenStack charms project uses for managing out virtualenvs with Zaza:

[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}

and a snippet from a test-requirements.txt:

git+https://github.com/openstack-charmers/zaza.git#egg=zaza