Permalink
Fetching contributors…
Cannot retrieve contributors at this time
239 lines (188 sloc) 7.52 KB

Communication Scopes

When writing an interface, there is also the concept of a communication scope. Not to be confused with relation scopes which are strictly to define behavior of the relationship. Communication scopes define who is participating in a particular conversation among related services.

There are three distinct flavors of scoping for a conversation. At times there will be fairly static information being transmitted between services - and this is a prime candidate for a GLOBAL scope. If the information varies from service to service but remains the same for each unit of the service, you will want to investigate SERVICE level conversations. The final, and default communication scope is UNIT level conversations, where each unit in a service group gets its own conversation with the provider.

GLOBAL

All connected services and units for this relation will share a single conversation. The same data will be broadcast to every remote unit, and retrieved data will be aggregated across all remote units and is expected to either eventually agree or be set by a single leader.

class MyRelationClient(RelationBase):
  scope = Scopes.GLOBAL

SERVICE

Each connected service for this relation will have its own conversation. The same data will be broadcast to every unit of each service’s conversation, and data from all units of each service will be aggregated and is expected to either eventually agree or be set by a single leader.

class MyRelationClient(RelationBase):
  scope = Scopes.SERVICE

UNIT

Each connected unit for this relation will have its own conversation. This is the default scope. Each unit’s data will be retrieved individually, but note that due to how Juju works, the same data is still broadcast to all units of a single service.

class MyRelationClient(RelationBase):
  scope = scopes.UNIT

Writing an interface-layer

Begin by making an interface repository if you don't currently have one.

mkdir -p $JUJU_REPOSITORY/interfaces
export INTERFACE_PATH=$JUJU_REPOSITORY/interfaces

The export of INTERFACE_PATH is an environment variable which tells the charm build process where to scan for local interfaces not found in the layer registry.

With our interface repository created, we can now create our new interface.

Start by creating the directory to warehouse your interface

mkdir -p $INTERFACE_PATH/http

And declare the interface's metadata in interface.yaml

name: http
summary: Basic HTTP interface
version: 1
repo: https://git.launchpad.net/~bcsaller/charms/+source/http

Writing the provides side

We're now ready to implement the provider interface in provides.py.

from charmhelpers.core import hookenv
from charms.reactive import hook
from charms.reactive import RelationBase
from charms.reactive import scopes


class HttpProvides(RelationBase):
    # Every unit connecting will get the same information
    scope = scopes.GLOBAL

    # Use some template magic to declare our relation(s)
    @hook('{provides:http}-relation-{joined,changed}')
    def changed(self):
        # Signify that the relationship is now available to our principal layer(s)
        self.set_state('{relation_name}.available')

    @hook('{provides:http}-relation-{broken,departed}')
    def broken(self):
        # Remove the state that our relationship is now available to our principal layer(s)
        self.remove_state('{relation_name}.available')

    # call this method when passed into methods decorated with
    # @when('{relation}.available')
    # to configure the relation data
    def configure(self, port):
        relation_info = {
            'hostname': hookenv.unit_get('private-address'),
            'port': port,
        }
        self.set_remote(**relation_info)

Implementing the provides side

With our provider interface written, lets take a look at how we might implement this in a charm.

In our metadata we define a website relation implementing the interface:

provides:
  website:
    interface: http

And in the reactive class, we will receive the RelationBase object, and invoke the configure method:

@when('website.available')
def configure_website(website):
  website.configure(80)

Note that you see an implicit object being passed in as website. This is a behavior pattern when using Interface Layers. As we programmed the configure method above in the HTTP Interface, this relation (as defined in metadata.yaml) thats implementing The interface is website. Thus we get an instance of the HTTP interface, as the website object, which we can then invoke the configure() method defined above in our event/hook code.

Writing the requires side

We're now ready to implement the requirer interface in requires.py

from charms.reactive import hook
from charms.reactive import RelationBase
from charms.reactive import scopes


class HttpRequires(RelationBase):
    scope = scopes.UNIT

    @hook('{requires:http}-relation-{joined,changed}')
    def changed(self):
        conv = self.conversation()
        if conv.get_remote('port'):
            # this unit's conversation has a port, so
            # it is part of the set of available units
            conv.set_state('{relation_name}.available')

    @hook('{requires:http}-relation-{departed,broken}')
    def broken(self):
        conv = self.conversation()
        conv.remove_state('{relation_name}.available')

    def services(self):
        """
        Returns a list of available HTTP services and their associated hosts
        and ports.
        The return value is a list of dicts of the following form::
            [
                {
                    'service_name': name_of_service,
                    'hosts': [
                        {
                            'hostname': address_of_host,
                            'port': port_for_host,
                        },
                        # ...
                    ],
                },
                # ...
            ]
        """
        services = {}
        for conv in self.conversations():
            service_name = conv.scope.split('/')[0]
            service = services.setdefault(service_name, {
                'service_name': service_name,
                'hosts': [],
            })
            host = conv.get_remote('hostname') or conv.get_remove('private-address')
            port = conv.get_remote('port')
            if host and port:
                service['hosts'].append({
                    'hostname': host,
                    'port': port,
                })
        return [s for s in services.values() if s['hosts']]

Implementing the requires side

With our requirer interface written, lets take a look at how we might implement this in a charm.

In our metadata we define a reverseproxy relation implementing the interface:

requires:
  reverseproxy:
    interface: website

And in our reactive file, we implement it as so:

from charms.reactive.helpers import data_changed

@when('reverseproxy.available')
def update_reverse_proxy_config(reverseproxy):
    services = reverseproxy.services()
    if not data_changed('reverseproxy.services', services):
        return
    for service in services:
        for host in service['hosts']:
            hookenv.log('{} has a unit {}:{}'.format(
                services['service_name'],
                host['hostname'],
                host['port']))