The hook environment, hook tools and how hooks are run
When a charm is deployed onto a unit, the raw charm is extracted into a directory; this directory is known as the charm directory. It’s owned and operated by juju, and juju sometimes temporarily cedes control of it to user code, by running a hook inside it.
When a hook’s running, it should be considered to have sole access to the charm directory; at all other times, you should consider that juju may be making arbitrarily scary changes to the directory, and that it is not safe to read or write to anything in there at all.
This is to say that the software you install must, once it’s running, be entirely independent of the charm that created it. It’s fine (and encouraged, with some caveats) to store charm state in the charm directory, but the state of your software must remain unperturbed by direct changes to the charm.
So, every hook runs with easy access to the charm files. Every hook also runs as root, with a number of useful variables set, and has access to hook-specific tools that let you interrogate and affect the juju environment.
No more than one hook will execute on a given unit at a given time. A unit in a container is considered to be on a different system to any unit on the container’s host machine.
The following variables are always available.
$CHARM_DIRvariable is the path to the charm directory.
$PATHvariable is prefixed with the path to the hook tools directory.
$JUJU_UNIT_NAMEvariable holds the name of the unit.
$JUJU_API_ADDRESSESvariable holds a space-separated list of API server addresses.
$JUJU_AVAILABILITY_ZONEvariable holds the current availability zone the charm is running in (not all cloud providers support Availability Zones).
In addition, every relation hook makes available relation-specific variables:
$JUJU_RELATIONvariable holds the relation name. This information is of limited value, because it’s always the same as the part of the hook name just before “-relation-”.
$JUJU_RELATION_IDvariable holds an opaque relation identifier, used to distinguish between multiple relations with the same name. It is vitally important, because it’s the only reasonable way of telling the difference between (say) a database application’s many independent clients.
…and, if that relation hook is not a -broken hook:
$JUJU_REMOTE_UNITvariable holds the name of the unit which is being reported to have -joined, -changed, or -departed.
Juju does not pay any attention to the values of the above variables when running hook tools: they’re a one-way communication channel from juju to the charm only. Finally, in all cases:
$JUJU_CONTEXT_IDvariables allow the hook tools to work: juju does pay attention to them, but you should treat them as opaque and avoid messing with them.
juju run runs commands in a Juju context and sets a value for
$JUJU_CONTEXT_ID each time it is used. This is what enables you to run hook tools.
So, if you use
juju run on a system with a unit
haproxy/0, like this:
juju run --unit haproxy/0 'echo $JUJU_CONTEXT_ID'
A different output will be returned each time you run it, because each
juju run establishes a different context. The output format will be similar, but the context ID will change to indicate the new context created for that
Finally, if you’re debugging, you’ll also have access to:
$JUJU_HOOK_NAMEvariable, which will be set to the current hook name.
All hook tools are available in all hooks. Many of the tools produce output, and those that do accept a
--format flag whose value can be set to
yaml as desired. If it’s not specified, the format defaults to
smart, which transforms the basic output as follows:
- strings are left untouched
- boolean values are converted to the strings
- ints and floats are converted directly to strings
- lists of strings are converted to a single newline-separated string
- all other types (in general, dictionaries) are formatted as YAML
Tools which do not produce output also accept the
--format flag, but ignore it, for compatibility reasons.
The various “relation-” tools infer context from the hook where possible. If they’re running in a relation hook, the current relation identifier is set as the default; and if they’re running in a -joined, -changed, or -broken hook, the current remote unit is set as the default.
To use relation hooks effectively, you should spend time making sure you understand the relation model.
Remember that all commands that produce output accept
--format json and
--format yaml, and you may consider it smarter to use those for clarity’s sake than to depend on the default
juju-log writes its arguments directly to the unit’s log file. All hook output is currently logged anyway, though this may not always be the case - If it’s important, please
juju-log "some important text"
This tool accepts a
--debug flag which causes the message to be logged at
DEBUG level; in all other cases it’s logged at
There are several cases where a charm needs to reboot a machine, such as after a kernel upgrade, or to upgrade the entire system. The charm may not be able to complete the hook until the machine is rebooted.
The juju-reboot command allows charm authors to schedule a reboot from inside a charm hook. The reboot will only happen if the hook completes without error. You can schedule a reboot like so:
--now option can be passed to block hook execution. in this case the
juju-reboot command will hang until the unit agent stops the hook and re-queues it for the next run. This will allow you to create multi-step install hooks.
Charm authors must wrap calls to juju-reboot to ensure it is actually necessary, otherwise the charm risks entering a reboot loop. The preferred work-flow is to check if the feature/charm is in the desired state, and reboot when needed. This bash example assumes that “$FEATURE_IS_INSTALLED” variable was defined by a check for the feature, then ‘juju-reboot’ is called if the variable is false:
if [[ $FEATURE_IS_INSTALLED == "false" ]] then install_feature juju-reboot --now fi
juju-reboot command can be called from any hook. It can also be called using the
juju run command.
unit-get returns information about the local unit. It accepts a single argument, which must be
public-address. It is not affected by context:
unit-get private-address 10.0.1.101 unit-get public-address foo.example.com
config-get returns information about the application configuration (as defined by the charm). If called without arguments, it returns a dictionary containing all config settings that are either explicitly set, or which have a non-nil default value. If the
--all flag is passed, it returns a dictionary containing all defined config settings including nil values (for those without defaults). If called with a single argument, it returns the value of that config key. Missing config keys are reported as having a value of nil, and do not return an error.
Getting the interesting bits of the config is done with:
config-get key: some-value another-key: default-value
To get the whole config including the nulls:
config-get --all key: some-value another-key: default-value no-default: null
To retrieve a specific value pass its key as argument:
config-get [key] some-value
This command will also work if no value is set and no default is set or even if the setting doesn’t exist. In both cases nothing will be returned.
config-get [key-with-no-default] config-get [missing-key]
The above two examples are not misprints - asking for a value which doesn’t exist or has not been set returns nothing and raises no errors.
open-port marks a port or range of ports on the local system as appropriate to open, if and when the application is exposed to the outside world. It accepts a single port or range of ports with an optional protocol, which may be
tcp is the default.
Open 80/tcp if and when the application is exposed:
Open 1234/udp if and when the application is exposed:
Open the range 8000 to 8080:
open-port will not have any effect if the application is not exposed, and may have a somewhat delayed effect even if it is. This operation is transactional, so changes will certainly not be made unless the hook exits successfully.
Juju also tracks ports opened across the machine and will not allow conflicts - if another charm has already opened the port (or one or more ports in a range) you have specified, your request will be ignored.
This command accepts and ignores
--format for compatibility purposes, but it doesn’t produce any output.
close-port unmarks a local system port. If the application is not exposed, it has no effect; otherwise the port is marked for imminent closure. It accepts the same flags and arguments as
Close 1234/udp if it was open:
Close port 80 if it was open:
Close a range of ports:
The opened-ports hook tool lists all the ports currently opened by the running charm. It does not, at the moment, include ports which may be opened by other charms co-hosted on the same machine lp#1427770.
The command returns a list of one port or range of ports per line, with the port number followed by the protocol (tcp or udp).
For example, running
opened-ports may return:
Opening ports is transactional (i.e. will take place on successfully exiting the current hook), and therefore
opened-ports will not return any values for pending
open-port operations run from within the same hook.
relation-set writes the local unit’s settings for some relation. It accepts any number of
key=value strings, and an optional
-r argument, which defaults to the current relation identifier. If it’s not running in a relation hook,
-r needs to be specified. The
value part of an argument is not inspected, and is stored directly as a string. Setting an empty string causes the setting to be removed.
Setting a pair of values for the local unit in the default relation identifier which is stored in the environment variable
echo $JUJU_RELATION_ID server:3
The setting is done with:
relation-set username=bob password=2db673e81ffa264c
To set the pair of values for the local unit in a specific relation specify the relation identifier:
relation-set -r server:3 username=jim password=12345
To clear a value for the local unit in the default relation enter:
relation-set is the single tool at your disposal for communicating your own configuration to units of related applications. At least by convention, the charm that
provides an interface is likely to set values, and a charm that
requires that interface will read them; but there’s nothing forcing this. Whatever information you need to propagate for the remote charm to work must be propagated via relation-set, with the single exception of the
private-address key, which is always set before the unit joins.
You may wish to overwrite the
private-address setting, for example if you’re writing a charm that serves as a proxy for some external application; but you should in general avoid removing that key, because most charms expect that value to exist unconditionally.
All values set are stored locally until the hook completes; at that point, if the hook exit code is 0, all changed values will be communicated to the rest of the system, causing -changed hooks to run in all related units.
There is no way to write settings for any unit other than the local unit; but any hook on the local unit can write settings for any relation the local unit is participating in.
relation-get reads the settings of the local unit, or of any remote unit, in a given relation (set with
-r, defaulting to the current relation identifier, as in
relation-set). The first argument specifies the settings key, and the second the remote unit, which may be omitted if a default is available (that is, when running a relation hook other than -broken).
If the first argument is omitted, a dictionary of all current keys and values will be printed; all values are always plain strings without any interpretation. If you need to specify a remote unit but want to see all settings, use
- for the first argument.
The environment variable
JUJU_REMOTE_UNIT stores the default remote unit:
echo $JUJU_REMOTE_UNIT mongodb/2
Getting the settings of the default unit in the default relation is done with:
relation-get username: jim password: "12345"
To get a specific setting from the default remote unit in the default relation you would instead use:
relation-get username jim
To get all settings from a particular remote unit in a particular relation you specify them together with the command:
relation-get -r database:7 - mongodb/5 username: bob password: 2db673e81ffa264c
relation-get produces results that are consistent but not necessarily accurate, in that you will always see settings that:
- were accurate at some point in the reasonably recent past
- are always the same within a single hook run…
except when inspecting the unit’s own relation settings, in which case local changes from
relation-setwill be seen correctly.
You should never depend upon the presence of any given key in
relation-get output. Processing that depends on specific values (other than
private-address) should be restricted to -changed hooks for the relevant unit, and the absence of a remote unit’s value should never be treated as an error in the local unit.
In practice, it is common and encouraged for -relation-changed hooks to exit early, without error, after inspecting
relation-get output and determining it to be inadequate; and for all other hooks to be resilient in the face of missing keys, such that -relation-changed hooks will be sufficient to complete all configuration that depends on remote unit settings.
Settings for remote units already known to have departed remain accessible for the lifetime of the relation.
relation-get currently has a bug that allows units of the same application to see each other’s settings outside of a peer relation. Depending on this behaviour inadvisable: if you need to share settings between units of the same application, always use a peer relation to do so, or you may be seriously inconvenienced when the hole is closed without notice.
relation-list outputs a list of all the related units for a relation identifier. If not running in a relation hook context,
-r needs to be specified with a relation identifier similar to the
To show all remote units for the current relation identifier:
Which should return something similar to:
mongodb/0 mongodb/2 mongodb/3
All remote units in a specific relation identifier can be shown with:
relation-list -r website:2 haproxy/0
relation-ids outputs a list of the related applications with a relation name. Accepts a single argument (relation-name) which, in a relation hook, defaults to the name of the current relation. The output is useful as input to the
relation-set commands to read or write other relation values.
The current relation name is stored in the environment variable
JUJU_RELATION. All “server” relation identifiers can be shown with:
relation-ids server:1 server:7 server:9
To show all relation identifiers with a different name pass it as an argument, for example:
relation-ids reverseproxy reverseproxy:3
juju run with
relation-ids to see all the reverse proxy relations for an haproxy unit, for example, use:
juju run --unit haproxy/0 'relation-ids reverseproxy'
Which returns output like this:
reverseproxy:110 reverseproxy:111 reverseproxy:112 reverseproxy:113 reverseproxy:114 reverseproxy:115 reverseproxy:116 reverseproxy:117 reverseproxy:118 reverseproxy:119
You can use this output with
relation-list to see the units for a given relation-id, like this:
juju run --unit haproxy/0 'relation-list -r reverseproxy:115'
Which will return the unit(s) for the given ID, like this:
In this example, only one unit was returned in the output,
kibana/2 because the Kibana application only has one unit. If more than one unit existed, all would be listed in the output.
You can use this output with
relation-get to find the relation data for each unit, like this:
juju run --unit haproxy/0 'relation-get -r reverseproxy:115 - kibana/2'
This returns the data that the Kibana charm in our example is sending to the haproxy charm when creating the relation:
host: 10.142.0.7 port: "80" private-address: 10.142.0.7
Introduced in version 1.24 of Juju, a new status mechanism allows Juju and its charms to more accurately reflect their current status. This places the responsibility on the charm to know its status, and set it accordingly using the
status-set hook tool. This hook tool takes 2 arguments. The first is the status to report, which can be one of the following:
- maintenance (the unit is not currently providing a application, but expects to be soon, E.g. when first installing)
- blocked (the unit cannot continue without user input)
- waiting (the unit itself is not in error and requires no intervention, but it is not currently in service as it depends on some external factor, e.g. an application to which it is related is not running)
- active (This unit believes it is correctly offering all the software it is primarily installed to provide)
For more extensive explanations of these statuses, and other possible status values which may be set by Juju itself, please see the status reference page.
The second argument is a user-facing message, which will be displayed to any users viewing the status, and will also be visible in the status history. This can contain any useful information.
This status message provides valuable feedback to the user about what is happening. Changes in the status message are not broadcast to peers and counterpart units - they are for the benefit of humans only, so tools representing Juju applications (e.g. the Juju GUI) should check occasionally and be told the current status message.
Spamming the status with many changes per second would not be welcome (and might be throttled by the state server). Nevertheless, a thoughtful charm will provide appropriate and timely feedback for human users, with estimated times of completion of long-running status changes, for example.
In the case of a
blocked status though the status message should tell the user explicitly how to unblock the unit insofar as possible, as this is primary way of indicating any action to be taken (and may be surfaced by other tools using Juju, e.g. the Juju GUI).
A unit in the
active state with should not generally expect anyone to look at its status message, and often it is better not to set one at all. In the event of a degradation of service, this is a good place to surface an explanation for the degradation (load, hardware failure or other issue).
A unit in
error state will have a message that is set by Juju and not the charm because the error state represents a crash in a charm hook - an unmanaged and uninterpretable situation. Juju will set the message to be a reflection of the hook which crashed. For example “Crashed installing the software” for an install hook crash, or “Crash establishing database link” for a crash in a relationship hook.
status-set maintenance "installing software" status-set maintenance "formatting storage space, time left: 120s" status-set waiting "waiting for database" status-set active status-set active "Storage 95% full" status-set blocked "Need a database relation" status-set blocked "Storage full"
status-get hook tool allows a charm to query what is recorded in Juju as the current workload status. Without arguments, it just prints the workload status value e.g. ‘maintenance’. With
--include-data specified, it prints YAML which contains the status value plus any data associated with the status.
status-get status-get --include-data