Proposed API change: Enhanced support for working with relations

Jim Baker jim.baker at canonical.com
Wed Feb 29 21:49:14 UTC 2012


I would appreciate any comments on this proposed API change. Please
note, this specification can be reviewed here:
https://codereview.appspot.com/5714043/
<https://codereview.appspot.com/5714043/>


Enhanced support for working with relations
===========================================

Overview
--------

Relations are a core aspect of Juju. Thus working with relations in a
more flexible fashion is a topic seen in a number of bugs, mailing
list discussions, and IRC chats. This proposal pulls together a number
of enhancements, backed by use cases from bug reports, to enhance
Juju's current relation support:

 * Support relation id and in general enhance the ability to refer to
   relations.

 * Enable `relation-get`, `relation-set`, `relation-list` to work in
   any hook for any relation.

 * Add `juju do` command for out of band execution of hook commands.

 * Enhance the output of `juju status` with additional relation
   information, which also fixes a bug related to more complex
   relationships where the same relation name is used more than once.


Relation references
-------------------

A number of important use cases in Juju require explicit reference to
a relation. Such references should be supported by either using the
relation name or the relation id:

  * The **relation name** is currently visible in Juju: it's an
    important component of relation hook script names and
    corresponding `metadata.yaml`, and it's made available as the
    environment variable `$JUJU_RELATION` to relation hook scripts.

  * Currently the **relation id** is not visible to user scripts, but
    an internal version is used by Juju itself.


Relation id format
~~~~~~~~~~~~~~~~~~

One issue that needs to be resolved is the format of the relation
id. There are a number of suggestions from the bug reports:

  * Normalize the internal relation id in a manner similiar to what is
    done with service ids. That is, ``relation-0000000042`` would be
    represented as ``relation-42``. However, it's worth noting that
    such service ids are not currently exposed outside of the Python
    API, so it's not much of a precedent. `Bug #791370
    https://bugs.launchpad.net/juju/+bug/791370`_ suggests this
    format.

  * Concatenate the services of the relation, along with the
    normalized internal relation id: ``<service name>-<service
    name>-<normalized id>``.  This format will facilitate looking at
    the relation id and discerning it represents an edge in the
    topology. Example: ``wordpress-varnish-42``. Peers would only use
    one service name, such as ``riak-3``. (Mentioned by `bug #767195
    https://bugs.launchpad.net/juju/+bug/767195`_.)

  * Use the interface name with the normalized internal id,
    ``<interface name>-<relation id>``. Example: ``http-42``. This
    format was mentioned in `bug #767195
    https://bugs.launchpad.net/juju/+bug/767195`_.

This proposal assumes the first format; see implementation details on
why this could work better.

Regardless of format, the internal id is never reused, so the external
relation id would be guaranteed to be unique for the lifetime of the
environment, regardless of the naming scheme choice.


New environment variable `JUJU_RELATION_ID`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The **implied relation** is currently named by `JUJU_RELATION`; it is
only available in relation hooks. In addition, such relation hooks
will have a new environment variable, `JUJU_RELATION_ID`, defined when
executed.


Commands to work with relation settings and membership
------------------------------------------------------

The hook commands working with relation settings (`relation-get`,
`relation-set`, and `relation-list`) are modified to enable their use
with other relations than the implied relation and also for other
hooks besides relation hooks::

   relation-get [-r RELATION_NAME] [--relation-id RELATION_ID]
                [-|SETTING] [UNIT_NAME]

   relation-set [-r RELATION_NAME] [--relation-id RELATION_ID]
                [SETTING=VALUE [SETTING=VALUE ...]] [UNIT_NAME]

The `-r` option is short for `--relation-name`. It is an error to
specify both `--relation-id` and `--relation-name`. When not running
in the context of a relation hook and its implied relation, it is
necessary to specify a relation. For `--relation-name`, the selected
relation will be respect to `UNIT_NAME`, which defaults to the unit
running this hook.

Example. To get the user name from the relation named `db` from
`mysql/0` from any hook::

   user=`relation-get -r db user mysql/0`

As with the other relation hook commands, the `relation-list` command
also can be used out of a relation hook and with other relations. This
command is further augmented to support listing only relations that
support a given interface, role, or scope::

   relation-list [-r RELATION_NAME] [--relation-id RELATION_ID]
                 [--format FORMAT] [--type TYPE]
                 [--interface INTERFACE] [--role ROLE] [--scope SCOPE]
                 [UNIT_NAME]

With no options, `relation-list` returns a whitespace separated list
of service units for the implied relation, excluding the unit running
this hook (`UNIT_NAME` defaults to this unit). This is the current
behavior.

Use the `--format` option with `json` or `yaml` to return the complete
relational adjacency for `UNIT_NAME`. Example::

    relation-0:
      interface: mysql
      services:
        mysql:
          relation-name: server
          role: provides
          units: [mysql/0]
        blog-0:
          relation-name: db
          role: consumes
          units: [blog-0/0, blog-0/1]
    relation-1:
      interface: mysql
      ...

(Here there is no filtering on unit membership to exclude the local
unit.)

Otherwise `FORMAT` defaults to `shell`, which is a whitespace
separated list to simplify usage in shell scripts. Use `--type` to
specify the desired type of item to return, either `relation-id`,
`relation-name`, `service`, or `unit`, with the default being
`unit`. The `--type` option is ignored if the output format is YAML or
JSON.

Relations can be filtered by using `--interface`, `--role`, and
`--scope` options. The `INTERFACE` is the name of the interface, as
specified in `metadata.yaml`. `ROLE` can be `provides`, `consumes`, or
`peers`; it defaults to all of them. `SCOPE` is `global` or
`container`, defaulting to all.

A specific relation can be listed by using `--relation-id` or
`--relation-name` (both options cannot be specified). If `UNIT_NAME`
does not actually have the relation by `--relation-id` or
`--relation-name`, then it is an error.

This generalization of `relation-list` makes it feasible for a bash
script to start with a given service unit and then traverse the
topology to all other service units, without requiring any complex
parsing by the bash script itself. In comparison, scripts written in
Python or other languages can access the relation information by
parsing the JSON/YAML output as desired.

Example. The keystone charm has the following definition in its
`metadata.yaml`::

  name: keystone
  summary: Proposed OpenStack identity service - Daemons
  description: |
    Proposed OpenStack identity service - Daemons
  provides:
    identity-service:
      interface: keystone
    keystone-service:
      interface: keystone
  requires:
    shared-db:
      interface: mysql-shared

For a keystone service unit, `relation-list` can be used to enumerate
all relations that provide the keystone interface, then set the ready
setting. Setting this value will then trigger `<relation
name>-relation-changed` hooks for service units on the opposite side
of these relations::

  for relation_id in $(relation-list --interface=keystone
--type=relation-id); do
    relation-set --relation-id=$relation_id ready=true
  done

Alternatively, this specific triggering could be initiated in this
example as follows::

  relation-set -r identity-service ready=true
  relation-set -r keystone-service ready=true

However, the loop above is more general and could be executed by a
charm that does not know specifics of the keystone charm
implementation on an arbitrary unit running keystone.

For all the relational hook commands (`relation-get`, `relation-set`,
`relation-list`), the following guarantees apply for non-implied
relations when run from any hook:

  * Referring to a relation causes its hook context to be instantiated
    and cached. Subsequent reads of settings and the relation listing
    will return the same values for the duration of the hook calling
    these hook commands ("consistency").

  * If the relation does not exist at the time of read/caching, an
    error is raised stating "Relation not found".

  * If the hook exits with status code 0, the hook contexts are then
    written into ZooKeeper, one at a time. In between this write and
    the initial read, it is possible for the relation to have been
    removed. However, this will be quietly ignored, as is the current
    case in say exiting from `<relation name>-relation-changed`.

    (Implementation note: this current behavior is due to the fact
    that such nodes are not garbage-collected from ZooKeeper, even if
    the separate topology node itself has been changed.)

    Note this is different than the special context seen in for broken
    hooks for the implied relation, which do allow for reading the
    local unit settings, but returns an empty membership and prohibits
    other gets/sets.

The current behavior for the implied relation of relation hooks is
that the relation context is read prior to the invocation of the
corresponding hook script; this proposal does not alter this behavior.


New `juju do` subcommand for out of band execution
--------------------------------------------------

Out of band execution of hook commands, which was raised by `mailing
list discussion
https://lists.ubuntu.com/archives/juju/2012-February/001259.html`_,
can be implmented by adding a new subcommand to the Juju CLI:

  `juju do [command [args]]`- Runs a command as if it were a
  non-relational hook script.

(There are a number of other names for this subcommand, including
`sudo` or `run-as-hook`.)

Note that no arguments are assumed in the command or a script invoked
by the command, so if using a relation hook command like relation-get,
these must be fully specified.

Some notes:
 
  * All hook commands are available. If the command run by `juju do`
    exits with 0, any changes to relation settings are then written.

  * `juju do` executes its command/script locally. This subcommand is
    not a remote hook execution facility. (Use `juju ssh` for that.)
    `juju do` may be executed on Juju machines (so wherever a unit
    agent runs), or by an admin on their local machine using the Juju
    CLI.

  * As with other parts of Juju, appropriate ACLs should be defined.
    However, such security is an orthogonal concern and is thus not
    part of this proposal's scope.

  * No restriction is made against the nesting of `juju do`, as
    perhaps could be seen in a complex script, but such usage may
    cause unexpected behavior.

  * The desired local unit name must be specified when working with
    relation hook commands for most usage. One exception is
    ``relation-list --relation-id=relation-1``, since this is
    sufficient to do the lookup.
 
  * The command being invoked is not itself interpreted in a shell
    context.

Examples. Get the user name for `mysql/0` from the relation named `db`:

   user=`juju do relation-get --relation db user mysql/0`

Force a port to open as if it had run in a hook on `wordpress/0`, then
expose the service (as normally done) from the Juju CLI::

   juju do open-port 80/tcp wordpress/0
   juju expose wordpress

Touch a dummy setting on one of the mysql service units::

   juju do relation-set --relation db dummy mysql/0

Assuming it's related to wordpress, which is also using `db` as the
relation name, this setting will trigger the `db-relation-changed`
hooks for each of the wordpress service units.

Run a script, that for example, touches a dummy setting for every
relation on every service unit. Naturally, this script could create a
storm of activity by triggering the execution of `<relation
name>-relation-changed` hooks on all these service units::

   juju do rerun-relation-changed-hooks.sh

Such a script would use the output of `juju status`, as augmented in
the next section of the proposal, to list all of the relations to be
so touched.


Relation information in ``juju status``
---------------------------------------

Except for the relation name, information about relations is currently
unavailable in ``juju status``.

In particular, assuming the first relation id formats, the following
is representative of the available information in the Juju topology
about the relation between two services (using external ids)::

  relation-0:
  - mysql
  - wordpress: {name: db, role: client}
    mysql: {name: server, role: server}

So for `relation-0`, it uses the mysql interace; from the wordpress
side, it has the relation name of `db`; from mysql, `server`.  In
addition, the `client` role corresponds to `requires` (which of course
really means `consumes`) and the `server` role to `provides` in the
`metadata.yaml`. It's not seen here, but `peer` would be used for a
`peer` relationship.

In augmenting ``juju status`` output, a new top-level key of
``relations`` is used. Its value is a map of relation ids to the
relation information. For example, if current status output is ::

  services:
    mysql:
      charm: local:oneiric/mysql-11
      relations: {db: wordpress}
      units:
        mysql/0:
          machine: 1
          relations:
            db: {state: up}
          state: started
          ...

then this would be augmented as follows::

  relations:
    relation-0:
      interface: mysql
      services:
        mysql:
          relation-name: server
          role: provides
          units:
            mysql/0:
          state: up
        blog-0:
          relation-name: db
          role: consumes
          units:
            blog-0/0:
              state: up
            blog-0/1:
              state: up
    relation-1:
      interface: mysql
      services:
        mysql:
          relation-name: server
          role: provides
          units:
            mysql/0:
          state: up
        blog-1:
          relation-name: db
          role: consumes
          units:
            blog-1/0:
              state: up
            blog-1/1:
              state: start

This makes the output of status much more lengthy, but it does enable
being able to get the status for more interesting setups where a mysql
service (or a keystone or similar hub service) provide the same
interface over a relation name, but to different established
relations. This is a bug in Juju: currently in `juju status`, the
implementation will collect status information such that only the
*last* relation iterated over for a service is recorded for a given
relation name.


Use cases
---------

The following use cases are derived from the feature requests of the
following bug reports and mailing list discussion:

 * https://bugs.launchpad.net/juju/+bug/726467
 * https://bugs.launchpad.net/juju/+bug/731532
 * https://bugs.launchpad.net/juju/+bug/767195
 * https://bugs.launchpad.net/juju/+bug/791042
 * https://bugs.launchpad.net/juju/+bug/791370
 * https://bugs.launchpad.net/juju/+bug/797241
 * https://bugs.launchpad.net/juju/+bug/873116
 * https://lists.ubuntu.com/archives/juju/2011-December/001125.html
 * https://lists.ubuntu.com/archives/juju/2012-February/001258.html


Relation membership and settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`Bug #726467 https://bugs.launchpad.net/juju/+bug/726467`_ presents
this scenario: the wordpress charm needs to check its dependent
relations before negotiating settings with its haproxy relation. This
can be enabled by `relation-list` working with with other relations
from the unit executing this hook.

Generally this checking would then also use `relation-get` with other
relations, as requested by `bug #731532
https://bugs.launchpad.net/juju/+bug/731532`_.

A similar bug (`#767195 https://bugs.launchpad.net/juju/+bug/767195`_)
requests that hooks must be able to enumerate and query relations.


Using relation hooks in nonrelational hooks
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`Bug #873116 https://bugs.launchpad.net/juju/+bug/873116`_ describes
the scenario that the `upgrade-charm` hook needs to support a
mechanism to "refresh the relationships" after deploying a new
charm. Being able to use `relation-set` in any hook, on any relation
for any unit, can support this. This functionality removes the need to
remove and re-create relationships following `juju upgrade-charm`.


Out of band hook execution
~~~~~~~~~~~~~~~~~~~~~~~~~~

Two recent queries on the mailing list (`Triggering relation events?
https://lists.ubuntu.com/archives/juju/2011-December/001125.html`_ and
`Relation ordering: Does it matter?
https://lists.ubuntu.com/archives/juju/2012-February/001258.html`_)
bring up the need to enable triggering Juju to re-run relation
hooks. For example, "Upon establishing the database relation, there is
nothing to signal to juju that it should try again, the
keystone<->compute relationship is broken until an administrator uses
'juju resolved ...'."

The natural way to do this in Juju is simple: simply set a relation
setting and the opposite side will run its `<relation
name>-relation-changed` hook. Out of band execution enables this in
cron, by the Juju admin, or any other entity that needs to make this
new information visible.


Determining the service in a broken relation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

`Bug #791042 https://bugs.launchpad.net/juju/+bug/791042`_ describes
the following scenario. The mysql charm should be able to record the
fact that a relation had been broken for a given service. If the
relation is subsequently re-added, the code path would be slightly
different since the database already existed.

The solution is for the charm in the `db-relation-joined` hook to look
up the service by using `relation-list`, then store it in a local file
along with the corresponding relation id::

  service=`relation-list --relation-id=$JUJU_RELATION_ID --role=consumes
--type=service`

This takes advantage of the fact that relation hooks always have a
unique relation id in the `JUJU_RELATION_ID` environment variable, which
can also be distinguished from a re-added relation.

A subsequent `db-relation-joined` (or `-changed`) hook can then use
this alternative code path.


Inspecting relation settings
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For `bug #797241 https://bugs.launchpad.net/juju/+bug/797241`_, the
admin should be able to inspect relation settings. An alternative was
proposed (`juju inspect service_name`, etc.), however this
functionality can be readily accomplished by using `juju do`.

For example, to print all the relation data, by relation name for a
service unit, we need code like the following in a hypothetical
`inspect.sh`::

  for relation_name in $(relation-list --type=relation-name $service); do
    echo $relation_name `relation-get --relation-name=$relation_name -
$unit`
  done

Then just use this at the command line::

  $ juju do inspect.sh mysql/0


Implementation plan
-------------------

Branches are required for the following features; these may be further
subdivided:

  * Relation id support
  * Relation hook contexts can be looked up for a relation id, are
    cached, and properly written
  * Augment `relation-get`, `relation-set`, `relation-list` to take
    options (each of these will be separate branches)
  * Implement `juju do` subcommand
  * Add relations key to status collection


Implementation details
----------------------

Relation id format
~~~~~~~~~~~~~~~~~~

From an implementation perspective, there's no difficulty in
constructing a relation id for the first proposed format when the
relation is remove. Other formats need information that will be
removed from ZooKeeper, even when the relation id is known. This
situation can be acceptable: `RELATION_NAME` is currently provided by
the `<relation name>-relation-broken` hook, and this is feasible since
it is stored in memory in the lifecycle executing hooks. But it's
worth bearing in mind that the other formats do have a potential
problem.


Looking up relation states
~~~~~~~~~~~~~~~~~~~~~~~~~~

The following keyword args needs to be added to
`RelationStateManager.get_relation_state` to get the desired
`RelationState`:

  * unit_id, relation_name

  * relation_id

With this approach, `get_relation_state` can exclusively take
endpoints (starargs) or the unit_id, relation_name pair or the
relation_id. This approach seems to be more fluent than adding two new
methods with lengthy names like
`get_relation_state_from_unit_id_relation_name`.


Adding JUJU_RELATION_ID environment variable
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The method `RelationInvoker.get_environment_from_change` needs to be
modified to add `JUJU_RELATION_ID` as an environment variable. This
requires adding the relation id in the `RelationChange`. In turn, this
simply needs to be passed through when constructed by `HookScheduler`,
which can look it up with `RelationStateManager.get_relation_state`
since the scheduler currently maintains fields for the unit id and
relation name.


Relation hook commands
~~~~~~~~~~~~~~~~~~~~~~

Support for looking up relation state is also used by the methods
`relation_get`, `relation_set`, and `relation_list` of
`UnitAgentServer` to construct (and cache for the duration of the hook
invocation) the appropriate `RelationHookContext`, in addition to the
implied relation hook context (if any) that is currently done.

Upon a successful exit of a hook, any additional relation hook
contexts requires that `Invoker._cleanup_process` flush these contexts
and log any changes.

On a related note: although `client_id` is in the current codebase,
it's never actually used, except in a limited way. `client id` is set
to a constant value defined in `juju.unit.lifecycle._EVIL_CONSTANT`
with value "constant". However, this does provide some limited denial
of using `relation-set`, etc, out of a hook (or with this proposal,
`juju do`).


`juju do` subcommand
~~~~~~~~~~~~~~~~~~~~

The `juju do` subcommand uses the encapsulation of `Invoker`; most of
the additional functionality required for an `Invoker` setup,
including domain socket listener support, is seen in `MockUnitAgent`
in test_invoker.py.


-------------- next part --------------
A non-text attachment was scrubbed...
Name: signature.asc
Type: application/pgp-signature
Size: 554 bytes
Desc: OpenPGP digital signature
URL: <https://lists.ubuntu.com/archives/juju/attachments/20120229/a31b8ef1/attachment-0001.pgp>


More information about the Juju mailing list