sudo snap install lxd
lxd init
# Disable IPv6 for lxd
lxc network set lxdbr0 ipv6.address none
lxc vs lxd?
lxd is the name for the managemnt software while lxc is the name for runtime. Please note that the lxd package also contains lxc.
sudo snap install juju --classic
# Check what clouds are known out-of-the-box
juju clouds --all
# Create a juju controller in juju cloud
juju bootstrap
# List juju controllers
lxc list && juju controllers && juju clouds
Charmchaft is a tool for build and package Charms.
sudo snap install charmcraft --class
mkdir charm-mini
cd charm-mini
charmcraft init
tree
# .
# ├── actions.yaml
# ├── charmcraft.yaml
# ├── config.yaml
# ├── CONTRIBUTING.md
# ├── LICENSE
# ├── metadata.yaml
# ├── README.md
# ├── requirements-dev.txt
# ├── requirements.txt
# ├── run_tests
# ├── src
# │ └── charm.py
# └── tests
# ├── __init__.py
# └── test_charm.py
#
# 2 directories, 13 files
Let's see the file src/charm.py
First look at src/charm.py
class CharmMiniCharm(CharmBase):
"""Charm the service."""
_stored = StoredState()
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.httpbin_pebble_ready, self._on_httpbin_pebble_ready)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.fortune_action, self._on_fortune_action)
self._stored.set_default(things=[])
To create a charm with ops(Charmed Operator Framework), you need to create a charm inhirt opt.charm.CharmBase
.
But how this work? we need to take a look into CharmBase
source code
class CharmBase(Object):
"""Base class that represents the charm overall.
...
"""
on = CharmEvents()
def __init__(self, framework: Framework, key: typing.Optional = None):
super().__init__(framework, None)
for relation_name in self.framework.meta.relations:
relation_name = relation_name.replace('-', '_')
self.on.define_event(relation_name + '_relation_created', RelationCreatedEvent)
self.on.define_event(relation_name + '_relation_joined', RelationJoinedEvent)
self.on.define_event(relation_name + '_relation_changed', RelationChangedEvent)
self.on.define_event(relation_name + '_relation_departed', RelationDepartedEvent)
self.on.define_event(relation_name + '_relation_broken', RelationBrokenEvent)
for storage_name in self.framework.meta.storages:
storage_name = storage_name.replace('-', '_')
self.on.define_event(storage_name + '_storage_attached', StorageAttachedEvent)
self.on.define_event(storage_name + '_storage_detaching', StorageDetachingEvent)
for action_name in self.framework.meta.actions:
action_name = action_name.replace('-', '_')
self.on.define_event(action_name + '_action', ActionEvent)
for container_name in self.framework.meta.containers:
container_name = container_name.replace('-', '_')
self.on.define_event(container_name + '_pebble_ready', PebbleReadyEvent)
@property
def app(self) -> model.Application:
"""Application that this unit is part of."""
return self.framework.model.app
@property
def unit(self) -> model.Unit:
"""Unit that this execution is responsible for."""
return self.framework.model.unit
@property
def meta(self) -> 'CharmMeta':
"""Metadata of this charm."""
return self.framework.meta
@property
def charm_dir(self) -> pathlib.Path:
"""Root directory of the charm as it is running."""
return self.framework.charm_dir
j
@property
def config(self) -> model.ConfigData:
"""A mapping containing the charm's config and current values."""
return self.model.config
You can sess Charm have some property functions to return some value from framework and model.
Also use lib ops.charm.CharmEvents
to control lifecycle.
So the basic idea here is:
classDiagram
class CharmBase {
+CharmEvents on
}
class CharmEvents {
}
CharmBase *-- CharmEvents: on
class Object {
+Framework framework
__init__(self, parent, key)
}
CharmBase <|-- Object
Object *-- Framework: framework
class Framework
Framework <|-- Object
class CharmMiniCharm
CharmMiniCharm <|-- CharmBase
And on(CharmEvents)
is the event descriptor used to set up event handlers for corresponding events.
You can setup corresponding event in __init__
like:
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.httpbin_pebble_ready, self._on_httpbin_pebble_ready)
...
For the event list you can read on the documentation:
TODO: juju event lifecycle with mermaid js
But what is self.framework
? We can see it on ops.framework.Framework
class Framework(Object):
"""Main interface to from the Charm to the Operator Framework internals."""
...
# Also in `ops.framework.Object`
class Object(metaclass=_Metaclass):
"""Initialize an Object as a new leaf in :class:`Framework`, identified by `key`.
Args:
parent: parent node in the tree.
key: unique identifier for this object.
Every object belongs to exactly one framework.
Every object has a parent, which might be a framework.
We track a "path to object," which is the path to the parent, plus the object's unique
identifier. Event handlers use this identity to track the destination of their events, and the
Framework uses this id to track persisted state between event executions.
The Framework should raise an error if it ever detects that two objects with the same id have
been created.
"""
framework = None # type: Framework
handle = None # type: Handle
handle_kind = HandleKind() # type: str
def __init__(self, parent, key):
kind = self.handle_kind
if isinstance(parent, Framework):
self.framework = parent
# Avoid Framework instances having a circular reference to themselves.
if self.framework is self:
self.framework = weakref.proxy(self.framework)
self.handle = Handle(None, kind, key)
else:
self.framework = parent.framework
self.handle = Handle(parent, kind, key)
self.framework._track(self)
# TODO Detect conflicting handles here.
...
Well, when you create the Charm, you will create a tree like structure, Framework is the root node of the tree.
flowchart TD
Framework --> Framework
Framework --> Object_A --> Object_C
Framework --> Object_B
Now replace the code in src/charm.py
with:
#!/usr/bin/env python3
# Copyright 2021 Erik Lönroth
# See LICENSE file for licensing details.
#
# Learn more at: https://juju.is/docs/sdk
import logging
from ops.charm import CharmBase
from ops.main import main
from ops.model import MaintenanceStatus, ActiveStatus
logger = logging.getLogger(__name__)
class DeployMinimalCharm(CharmBase):
"""
A minimalistic charm which only implements the deployment hooks
install -> config_changed -> start
"""
def __init__(self, *args):
super().__init__(*args)
self.framework.observe(self.on.install, self._on_install)
self.framework.observe(self.on.config_changed, self._on_config_changed)
self.framework.observe(self.on.start, self._on_start)
def _on_install(self, event):
"""
install is the first core hook to fire when deploying.
"""
logger.info("Step 1/3: INSTALL")
self.unit.status = MaintenanceStatus("Step: 1/3")
def _on_config_changed(self, event):
"""
config_changed is second core hook to fire when deploying.
"""
logger.info("Step 2/3: CONFIG_CHANGED")
self.unit.status = MaintenanceStatus("Step: 2/3")
def _on_start(self, event):
"""
start is the last core hook to fire when deploying.
"""
logger.info("Step 3/3: START")
self.unit.status = ActiveStatus("Step: 3/3")
if __name__ == "__main__":
main(DeployMinimalCharm)
Back to CharmMiniCharm
, lets build a Charm now
charmcraft build
# Charmcraft will request lxc to deploy container for building the Charm.
# To check what happens on the level of lxc:
lxc list --project charmcraft
# You can clean lxc with
charmcraft clean
# list charm
ls | grep *.charm
# charm-mini_ubuntu-20.04-amd64.charm
We have a charm now!
But what is charm?
It's a zip file actually, you can try to unzip it && see what's inside.
unzip charm-mini_ubuntu-20.04-amd64.charm -p tmp_charm && tree tmp_charm
Deploy charm with:
juju deploy ./charm-mini_ubuntu-20.04-amd64.charm
Monitor status && log
juju debug-log
juju status --watch 5s
Model Controller Cloud/Region Version SLA Timestamp
default localhost-localhost localhost/localhost 2.9.29 unsupported 16:22:16+08:00
App Version Status Scale Charm Channel Rev Exposed Message
charm-mini active 1 charm-mini 0 no Step: 3/3
Unit Workload Agent Machine Public address Ports Message
charm-mini/0* active idle 0 10.127.138.27 Step: 3/3
Machine State DNS Inst id Series AZ Message
0 started 10.127.138.27 juju-d3c39e-0 focal Running
charmcraft clean
juju remove-application charm-mini