Ubuntu logo

Writing a charm

This tutorial demonstrates the basic workflow for writing, running and debugging a juju charm. Charms are a way to package and share your service deployment and orchestration knowledge and share them with the world.

Creating the charm

In this example we are going to write a charm to deploy the drupal CMS system. For the sake of simplicity, we are going to use the mysql charm that comes bundled with juju. Assuming the current directory is the juju trunk, let's enter the directory

cd examples/precise

Creating a README File

First create a README file with your text editor. This file must be plain text, the Charm Store will parse Markdown format and display it on the Charm Store's web interface, so it will be the public facing page for your charm. We recommend you leave basic usage instructions on how to use your charm in the README.

Making the metadata

Now let's make the metadata and hooks directory:

mkdir -p drupal/hooks

vim drupal/metadata.yaml

Edit the metadata.yaml file to resemble:

name: drupal
summary: "Drupal CMS"
maintainer: "Drupal PowerUser <drupaluser@somedomain.foo>"
description: |
    Installs the drupal CMS system, relates to the mysql charm provided in
    examples directory. Can be scaled to multiple web servers
requires:
  db:
    interface: mysql

The metadata.yaml file provides metadata around the charm. The file declares a charm with the name drupal. Since this is the first time to edit this charm, its revision number is one. A short and long description of the charm are provided. The final field is requires, this mentions the interface type required by this charm. Since this drupal charm uses the services of a mysql database, we need to require it in the metadata. Since this charm does not provide a service to any other charm, there is no provides field. You might be wondering where did the interface name "mysql" come from, you can locate the interface information from the mysql charm's metadata.yaml. Here it is for convenience:

name: mysql
summary: "MySQL relational database provider"
maintainer: "Joe Charmer <youremail@whatever.com>"
description: |
    Installs and configures the MySQL package (mysqldb), then runs it.

    Upon a consuming service establishing a relation, creates a new
    database for that service, if the database does not yet
    exist. Publishes the following relation settings for consuming
    services:

      database: database name
      user: user name to access database
      password: password to access the database
      host: local hostname
provides:
  db:
    interface: mysql

That very last line mentions that the interface that mysql provides to us is "mysql". Also the description mentions that four parameters are sent to the connecting charm (database, user, password, host) in order to enable it to connect to the database. We will make use of those variables once we start writing hooks. Such interface information is either provided in a bundled README file, or in the description. You can also read the charm code to discover such information as well.

In the next steps we will write the necessary hook scripts.

Have a plan

When attempting to write a charm, it is beneficial to have a mental plan of what it takes to deploy the software. In our case, you should deploy drupal manually, understand where its configuration information is written, how the first node is deployed, and how further nodes are configured. With respect to this charm, this is the plan:

  • Install hook installs all needed components (apache, php, drush)
  • Once the database connection information is ready, call drush on first node to perform the initial setup (creates DB tables, completes setup)
  • For scaling onto other nodes, the DB tables have already been set-up. Thus we only need to append the database connection information into drupal's settings.php file. We will use a template file for that

Note

The hooks in a charm are executable files that can be written using any scripting or programming language. In our case, we'll use bash

For production charms it is always recommended that you install software components from the Ubuntu archive (using apt-get) in order to get security updates. However in this example I am installing drush (Drupal shell) using apt-get, then using that to download and install the latest version of drupal. If you were deploying your own code, you could just as easily install a revision control tool (bzr, git, hg...etc) and use that to checkout a code branch to deploy from. This demonstrates the flexibility offered by juju which doesn't really force you into one way of doing things.

Write hooks

Let's change into the hooks directory:

$ cd drupal/hooks
vim install

Since you should have already installed drupal, you have an idea what it takes to get it installed. My install script looks like:

#!/bin/bash

set -eux # -x for verbose logging to juju debug-log
juju-log "Installing drush,apache2,php via apt-get"
apt-get -y install drush apache2 php5-gd libapache2-mod-php5 php5-cgi mysql-client-core-5.5
a2enmod php5
/etc/init.d/apache2 restart
juju-log "Using drush to download latest Drupal"
# Typo on next line, it should be www not ww
cd /var/ww && drush dl drupal --drupal-project-rename=juju

I have introduced an artificial typo on the last line "ww not www", this is to simulate any error which you are bound to face sooner or later. Let's create other hooks:

$ vim start

The start hook is empty, however it needs to be a valid executable, thus we'll add the first bash shebang line, here it is:

#!/bin/bash

Here's the "stop" script:

#!/bin/bash
juju-log "Stopping apache"
/etc/init.d/apache2 stop

The final script, which does most of the work is "db-relation-changed". This script gets the database connection information set by the mysql charm then sets up drupal for the first time, and opens port 80 for web access. Let's start with a simple version that only installs drupal on the first node. Here it is:

#!/bin/bash
set -eux # -x for verbose logging to juju debug-log
hooksdir=$PWD
user=`relation-get user`
password=`relation-get password`
host=`relation-get host`
database=`relation-get database`
# All values are set together, so checking on a single value is enough
# If $user is not set, DB is still setting itself up, we exit awaiting next run
[ -z "$user" ] && exit 0
juju-log "Setting up Drupal for the first time"
cd /var/www/juju && drush site-install -y standard \
--db-url=mysql://$user:$password@$host/$database \
--site-name=juju --clean-url=0
cd /var/www/juju && chown www-data sites/default/settings.php
open-port 80/tcp

The script is quite simple, it reads the four variables needed to connect to mysql, ensures they are not null, then passes them to the drupal installer. Make sure all the hook scripts have executable permissions, and change directory above the examples directory:

$ chmod +x *
$ cd ../../../..

Checking on the drupal charm file-structure, this is what we have:

$ find examples/precise/drupal
examples/precise/drupal
examples/precise/drupal/metadata.yaml
examples/precise/drupal/hooks
examples/precise/drupal/hooks/db-relation-changed
examples/precise/drupal/hooks/stop
examples/precise/drupal/hooks/install
examples/precise/drupal/hooks/start

Test run

Let us deploy the drupal charm. Remember that the install hook has a problem and will not exit cleanly. Deploying:

juju bootstrap

Wait a minute for the environment to bootstrap. Keep issuing the status command till you know the environment is ready:

juju status

2011-06-07 14:04:06,816 INFO Connecting to environment.
machines:
  0:
    agent-state: running
    dns-name: ec2-50-16-107-102.compute-1.amazonaws.com
    instance-id: i-130c9168
    instance-state: running
services:
2011-06-07 14:04:11,125 INFO 'status' command finished successfully

It can be beneficial when debugging a new charm to always have the distributed debug-log running in a separate window:

juju debug-log

Let's deploy the mysql and drupal charms:

juju deploy --repository=examples local:precise/mysql
juju deploy --repository=examples local:precise/drupal

This deploy is telling juju to look in a local repository for our charm, specifically in the examples/precise/mysql and examples/precise/drupal folders. Local repositories specified with the --repository switch must point to a directory which contains sub-directories named after an Ubuntu series (e.g. precise/) and the charms. The repository can be named anything you wish. Thus, when creating charms locally, this syntax should be followed

repositoryName/ubuntuReleaseName/charmName

Once the machines are started (hint: check the debug-log), issue a status command:

juju status

machines:
  0:
    agent-state: running
    dns-name: ec2-50-16-107-102.compute-1.amazonaws.com
    instance-id: i-130c9168
    instance-state: running
  1:
    agent-state: running
    dns-name: ec2-50-19-24-186.compute-1.amazonaws.com
    instance-id: i-17079a6c
    instance-state: running
  2:
    agent-state: running
    dns-name: ec2-23-20-194-198.compute-1.amazonaws.com
    instance-id: i-d7079aac
    instance-state: running
services:
  mysql:
    charm: cs:precise/mysql-3
    relations: {}
    units:
      mysql/0:
        agent-state: started
        machine: 2
        public-address: ec2-23-20-194-198.compute-1.amazonaws.com
  drupal:
    charm: local:precise/drupal-3
    relations: {}
    units:
      drupal/0:
        agent-state: install-error
        machine: 1
        public-address: ec2-50-19-24-186.compute-1.amazonaws.com

Note how mysql is listed as started, while drupal's state is install_error. This is because the install hook has an error, and did not exit cleanly (exit code 1).

Debugging hooks

Let's debug the install hook, from a new window:

juju debug-hooks drupal/0

This will connect you to the drupal machine, and present a shell. The way the debug-hooks functionality works is by starting a new terminal window instead of executing a hook when it is triggered. This way you get a chance of running the hook manually, fixing any errors and re-running it again. In order to trigger re-running the install hook, from another window:

juju resolved --retry drupal/0

Switching to the debug-hooks window, you will notice a new window named "install" popped up. Note that "install" is the name of the hook that this debug-hooks session is replacing. We change directory into the hooks directory and rerun the hook manually:

$ cd /var/lib/juju/units/drupal-0/charm/hooks/
$ ./install
# -- snip --
+ cd /var/ww
./install: line 10: cd: /var/ww: No such file or directory

Problem identified. Let's edit the script, changing ww into www. Rerunning it again should work successfully. This is why it is very good practice to write hook scripts in an idempotent manner such that rerunning them over and over always results in the same state. Do not forget to exit the install window by typing "exit", this signals that the hook has finished executing successfully. If you have finished debugging, you may want to exit the debug-hooks session completely by typing "exit" into the very first window Window0

Note

While we have fixed the script, this was done on the remote machine only. You need to update the local copy of the charm with your changes, increment the revision number in the revision file and perform a charm upgrade to push the changes, like:

juju upgrade-charm --repository=examples/ drupal

Let's continue after having fixed the install error:

juju add-relation mysql drupal

Watching the debug-log window, you can see debugging information to verify the hooks are working as they should. If you spot any error, you can launch debug-hooks in another window to start debugging the misbehaving hooks again. Note that since "add-relation" relates two charms together, you cannot really retrigger it by simply issuing "resolved --retry" like we did for the install hook. In order to retrigger the db-relation-changed hook, you need to remove the relation, and create it again like so:

juju remove-relation mysql drupal
juju add-relation mysql drupal

The service should now be ready for use. The remaining step is to expose it to public access. While the charm signaled it needs port 80 to be open, for public accessibility, the port is not open until the administrator explicitly uses the expose command:

juju expose drupal

Let's see a status with the ports exposed:

juju status

machines:
  0:
    agent-state: running
    dns-name: ec2-50-16-107-102.compute-1.amazonaws.com
    instance-id: i-130c9168
    instance-state: running
  1:
    agent-state: running
    dns-name: ec2-50-19-24-186.compute-1.amazonaws.com
    instance-id: i-17079a6c
    instance-state: running
  2:
    agent-state: running
    dns-name: ec2-23-20-194-198.compute-1.amazonaws.com
    instance-id: i-d7079aac
    instance-state: running
services:
  mysql:
    charm: cs:precise/mysql-3
    relations:
      db:
      - drupal
    units:
      mysql/0:
        agent-state: started
        machine: 2
        public-address: ec2-23-20-194-198.compute-1.amazonaws.com
  drupal:
    charm: cs:precise/drupal-3
    exposed: true
    relations:
      db:
      - mysql
    units:
      drupal/0:
        agent-state: started
        machine: 1
        open-ports:
        - 80/tcp
        public-address: ec2-50-19-24-186.compute-1.amazonaws.com

Congratulations, your charm should now be working successfully! The db-relation-changed hook previously shown is not suitable for scaling drupal to more than one node, since it always drops the database and recreates a new one. A more complete hook would need to first check whether or not the DB tables exist and act accordingly. Here is how such a hook might be written:

#!/bin/bash
set -eux # -x for verbose logging to juju debug-log
hooksdir=$PWD
user=`relation-get user`
password=`relation-get password`
host=`relation-get host`
database=`relation-get database`
# All values are set together, so checking on a single value is enough
# If $user is not set, DB is still setting itself up, we exit awaiting next run
[ -z "$user" ] && exit 0

if $(mysql -u $user --password=$password -h $host -e 'use drupal; show tables;' | grep -q users); then
    juju-log "Drupal already set-up. Adding DB info to configuration"
    cd /var/www/juju/sites/default
    cp default.settings.php settings.php
    sed -e "s/USER/$user/" \
    -e "s/PASSWORD/$password/" \
    -e "s/HOST/$host/" \
    -e "s/DATABASE/$database/" \
    $hooksdir/drupal-settings.template >> settings.php
else
    juju-log "Setting up Drupal for the first time"
    cd /var/www/juju && drush site-install -y standard \
    --db-url=mysql://$user:$password@$host/$database \
    --site-name=juju --clean-url=0
fi
cd /var/www/juju && chown www-data sites/default/settings.php
open-port 80/tcp

Note

Any files that you store in the hooks directory are transported as is to the deployment machine. You can drop in configuration files or templates that you can use from your hook scripts. An example of this technique is the drupal-settings.template file that is used in the previous hook. The template is rendered using sed, however any other more advanced template engine can be used

Here is the template file used:

$databases = array (
  'default' =>
  array (
    'default' =>
    array (
      'database' => 'DATABASE',
      'username' => 'USER',
      'password' => 'PASSWORD',
      'host' => 'HOST',
      'port' => '',
      'driver' => 'mysql',
      'prefix' => '',
    ),
  ),
);

Learn more

Read more detailed information about Charms and hooks. For more hook examples, please check the examples directory in the juju source tree, or check out the various charms already included in the Charm Store.