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.GLOBALSERVICE
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.SERVICEUNIT
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.UNITWriting 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/interfacesThe 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/httpAnd declare the interface's metadata in interface.yaml
name: http
summary: Basic HTTP interface
version: 1
repo: https://git.launchpad.net/~bcsaller/charms/+source/httpWriting 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: httpAnd 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: websiteAnd 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']))