At the heart of conjure-up are spells. They consist of all the information required to deploy your applications and walk the user through getting started as quickly as possible.

There are 2 main parts that make up a spell. The first being a Juju bundle, these are your blueprints for defining the amount of units for each application along with any default configuration options you wish to expose to the user for modification.

The second part is the additional data added to the spell to be conjure-up enabled. This contains your metadata which describes the spell and a set of steps that you define for a user to walkthrough. This allows you as a spell author to ensure that a user can learn about your application and at the end know exactly what to do next in order to make the most out of your application.

It is recommended to learn the Anatomy of a conjure-up spell in order to get started building your spell!

Anatomy of a conjure-up spell

We will start with the directory layout of a typical spell. Some of this will be familiar as we build on the existing Juju bundle specification.

my-super-application>
   - bundle.yaml (1)
   - README.md
   - metadata.yaml (2)
   - steps/ (3)
     - 00_deploy-done
     - 00_pre-deploy
     - step-01_my_first_step
     - step-01_my_first_step.yaml
1 If this file exist in the spell directory it will be used, otherwise a bundle-location is required. More information on creating Juju bundle files
2 Spell Metadata
3 Executable scripts to be run prior to deployment, to check when a deployment is finished, and steps to be processed for configuring deployment. See Steps for more information.

Spell Metadata

The spell metadata contains properties that are useful for conjure-up to process certain aspects of a spell. Available properties are as follows:

friendly-name

This is used during the spell selection within conjure-up.

cloud-whitelist

A list of Public clouds available for a spell.

cloud-blacklist

A list of Public clouds that are known to not work with a spell.

options-whitelist

Additional charm options to be exposed for editing within conjure-up.

bundle-location

A URL of the location of a bundle.yaml, ie https://api.jujucharms.com/charmstore/v5/wiki-scalable/archive/bundle.yaml

metadata.yaml example
cloud-whitelist:
- localhost
friendly-name: OpenStack with NovaLXD
options-whitelist:
  keystone:
  - admin-password
  lxd:
  - block-devices
  mysql:
  - max-connections
  neutron-gateway:
  - ext-port
  nova-compute:
  - virt-type

Steps

During a deployment, conjure-up will always check for and run a predefined set of scripts:

00_pre-deploy

This script contains necessary instructions for configuring a system prior to a deployment (00_pre-deploy).

00_deploy-done

This script will contain instructions for verifying that deployment has finished and is ready for its next steps (00_deploy-done).

step-##_a_step

This is an alpha numerical script that will be executed in order to perform any actions necessary to provide a turn key and usuable deployment.

00_pre-deploy

In some cases, like OpenStack with NovaLXD modifications need to be made to the LXD container profile to allow for certain kernel modules and networking to be enabled. This needs to happen prior to any applications being deployed in the container.

An example of 00_pre-deploy for OpenStack with NovaLXD
#!/usr/bin/env python3

import os
import time
from subprocess import run, PIPE, CalledProcessError
from conjureup.hooklib.writer import success, error, log

SCRIPTPATH = os.path.dirname(os.path.abspath(__file__))


provider_type = os.environ.get('JUJU_PROVIDERTYPE', None) (1)

if provider_type == "lxd": (2)
    log.debug("Running pre-deploy for OpenStack")
    # Give LXD enough time to learn about the profile
    time.sleep(5)
    try:
        profilename = run('juju switch | cut -d/ -f2',
                          shell=True,
                          stdout=PIPE,
                          stderr=PIPE)
        profilename = profilename.stdout.decode().strip()
    except CalledProcessError as e:
        error(e)

    log.debug("Processing lxd profile: {}".format(profilename))

    try:
        profile_edit = run(
            'sed "s/##MODEL##/{profile}/" '
            '{scriptpath}/lxd-profile.yaml | '
            'lxc profile edit "juju-{profile}"'.format(
                profile=profilename,
                scriptpath=SCRIPTPATH),
            shell=True,
            stdout=PIPE,
            stderr=PIPE) (3)
    except CalledProcessError as e:
        error(e)

    if profile_edit.returncode > 0:
        error(profile_edit.stderr)

success("Successful pre-deploy.") (4)
1 Is exposed as an environment variable to check the type of public cloud this script is running in.
2 Since we are doing this on a container it makes sense to only work with the LXD type.
3 This performs an inplace update of the LXD profile. Due to the nature of LXD this profile will be available immediately even on containers that have already started.
4 Helper function part of the builtin hooklib for writing steps. This lets conjure-up know that this pre-deploy task has completed without error.

00_deploy-done

Before we can process any additional steps we need to wait for all the deployed applications to become in a ready state. Below demonstrates a couple of ways to check for an error of the unit or machine:

An example of 00_deploy-done for OpenStack with NovaLXD
#!/usr/bin/env python3
from conjureup.hooklib.writer import success, fail, error, log
from conjureup.hooklib import juju

log.debug("Running deploy-done for OpenStack installation.")
agent_states = juju.agent_states() (1)

errored_units = [(unit_name, message) for unit_name, state, message
                 in agent_states if state == "error"] (2)
if len(errored_units) > 0:
    errs = "\n".join(["{}: {}".format(n, m) for n, m in errored_units])
    error('Deployment errors:\n{}'.format(errs))

machines = juju.machine_states()
errored_machines = [(name, err) for name, status, err in machines
                    if status == "error"] (3)
if len(errored_machines) > 0:
    errs = "\n".join(["{}: {}".format(n, m) for n, m in errored_machines])
    error("Machine creation errors:\n{}".format(errs))

if len(agent_states) == 0:
    fail('Applications are still deploying') (4)

if all([state == 'active' for _, state, _ in agent_states]):
    success('Applications are ready') (5)

fail('Applications not ready yet')
1 Part of the conjure-up hooklib for grabbing the status output from Juju. This is similar to running juju status --format yaml
2 Check for any Application units that have errored and trigger a failure in conjure-up to be seen by the user.
3 Check for any Machines that have errored and trigger a failure in conjure-up to be seen by the user.
4 This triggers a non fatal failure which lets conjure-up know that at least 1 or more applications are still pending.
5 All applications, units, and machines are in a successful state and which triggers conjure-up to move on to the Post processing

Post processing

There are 2 sections to post processing. The first section is the step metadata, this metadata provides conjure-up some context about what to display to the user for configuration and how to pass that information to the processing script.

Steps are created alpha numerically and have 2 files associated. The first file being the step script named step-01_keypair. The second file is the metadata for that step named step-01_keypair.yaml.

The metadata for a step consists of:

title

A short title of the step

description

A summary of what this steps does

viewable

Boolean to indicate if this steps summary and actions are seen within conjure-up

required

Boolean to indicate that this step is a requirement and has to be run

additional-input

Additional configuration variables that can be changed by the user within conjure-up [additional-input]

Additional Input

This section of the step describes the configuration object and how it is to be displayed to the user within conjure-up and how a step would utilize the result from the user input.

The additional input has the following properties:

label

Rendered label describing the input

key

The result of input is stored in this key which is exposed via environment variables

type

Type of input

default

Default value for input

A full example of step-01_keypair.yaml
title: SSH
description: |
  Import SSH keypairs into OpenStack. This allows you to access the newly deployed instances via SSH with your current user. If you are not sure about the location of a ssh key leave it as is and we will create one automatically.
viewable: True
required: True
additional-input:
  - label: SSH public key path
    key: SSHPUBLICKEY
    type: text
    default: ~/.ssh/id_rsa.pub
A full example of step-01_keypair script
#!/bin/bash

# Path to executing script
SCRIPT=$(realpath $0)

# Directory housing script
SCRIPTPATH=$(dirname $SCRIPT)

. $SCRIPTPATH/share/common.sh

_ssh_public_key=$(expandPath $SSHPUBLICKEY) (1)
debug "Environment Variables: $_ssh_public_key"

tmpfile=$(mktemp)
debug "Created tmpfile: $tmpfile"

cat <<EOF> $tmpfile
sudo apt update > /dev/null 2>&1
sudo apt -qyf install python3-openstackclient > /dev/null 2>&1
EOF

# write credentials
$SCRIPTPATH/share/novarc >> $tmpfile

# include lib
cat $SCRIPTPATH/share/common.sh >> $tmpfile

if [ ! -f $_ssh_public_key ]; then
    debug "Couldnt find $_ssh_public_key, attempting to create one: " ${_ssh_public_key%.*}
    ssh-keygen -N '' -f ${_ssh_public_key%.*} > /dev/null 2>&1
    debug "ssh-keygen result: $?"
fi

# Set ssh public key on controller
echo "export SSHPUBLICKEY=$HOME/.ssh/$(basename $_ssh_public_key)" >> $tmpfile

# write final script
cat $SCRIPTPATH/share/keypair.sh >> $tmpfile

debug "Creating .ssh directory on controller node"
juju ssh -m $JUJU_CONTROLLER:$JUJU_MODEL nova-cloud-controller/0 "mkdir -p ~/.ssh && chmod 700 ~/.ssh" (2)

debug "SCPing over ${_ssh_public_key%.*} to controller node"
juju scp -m $JUJU_CONTROLLER:$JUJU_MODEL ${_ssh_public_key%.*}* nova-cloud-controller/0:.ssh/.
debug "scp result: $?"

debug "SCPing over $tmpfile controller node"
juju scp -m $JUJU_CONTROLLER:$JUJU_MODEL $tmpfile nova-cloud-controller/0:keypair.sh
debug "scp result: $?"

juju ssh -m $JUJU_CONTROLLER:$JUJU_MODEL nova-cloud-controller/0 "bash keypair.sh"
1 This is the environment variable that was defined in the key section of step-01_keypair.yaml
2 JUJU_CONTROLLER and JUJU_MODEL are exposed through environments variables for all steps. These can be relied on to make sure you are always operating in the most current juju environment.

The full source for this spell can be found at our Github spells registry