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.

Environment variables

The following variables are always available.

  • The $CHARM_DIR variable is the path to the charm directory.
  • The $PATH variable is prefixed with the path to the hook tools directory.
  • The $JUJU_UNIT_NAME variable holds the name of the unit.
  • The $JUJU_API_ADDRESSES variable holds a space-separated list of API server addresses.
  • The $JUJU_AVAILABILITY_ZONE variable 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:

  • The $JUJU_RELATION variable 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-”.
  • The $JUJU_RELATION_ID variable 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:

  • The $JUJU_REMOTE_UNIT variable 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:

  • The $JUJU_AGENT_SOCKET and $JUJU_CONTEXT_ID variables 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 juju run.

haproxy/0-juju-run-9100131373820979914

Finally, if you’re debugging, you’ll also have access to:

  • The $JUJU_HOOK_NAME variable, which will be set to the current hook name.

Hook tools

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 json or 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 True and False
  • 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 smart format.

juju-log

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 it.

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 INFO level.

juju-reboot [--now]

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:

juju-reboot

The --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

The juju-reboot command can be called from any hook. It can also be called using the juju run command.

unit-get

unit-get returns information about the local unit. It accepts a single argument, which must be private-address or 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

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

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 udp or tcp, where tcp is the default.

Examples:

Open 80/tcp if and when the application is exposed:

open-port 80

Open 1234/udp if and when the application is exposed:

open-port 1234/udp

Open the range 8000 to 8080:

open 8000-8080/tcp

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

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 open-port.

Examples:

Close 1234/udp if it was open:

close-port 1234/udp

Close port 80 if it was open:

close-port 80

Close a range of ports:

close-port 80-100

opened-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:

70-80/tcp
81/tcp

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

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.

Examples:

Setting a pair of values for the local unit in the default relation identifier which is stored in the environment variable JUJU_RELATION_ID:

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 deprecated-or-unused=

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

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

Note that 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-set will 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

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 therelation-get and relation-set commands.

Examples:

To show all remote units for the current relation identifier:

relation-list

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

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-list, relation-get, and relation-set commands to read or write other relation values.

Examples:

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

To use 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:

kibana/2

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

status-set

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.

Examples:

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

The 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.

Examples:

status-get
status-get --include-data
2 Likes

We’ll want to update this document for application relation data. Several items

JUJU_REMOTE_APP
relation-get --app
relation-set --app

1 Like

As I was exploring this myself, I must have ended up in a bug (I think?) … the permission denied we not in the manual =D

juju run --unit haproxy/0 'relation-get -r reverseproxy:1 - microservice/0 --format yaml'
ERROR permission denied

I’m OK with removing this post if it turns out to be a bug somewhere and happy to post a bug report.

lxc version:
Client version: 3.18
Server version: 3.18
Juju version: 2.6.10-eoan-amd64

Are you sure that the relationship between reverse proxy and ‘microserver’ is relation-id 1? That sounds like you are asking about a unit that isn’t in the relation.
You’d have to run “relation-ids” and “relation-list” to make sure about the identifiers.

1 Like
$ juju run --unit haproxy/0 'relation-get -r reverseproxy:1 - microservice/0 --format yaml'
ERROR permission denied
$ juju run --unit haproxy/0 'relation-get -r reverseproxy:1 - microsample/0 --format yaml'
egress-subnets: 10.113.159.165/32
hostname: 10.113.159.165
ingress-address: 10.113.159.165
port: "8080"
private-address: 10.113.159.165

So, thanx to @jameinel I learned that getting the correct name of things are important. I was trying to access data from a unit that simply didn’t exists by that name. microservice != microsample.

I’m leaving this conversation for all that might end up like me.

1 Like

How can I, outside of a *-relation-* hook, do a relation-get --app to get the app data set by either myself or the remote app? For example I get a permission denied in this example, where I am trying to get the relation values that the lizardfs-master app has set from itself.

juju run --unit lizardfs-master/5 'relation-get --app -r master:13 - $JUJU_UNIT_NAME

The lizardfs-master app has set relation app relation values for the master:13 relation by using relation-set --app inside of a relation hook, and the code above was my attempt at getting that data outside of a relation hook.

The only way that I’ve managed to get application data was inside of a relation hook with realation-get --app key and no other arguments.

A unit that is not the leader is not allowed to read the values for this app. This is because there are issues with exposing values that don’t have associated events, and race issues even if you expose those values with events. If you want to share information from a lead unit to the other units, use a peer relation. (all units can read the app data of a peer relation, because that is ‘what you are related to’.)
eg:
application A related to B.
A/* can read the app data from B
only the lead unit of A can read/write to the app data from A to B.
B/* can read the app data from B.
Only the lead B unit can read/write the app data from B for A.
If A has a peer relation, then the lead unit can set the app data for A to itself, but all units can read the app data for A.

Does that make sense?
It is a little unintuitive that peers cannot read the data A is sending to B. But if they did, then every change the lead unit of A makes would need to wake up all units of B and all units of A to inform them that the data has changed. (one of the goals of application data was to reduce the total number of events things had to respond to.) However, if you were sharing something like the password-for-B to all the units of A, there is no ordering between the events that A would have gotten, and the events for B. So if A/leader set the password, you might see B/1 get the new password, then A/2 would see it, then B/3, etc. Which means that B/1 might try to talk to A/1 with the new password, but A/1 hasn’t seen it yet.
If you do want A coordinating with A, then A/leader can set the password in a peer relation, and then when A/1 reads that password, it can set it in the A/1 unit data bag for B. Then when B/2 sees the password from A/1 it knows that it can talk to A/1.

1 Like

OK, that makes sense, thanks. :slight_smile:

The juju-reboot command can be called from any hook. It can also be called using the juju run command.

When I try this hook from the terminal, to reboot one of my machines:

$ juju run --unit influxdb/1 juju-reboot
ERROR juju-reboot is not supported when running an action.

And the unit does not reboot. Was this hook deprecated?

I use juju run --unit influxdb/1 -- sudo reboot instead, but the command always times out. Is there a better way to reboot a machine deployed via Juju?

Edit: just found out that juju ssh influxdb/1 sudo reboot works better, as the Juju command does not block. The machine reboots, the SSH connection is closed, and the terminal is free to use.

is there no way for a Juju action… to modify the config ? I can config-get … but I notice a lack of config-set

I use this when setting the charms underlying binary version… and the action simply allows me to change whatever version of the app is installed… but now the config state will be stale… so I want to update it… but seems I cannot or should not ?

Hey! No, charms cannot modify their own config. The reason is that it could lead to very confusing results for an admin - imagine you set some config on a charm, only to find that it has later changed without you knowing? :sweat_smile:

I wouldn’t generally encourage allowing users to set an installed version of software - it opens you up to bugs where they possibly select a version you haven’t tested with the charm code.

I would consider instead releasing the charm multiple times in different tracks for the different supported workload versions. For example 1.25/edge, 2.0/stable, etc.

2 Likes