Zk | 2015-04-09-dogfooding-2


type: post date: 2015-04-09 slug: dogfooding-2 title: Dogfooding - Pt. 2


This is a continuation of the first post; you should read that first!

This is part 2 of the "Dogfooding Juju" series that I'm doing. This time, I want to go into a little bit of detail about the Warren charm and how I wound up structuring it. As I mentioned in the previous post, there are perhaps more elegant ways to do this, but I found the documentation to be lacking in ways that prevented me from dedicating relatively scant free time to the task. Instead, I wound up following the path that I've followed before on the job with several of the charms we use for our own projects.

How a Charm Works

One can think of a charm as a deployment solution. In a lot of ways, it follows the same path as many other dev-ops solutions out there, such as Ansible, Chef, or Puppet. In fact, one can use any one of those solutions (or more than one, if one is so inclined) in managing what happens during the deployment of a charm into a service, as many charms do. Charms are meant to be biased, best-practice solutions that install a service and describe the way that service relates to other services in the Juju environment.

A charm is, at its core, primarily built around two concepts: configuration and hooks. Configuration describes the way the charm is built and how it can interact with other services, while hooks describe how the service responds to state changes, both internally and externally. There is a third bit, which we won't get into here, as it's not relevant, but is worth mentioning as part of the charm, which is "actions". Actions are code within the service that responds to requests, either from the user or from the environment, through Juju itself.

A lot of (digital) ink has been spilled on charm building, so I'm not going to go too far in depth beyond explaining how this works with the Warren charm. Please feel free to check out the extensive docs if you're interested in diving deeper.

Configuration

Configuration primarily happens in two files used by the charm: metadata.yaml and config.yaml. metadata.yaml contains information about the charm, who wrote it, and how it connects to various other charms within the environment, while config.yaml contains all of the configuration options that a charm may use during execution of any of its numerous hooks.

Anyone who is familiar with packaging software in any way will be familiar with the way these two files work. You can specify the name of the charm, the authors, a short description, and some tags in the metadata.yaml file. Additionally, if this charm relies on other services, they will be defined in the interfaces section. config.yaml is basically a schema describing the configuration options that the charm uses. For each option, a type, a description, and a default may be provided.

Hooks

Hooks are where all of the action takes place in a charm. There are a few main hooks, and then several which depend on the state of the environment. The main hooks are:

The other hooks you might encounter are relation hooks. These are fired as the state of relations to the charm change. They come in four types, each of which includes the name of the relation interface as part of it:

The Structure of the Warren Charm

The Warren charm is basically typical, as far as charms go. It has its own metadata and config files, as well as a full collection of hooks.

Configuration

metadata.yaml

The metadata.yaml file contains a lot of basics that will be familiar at a glance. Name, summary, maintainer, description, tags, these are all pretty straight forward. Of note, however, are the subordinate element, which declares whether or not this service will be subordinate to another (a topic for a later date), and the provides/requires elements, which describe how this service can relate to others.

Provides describes the interface that this service will expose to others within the Juju environment. Of particular note (mostly because the others haven't been implemented yet) is the website interface, which provides a means of hosting content over HTTP/S. This will be used by the haproxy charm, which will provide load balancing over this interface.

Requires describes the interfaces that this service needs other charms to provide within the environment in order to run fully. In this case, this means Mongo via the mongodb charm, and ElasticSearch via the eponymous charm.


name: warren-charm
summary: Warren is a networked content-sharing site.
maintainer: Madison Scott-Clary 
description: |
  Warren is a networked content-sharing site, allowing users to not only post
  their creations, but link them together into a web of their works, and the
  works of others.  It manages each post as an abstract entity and uses content
  types to render those abstract types into something viewable within a
  browser.
tags:
  - social
  - cms
  - applications
subordinate: false
provides:
  website:
    interface: http
  nrpe-external-master:
    interface: nrpe-external-master
    scope: container
  local-monitors:
    interface: local-monitors
    scope: container
requires:
  mongodb:
    interface: mongodb
  elasticsearch:
    interface: elasticsearch

config.yaml

Our configuration values for this charm are also pretty straight-forward. You can see that we have options for an SMTP server, which will be used for sending notification emails, two keys which are used for encrypting session data, the database name, the port to listen on, and the source. Source is interesting because it's structured to allow various different ways to fetch the source for building Warren. Since this is a thin charm (that is, it does not include any of the source for Warren itself), the charm will have to figure out how to fetch the source as required. We've provided a few ways of specifying that, all of which interface with Git: one can specify a branch name, a tag name, or a commit SHA.


options:
  smtp-server:
    default: smtp.example.com
    description: Address for the SMTP server for sending emails from Warren
    type: string
  session-auth-key:
    default: CHANGEME--------
    description: Session authentication key (16 or 32 bytes)
    type: string
  session-encryption-key:
    default: CHANGEME--------
    description: Session encryption key (16, 32, or 64 bytes)
    type: string
  mongo-db:
    default: warren
    description: The mongo database name
    type: string
  listen_port:
    default: 3000
    description: The port to listen on
    type: int
  source:
    default: "branch:master"
    description: A string containing a "branch:", "tag:", or "commit:" followed
      by a branch name, a tag name, or a commit, respectively
    type: string

Hooks

This is where the meat of the charm lives. Hooks are executable bits of code within the /hooks directory of the charm, each named appropriately. That is, there is an executable file in /hooks named install, one named start, and so on for all of the hooks that will be fired for our service. As is standard practice for this type of charm, I actually have all of the code in one file, hooks.py, and all of the hooks files are simply symlinked to point to that file.

I'm not going to go too in depth here, nor post the entire file, which you can look at yourself, but simply outline the way the hooks are called. Future posts may go more in depth as to how things work on a more atomic level.

First is our install hook, as shown by the decorator. This one takes care of some initial work that needs to be done to get the service up and running. It updates all packages, ensures dependencies (such as golang, git, and bzr), adds a user which will be used to run the service, makes source and build directories, and installs the source for Warren.


@hooks.hook('install')
def install():
    '''Install required packages, user, and warren source.'''
    apt_get_update()
    ensure_packages(*dependencies)    
    host.adduser(owner)
    prep_installation()
    install_from_source()

The stop hook is similarly simple. It stops the Warren service and deletes the upstart file for starting it.


@hooks.hook('stop')
def stop():
    '''Stop the warren service.'''
    log('Stopping service...')
    host.service_stop(system_service)
    if upstart_conf:
        unlink_if_exists(upstart_conf)

Here's where the fun begins. As is standard practice for several charms, many hooks should behave in the same way. This was put to me by Kapil Thangavelu as, "There should only be a config-changed hook, and everything else is subordinate to that." This means that all or most relation hooks, the config-changed hook, and the start hook should basically be the same.

Below, we've decorated the main hook method will most of our relation hooks, start, and config-changed. The work this does is fairly straight forward. It fetches the source and updates to the specified version if necessary, writes the Warren config file, writes the upstart file, opens or closes ports as necessary, and restarts the service.


@hooks.hook('start')
@hooks.hook('config-changed')
@hooks.hook('mongodb-relation-joined')
@hooks.hook('mongodb-relation-departed')
@hooks.hook('mongodb-relation-broken')
@hooks.hook('mongodb-relation-changed')
@hooks.hook('elasticsearch-relation-joined')
@hooks.hook('elasticsearch-relation-departed')
@hooks.hook('elasticsearch-relation-broken')
@hooks.hook('elasticsearch-relation-changed')
def main_hook():
    '''Main hook functionality
    On most hooks, we simply need to write config files, work with hooks, and
    restart.  If the source has changed, we'll additionally need to rebuild.
    '''
    if config.changed('source'):
        log('Source changed; rebuilding...')
        install_from_source()
    write_init_file()
    write_config_file()
    manage_ports()
    restart()

In our case, the haproxy hooks take a little bit more work, however. The haproxy service requires a bit of information from us: the hostname for this unit of the Warren service, and the port on which it is listening. For each website relation on this service, we simply send (using relation_set) those data to the remote service.


@hooks.hook('website-relation-joined')
@hooks.hook('website-relation-departed')
@hooks.hook('website-relation-broken')
@hooks.hook('website-relation-changed')
def website_relation_hook():
    '''Notify all website relations of our address and port.'''
    for relation_id in relations.get('website', {}).keys():
        private_address = hookenv.unit_private_ip()
        hookenv.relation_set(
            relation_id=relation_id,
            relation_settings={'hostname': private_address, 'port': config['listen_port']})

How are the hooks run? Simple. When the hooks.py file is called, we pass all the work on to the charmhelpers library, which will decide which decorated hook methods to call:


if __name__ == "__main__":
    hooks.execute(sys.argv)

The Good

There's just so much to be said for having a repeatable, debuggable (I'll get into juju debug-hooks at some point, promise!) means of deploying a service. With this layout for a charm, it's easy to see what hook does what, and is fairly easy to organize your code around that. The configuration files are in a familiar and readable format (I'm looking at you, countless *.pom files), and the python charmhelpers package keeps our hooks fairly simple.

The Bad

I'll be totally honest and say that a lot of the work that I did on this charm came from observing the ways other charms were built, not by reading documentation. I don't mean to harp on this, but I simply had no other path forward for creating my charm, there wasn't much to read. Again, this is something I'll be focusing on helping along, myself.

My other problems stem from the issues involved with this path forward and may be mitigated by utilizing the new services framework.

The hooks.py file is big, but there are enough hooks and enough code repetition that it wouldn't necessarily make sense to have it any other way. There are a few other charms that have gotten big enough to divide the deployment strategies into several different files and classes (notably the Juju GUI charm) in sensible ways. In the case of Warren, though there weren't obvious break points, and yet the file still feels relatively long.

What's next

In the next post, I'd like to go more in depth on the process of developing a charm. That means going into debug-hooks, juju ssh, and a few other commands that are useful for developing and debugging a charm.