Charms are very flexible and can be written several ways and even in different languages. Having good template limits those options but gets you started faster. This post will cover a template we’ll be using for the pirate-charmers charms that get you started with unit and functional testing out of the box. By the end of this post you’ll be able to quickly create a template that passes testing so you can focus on charming and not test frameworks.
I’ll perform the install inside a LXD container from scratch. If you want to do the same to kick the tires you’ll need a container that supports nesting. Create the container with:
lxc launch ubuntu -c security.nesting=true -c security.privileged=true
lxc exec $YOUR_CONTAINER_NAME -- sudo su - ubuntu
Once the container has started you can use the second command to become ubuntu in the container. You can do this without a privileged container here is an example with more information.
Charm Tools
Templates are available via the charm-create command which comes with the charm tools snap necessary for building charms. Today that tools doesn’t support external 3rd party templates. The feature is being reviewed to allow this support but until that time I’m working around the issue with a monkey patch on the tool to enable a custom template.
If you don’t already have the charm tools you should install the snap.
sudo snap install charm --classic
Verify the install by running charm create help
charm create -h
This should output the help from charm-create.
Next you’ll need the charm-template repository to provide the monkey patch. This is not the repository for the template, this is the monkey patch which should go away if/when charm-tools are able to natively include external templates. You do not need to edit or download the template repository the tool will do that when you create a new charm.
git clone https://github.com/pirate-charmers/charm-template.git
cd charm-template
Now verify the monkey patch is working by running the charm-create.py from the repository and verifying the new template is available.
./charm-create.py -h
You should see python-pytest is listed as an installed template.
optional arguments:
-h, --help show this help message and exit
-t TEMPLATE, --template TEMPLATE
Name of charm template to use; default is reactive-
python. Installed templates: python-pytest
Template python-pytest
Creating a charm
To create a new charm from the template call charm create and specify the template as python-pytest. Additionally, charm-create accepts a charm name and an optional root folder. My preference is to always provide both, if the application is available in apt some metadata will be filled out for you. For this example I’ll create an empty charm for ddclient.
./charm-create.py -t python-pytest ddclient ../layer-ddclient
cd ../layer-ddclient
Testing
Before going through how the template is laid out we’ll kick off a full set of tests. If you are familiar with charming and python tests this might be all the demo you need to get started. Deploying the charm can take some time so this also allows tests to run while you read about the template.
Since I’m doing this in a fresh bionic LXD I’ll setup juju for local deploys, you can read about that here if you are new to juju. I’m running as the user ‘ubuntu’ on bionic in a LXD container which can be setup with:
sudo snap install juju --classic
juju bootstrap localhost lxd
If everything went well juju status
should show an empty model named default
is ready for use. Now you can run the tests with from the root of the template
folder you’ll and will need just two dependencies make and tox:
sudo apt install make tox
JUJU_REPOSITORY=$(pwd) make test
In this case I’m setting the current directory to the JUJU_REPOSITORY because I’m running in a container. I have an environment variable set for a common location that I build charms to on my workstation which removes the need to specify the repository location when calling make. If you are new to charming you can read more in the getting started documents about the environment variables. However, with this template only the JUJU_REPOSITORY need to be set because the Layer and Interface paths are set by the template.
That’s all you should need. This will install a python virtual environment for unit testing, unit test the template, build it into a charm, install a virtual environment for functional testing, and deploy the charm with libjuju. You’ll see reports for the tests that pass, unit test coverage, and the result of linting with flake8.
The output looks like this:
$ JUJU_REPOSITORY=$(pwd) make test
unit installed: atomicwrites==1.2.1,attrs==18.2.0,charmhelpers==0.19.7,charms.reactive==1.1.2,coverage==4.5.2,Jinja2==2.10,MarkupSafe==1.1.0,mock==2.0.0,more-itertools==5.0.0,netaddr==0.7.19,pbr==5.1.1,pkg-resources==0.0.0,pluggy==0.8.1,py==1.7.0,pyaml==18.11.0,pytest==4.1.1,pytest-cov==2.6.1,PyYAML==3.13,six==1.12.0,Tempita==0.5.2
unit runtests: PYTHONHASHSEED='3584756322'
unit runtests: commands[0] | pytest -v --ignore /home/ubuntu/charm-template/layer-ddclient/src/tests/functional --cov=lib --cov=reactice --cov=actions --cov-report=term
========================================= test session starts ==========================================
platform linux -- Python 3.6.7, pytest-4.1.1, py-1.7.0, pluggy-0.8.1 -- /home/ubuntu/charm-template/layer-ddclient/src/.tox/unit/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/charm-template/layer-ddclient/src, inifile:
plugins: cov-2.6.1
collected 3 items
tests/unit/test_actions.py::TestActions::test_example_action PASSED [ 33%]
tests/unit/test_lib.py::TestLib::test_pytest PASSED [ 66%]
tests/unit/test_lib.py::TestLib::test_ddclient PASSED [100%]Coverage.py warning: Module reactice was never imported. (module-not-imported)
----------- coverage: platform linux, python 3.6.7-final-0 -----------
Name Stmts Miss Cover
--------------------------------------------
actions/example-action 3 0 100%
lib/lib_ddclient.py 6 1 83%
--------------------------------------------
TOTAL 9 1 89%
======================================= 3 passed in 0.40 seconds =======================================
_______________________________________________ summary ________________________________________________
unit: commands succeeded
congratulations :)
Building charm to base directory /home/ubuntu/charm-template/layer-ddclient
fatal: No names found, cannot describe anything.
Makefile:36: recipe for target 'build' failed
make: [build] Error 128 (ignored)
build: Destination charm directory: /home/ubuntu/charm-template/layer-ddclient/builds/ddclient
build: Please add a `repo` key to your layer.yaml, with a url from which your layer can be cloned.
build: Processing layer: layer:options
build: Processing layer: layer:basic
build: Processing layer: ddclient (from src)
proof: I: `display-name` not provided, add for custom naming in the UI
proof: W: Includes template README.ex file
proof: W: README.ex includes boilerplate: Step by step instructions on using the charm:
proof: W: README.ex includes boilerplate: You can then browse to http://ip-address to configure the service.
proof: W: README.ex includes boilerplate: - Upstream mailing list or contact information
proof: W: README.ex includes boilerplate: - Feel free to add things if it\'s useful for users
proof: I: all charms should provide at least one thing
functional installed: asn1crypto==0.24.0,atomicwrites==1.2.1,attrs==18.2.0,bcrypt==3.1.6,certifi==2018.11.29,cffi==1.11.5,chardet==3.0.4,cryptography==2.5,flake8==3.6.0,idna==2.8,juju==0.11.2,jujubundlelib==0.5.6,macaroonbakery==1.2.1,mccabe==0.6.1,mock==2.0.0,more-itertools==5.0.0,paramiko==2.4.2,pbr==5.1.1,pkg-resources==0.0.0,pluggy==0.8.1,protobuf==3.6.1,py==1.7.0,pyasn1==0.4.5,pycodestyle==2.4.0,pycparser==2.19,pyflakes==2.0.0,pymacaroons==0.13.0,PyNaCl==1.3.0,pyRFC3339==1.1,pytest==4.1.1,pytest-asyncio==0.10.0,pytz==2018.9,PyYAML==3.13,requests==2.21.0,six==1.12.0,theblues==0.5.1,urllib3==1.24.1,websockets==7.0
functional runtests: PYTHONHASHSEED='3922428688'
functional runtests: commands[0] | pytest -v --ignore /home/ubuntu/charm-template/layer-ddclient/src/tests/unit
========================================= test session starts ==========================================
platform linux -- Python 3.6.7, pytest-4.1.1, py-1.7.0, pluggy-0.8.1 -- /home/ubuntu/charm-template/layer-ddclient/src/.tox/functional/bin/python3
cachedir: .pytest_cache
rootdir: /home/ubuntu/charm-template/layer-ddclient/src, inifile:
plugins: asyncio-0.10.0
collected 4 items
tests/functional/test_deploy.py::test_ddclient_deploy[xenial] PASSED [ 25%]
tests/functional/test_deploy.py::test_ddclient_deploy[bionic] PASSED [ 50%]
tests/functional/test_deploy.py::test_ddclient_status PASSED [ 75%]
tests/functional/test_deploy.py::test_example_action PASSED [100%]
====================================== 4 passed in 353.29 seconds ======================================
_______________________________________________ summary ________________________________________________
functional: commands succeeded
congratulations :)
Running flake8
lint installed: flake8==3.6.0,mccabe==0.6.1,pkg-resources==0.0.0,pycodestyle==2.4.0,pyflakes==2.0.0
lint runtests: PYTHONHASHSEED='1094685518'
lint runtests: commands[0] | flake8
_______________________________________________ summary ________________________________________________
lint: commands succeeded
congratulations :)
You can run juju status
to see the application has deployed for both xenial
and bionic.
$ juju status
Model Controller Cloud/Region Version SLA Timestamp
default lxd localhost/localhost 2.5.0 unsupported 15:49:04Z
App Version Status Scale Charm Store Rev OS Notes
ddclient-bionic active 1 ddclient local 0 ubuntu
ddclient-xenial active 1 ddclient local 0 ubuntu
Unit Workload Agent Machine Public address Ports Message
ddclient-bionic/0* active idle 1 10.229.130.66
ddclient-xenial/0* active idle 0 10.229.130.207
Machine State DNS Inst id Series AZ Message
0 started 10.229.130.207 juju-3f9a32-0 xenial Running
1 started 10.229.130.66 juju-3f9a32-1 bionic Running
At this point you have an empty but deployable charm which unit tests and deploys locally. If you are familiar with charming, this might be all you need to get started. If you are new to charming you can use the template to learn even before you understand all of the moving parts. Eventually you will want to understand how testing is done. I’ll follow up with additional post(s) to go through the details of how the template is organized and why.
To clean up your model you can destroy the default model and create a clean one to deploy to again.
juju destroy-model default && juju add-model default
Be careful when using destroy-model, it will remove all machines in the model. I do not install my ‘production’ workloads in the model named default. I consider anything in such a model expendable. I recommend getting in a similar habit to avoid completely removing a production model. While you will be prompted, I destroy and recreate the default models on my local host frequently enough it’s muscle memory at this point and a warning prompt no longer causes me any pause. Pick a name for testing and stick to it, it just might save your deployment.