Start of cookie cutter

This commit is contained in:
Liam Young 2022-02-08 11:12:26 +00:00
parent e874a478c9
commit 2ab6d90b0b
31 changed files with 766 additions and 439 deletions

View File

@ -1,5 +1,30 @@
Advanced Sunbeam OpenStack
========================================
Advanced Sunbeam OpenStack Documentation
========================================
Tuturials
#########
`Writing an OpenStack API charm with ASO <writing-OS-API-charm.rst>`_.
How-Tos
#######
`How-To write a pebble handler <howto-pebble-handler.rst>`_.
`How-To write a relation handler <howto-relation-handler.rst>`_.
`How-To write a charm context <howto-config-context.rst>`_.
Reference
#########
Concepts
########
`How to Write a charm with ASO <howto-write-charm.rst>`_.
`ASO Concepts <concepts.rst>`_.

5
aso-charm-init.sh Executable file
View File

@ -0,0 +1,5 @@
#!/usr/bin/env bash
[ -e .tox/cookie/bin/activate ] || tox -e cookie
source .tox/cookie/bin/activate
shared_code/aso-charm-init.py $@

View File

@ -113,7 +113,7 @@ Charms
ASO currently provides two base classes to choose from when writing a charm.
The first is `OSBaseOperatorCharm` and the second, which is derived from the
first, `OSBaseOperatorAPICharm`.
first, `OSBaseOperatorAPICharm`.
The base classes setup a default set of relation handlers (based on what
relations are present in the charm metadata) and default container handlers.

1
cookie-requirements.txt Normal file
View File

@ -0,0 +1 @@
cookiecutter

55
howto-config-context.rst Normal file
View File

@ -0,0 +1,55 @@
=============================
How-To Write a config context
=============================
A config context is an additional context that is passed to the template
renderer in its own namespace. They are usually useful when some logic
needs to be applied to user supplied charm configuration. The context
has access to the charm object.
Below is an example which applies logic to the charm config as well as
collecting the application name to constuct the context.
.. code:: python
class CinderCephConfigurationContext(ConfigContext):
"""Cinder Ceph configuration context."""
def context(self) -> None:
"""Cinder Ceph configuration context."""
config = self.charm.model.config.get
data_pool_name = config('rbd-pool-name') or self.charm.app.name
if config('pool-type') == "erasure-coded":
pool_name = (
config('ec-rbd-metadata-pool') or
f"{data_pool_name}-metadata"
)
else:
pool_name = data_pool_name
backend_name = config('volume-backend-name') or self.charm.app.name
return {
'cluster_name': self.charm.app.name,
'rbd_pool': pool_name,
'rbd_user': self.charm.app.name,
'backend_name': backend_name,
'backend_availability_zone': config('backend-availability-zone'),
}
Configuring Charm to use custom config context
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The charm can append the new context onto those provided by the base class.
.. code:: python
class MyCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Charm the service."""
@property
def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]:
"""Configuration contexts for the operator."""
contexts = super().config_contexts
contexts.append(
sunbeam_ctxts.CinderCephConfigurationContext(self, "cinder_ceph"))
return contexts

132
howto-pebble-handler.rst Normal file
View File

@ -0,0 +1,132 @@
=============================
How-To Write a pebble handler
=============================
A pebble handler sits between a charm and a container it manages. A pebble
handler presents the charm with a consistent method of interaction with
the container. For example the charm can query the handler to check config
has been rendered and services started. It can call the `execute` method
to run commands in the container or call `write_config` to render the
defined files into the container.
Common Pebble handler changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
ASO provides a pebble handler base classes which provide the starting point
for writing a new handler. If the container runs a service then the
`ServicePebbleHandler` should be used. If the container does not provide a
service (perhaps its just an environment for executing commands that affact
other container) then `PebbleHandler` should be used.
.. code:: python
import container_handlers
class MyServicePebbleHandler(container_handlers.ServicePebbleHandler):
"""Manage MyService Container."""
The handlers can create directories in the container once the pebble is
available.
.. code:: python
@property
def directories(self) -> List[sunbeam_chandlers.ContainerDir]:
"""Directories to create in container."""
return [
sunbeam_chandlers.ContainerDir(
'/var/log/my-service',
'root',
'root'),
In addition to directories the handler can list configuration files which need
to be rendered into the container. These will be rendered as templates using
all available contexts.
.. code:: python
def default_container_configs(
self
) -> List[sunbeam_core.ContainerConfigFile]:
"""Files to render into containers."""
return [
sunbeam_core.ContainerConfigFile(
'/etc/mysvc/mvsvc.conf',
'root',
'root')]
If a service should be running in the conainer the handler specifies the
layer describing the service that will be passed to pebble.
.. code:: python
def get_layer(self) -> dict:
"""Pebble configuration layer for MyService service."""
return {
"summary": "My service",
"description": "Pebble config layer for MyService",
"services": {
'my_svc': {
"override": "replace",
"summary": "My Super Service",
"command": "/usr/bin/my-svc",
"startup": "disabled",
},
},
}
Advanced Pebble handler changes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default the pebble handler is the observer of pebble events. If this
behaviour needs to be altered then `setup_pebble_handler` method can be
changed.
.. code:: python
def setup_pebble_handler(self) -> None:
"""Configure handler for pebble ready event."""
pass
Or perhaps it is ok for the pebble handler to observe the event but a
different reaction is required. In this case the method associated
with the event can be overridden.
.. code:: python
def _on_service_pebble_ready(
self, event: ops.charm.PebbleReadyEvent
) -> None:
"""Handle pebble ready event."""
container = event.workload
container.add_layer(self.service_name, self.get_layer(), combine=True)
self.execute(["run", "special", "command"])
logger.debug(f"Plan: {container.get_plan()}")
self.ready = True
self._state.pebble_ready = True
self.charm.configure_charm(event)
Configuring Charm to use custom pebble handler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The charms `get_pebble_handlers` method dictates which pebble handlers are used.
.. code:: python
class MyCharmCharm(NeutronOperatorCharm):
def get_pebble_handlers(self) -> List[sunbeam_chandlers.PebbleHandler]:
"""Pebble handlers for the service."""
return [
MyServicePebbleHandler(
self,
'my-server-container',
self.service_name,
self.container_configs,
self.template_dir,
self.openstack_release,
self.configure_charm,
)
]

142
howto-relation-handler.rst Normal file
View File

@ -0,0 +1,142 @@
===============================
How-To Write a relation handler
===============================
A relation handler gives the charm a consistent method of interacting with
relation interfaces. It can also encapsulate common interface tasks, this
removes the need for duplicate code across multiple charms.
This how-to will walk through the steps to write a database relation handler
for the requires side.
In this database interface the database charm expects the client to provide the name
of the database(s) to be created. To model this the relation handler will require
the charm to specify the database name(s) when the class is instantiated
.. code:: python
class DBHandler(RelationHandler):
"""Handler for DB relations."""
def __init__(
self,
charm: ops.charm.CharmBase,
relation_name: str,
callback_f: Callable,
databases: List[str] = None,
) -> None:
"""Run constructor."""
self.databases = databases
super().__init__(charm, relation_name, callback_f)
The handler initialises the interface with the database names and also sets up
an observer for relation changed events.
.. code:: python
def setup_event_handler(self) -> ops.charm.Object:
"""Configure event handlers for a MySQL relation."""
logger.debug("Setting up DB event handler")
# Lazy import to ensure this lib is only required if the charm
# has this relation.
import charms.sunbeam_mysql_k8s.v0.mysql as mysql
db = mysql.MySQLConsumer(
self.charm, self.relation_name, databases=self.databases
)
_rname = self.relation_name.replace("-", "_")
db_relation_event = getattr(
self.charm.on, f"{_rname}_relation_changed"
)
self.framework.observe(db_relation_event, self._on_database_changed)
return db
The method run when tha changed event is seen checks whether all required data
has been provided. If it is then it calls back to the charm, if not then no
action is taken.
.. code:: python
def _on_database_changed(self, event: ops.framework.EventBase) -> None:
"""Handle database change events."""
databases = self.interface.databases()
logger.info(f"Received databases: {databases}")
if not self.ready:
return
self.callback_f(event)
@property
def ready(self) -> bool:
"""Whether the handler is ready for use."""
try:
# Nothing to wait for
return bool(self.interface.databases())
except AttributeError:
return False
The `ready` property is common across all handlers and allows the charm to
check the state of any relation in a consistent way.
The relation handlers also provide a context which can be used when rendering
templates. ASO places each relation context in its own namespace.
.. code:: python
def context(self) -> dict:
"""Context containing database connection data."""
try:
databases = self.interface.databases()
except AttributeError:
return {}
if not databases:
return {}
ctxt = {}
conn_data = {
"database_host": self.interface.credentials().get("address"),
"database_password": self.interface.credentials().get("password"),
"database_user": self.interface.credentials().get("username"),
"database_type": "mysql+pymysql",
}
for db in self.interface.databases():
ctxt[db] = {"database": db}
ctxt[db].update(conn_data)
connection = (
"{database_type}://{database_user}:{database_password}"
"@{database_host}/{database}")
if conn_data.get("database_ssl_ca"):
connection = connection + "?ssl_ca={database_ssl_ca}"
if conn_data.get("database_ssl_cert"):
connection = connection + (
"&ssl_cert={database_ssl_cert}"
"&ssl_key={database_ssl_key}")
ctxt[db]["connection"] = str(connection.format(
**ctxt[db]))
return ctxt
Configuring Charm to use custom relation handler
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The base class will add the default relation handlers for any interfaces
which do not yet have a handler. Therefore the custom handler is added to
the list and then passed to the super method. The base charm class will
see a handler already exists for shared-db and not add the default one.
.. code:: python
class MyCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Charm the service."""
def get_relation_handlers(self, handlers=None) -> List[
sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = handlers or []
if self.can_add_handler("shared-db", handlers):
self.db = sunbeam_rhandlers.DBHandler(
self, "shared-db", self.configure_charm, self.databases
)
handlers.append(self.db)
handlers = super().get_relation_handlers(handlers)
return handlers

View File

@ -1,432 +0,0 @@
=============
New API Charm
=============
The example below will walk through the creation of a basic API charm for the
OpenStack `Glance <https://wiki.openstack.org/wiki/Glance>`__ service designed
to run on kubernetes.
Create the skeleton charm
=========================
Prerequisite
~~~~~~~~~~~~
The charmcraft tool builds a skeleton charm.
.. code:: bash
mkdir charm-glance-operator
cd charm-glance-operator/
charmcraft init --name sunbeam-glance-operator
Some useful files can be found in ASO so that needs
to be available locally
.. code:: bash
git clone https://github.com/openstack-charmers/advanced-sunbeam-openstack
Amend charmcraft file to include git at build time:
.. code:: bash
parts:
charm:
build-packages:
- git
Add Metadata
============
The first job is to write the metadata yaml.
.. code:: yaml
# Copyright 2021 Canonical Ltd
# See LICENSE file for licensing details.
name: sunbeam-glance-operator
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
summary: OpenStack Image Registry and Delivery Service
description: |
The Glance project provides an image registration and discovery service
and an image delivery service. These services are used in conjunction
by Nova to deliver images.
version: 3
bases:
- name: ubuntu
channel: 20.04/stable
tags:
- openstack
- storage
- misc
containers:
glance-api:
resource: glance-api-image
resources:
glance-api-image:
type: oci-image
description: OCI image for OpenStack Glance (kolla/glance-api-image)
requires:
shared-db:
interface: mysql_datastore
limit: 1
ingress:
interface: ingress
identity-service:
interface: keystone
limit: 1
amqp:
interface: rabbitmq
image-service:
interface: glance
ceph:
interface: ceph-client
peers:
peers:
interface: glance-peer
The first part of the metadata is pretty self explanatory, is sets out the some
general information about the charm. The `containers` section lists all the
containers that this charm will manage. Glance consists of just one container
so just one container is listed here. Similarly in the resources section all
the container images are listed. Since there is just one container only one
image is listed here.
The requires section lists all the relations this charm is reliant on. These
are all standard for an OpenStack API charm plus the additional ceph relation.
Common Files
============
ASO contains some common files which need to copied into the charm.
.. code:: bash
cp advanced-sunbeam-openstack/shared_code/tox.ini charm-glance-operator/
cp advanced-sunbeam-openstack/shared_code/requirements.txt charm-glance-operator/
cp -r advanced-sunbeam-openstack/shared_code/templates charm-glance-operator/src/
cp advanced-sunbeam-openstack/shared_code/.stestr.conf charm-glance-operator/
cp advanced-sunbeam-openstack/shared_code/test-requirements.txt charm-glance-operator/
At the moment the wsgi template needs to be renamed to add incluse the
service name.
.. code:: bash
cd charm-glance-operator
mv /src/templates/wsgi-template.conf.j2 ./src/templates/wsgi-glance-api.conf.j2
There are some config options which are common accross the OpenStack api charms. Since
this charm uses ceph add the ceph config options too.
.. code:: bash
cd advanced-sunbeam-openstack/shared_code/
echo "options:" > ../../charm-glance-operator/config.yaml
cat config-api.yaml >> ../../charm-glance-operator/config.yaml
cat config-ceph-options.yaml >> ../../charm-glance-operator/config.yaml
Fetch interface libs corresponding to the requires interfaces:
.. code:: bash
cd charm-glance-operator
charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress
charmcraft fetch-lib charms.sunbeam_mysql_k8s.v0.mysql
charmcraft fetch-lib charms.sunbeam_keystone_operator.v0.identity_service
charmcraft fetch-lib charms.sunbeam_rabbitmq_operator.v0.amqp
charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
Templates
=========
Much of the glance configuration is covered by common templates which were copied
into the charm in the previous step. The only additional template for this charm
is for `glance-api.conf`. Add the following into `./src/templates/glance-api.conf.j2`
.. code::
###############################################################################
# [ WARNING ]
# glance configuration file maintained by Juju
# local changes may be overwritten.
###############################################################################
[DEFAULT]
debug = {{ options.debug }}
transport_url = {{ amqp.transport_url }}
{% include "parts/section-database" %}
{% include "parts/section-identity" %}
[glance_store]
default_backend = ceph
filesystem_store_datadir = /var/lib/glance/images/
[ceph]
rbd_store_chunk_size = 8
rbd_store_pool = glance
rbd_store_user = glance
rados_connect_timeout = 0
rbd_store_ceph_conf = /etc/ceph/ceph.conf
[paste_deploy]
flavor = keystone
Charm
=====
This is subject to change as more of the common code is generalised into aso.
Inherit from OSBaseOperatorAPICharm
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Start by creating a charm class that inherits from the `OSBaseOperatorAPICharm`
class which contains all the code which is common accross OpenStack API charms.
.. code:: python
#!/usr/bin/env python3
"""Glance Operator Charm.
This charm provide Glance services as part of an OpenStack deployment
"""
import logging
from typing import List
from ops.framework import StoredState
from ops.main import main
import advanced_sunbeam_openstack.cprocess as sunbeam_cprocess
import advanced_sunbeam_openstack.charm as sunbeam_charm
import advanced_sunbeam_openstack.core as sunbeam_core
import advanced_sunbeam_openstack.relation_handlers as sunbeam_rhandlers
import advanced_sunbeam_openstack.config_contexts as sunbeam_ctxts
from charms.observability_libs.v0.kubernetes_service_patch \
import KubernetesServicePatch
logger = logging.getLogger(__name__)
class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Charm the service."""
ceph_conf = "/etc/ceph/ceph.conf"
_state = StoredState()
service_name = "glance-api"
wsgi_admin_script = '/usr/bin/glance-wsgi-api'
wsgi_public_script = '/usr/bin/glance-wsgi-api'
def __init__(self, framework):
super().__init__(framework)
self.service_patcher = KubernetesServicePatch(
self,
[
('public', self.default_public_ingress_port),
]
)
The `KubernetesServicePatch` module is used to expose the service within kubernetes
so that it is externally visable. Hopefully this will eventually be accomplished by
Juju and and can be removed.
Ceph Support
~~~~~~~~~~~~
This glance charm with relate to Ceph to store uploaded images. A relation to Ceph
is not common accross the api charms to we need to add the components from ASO to
support the ceph relation.
.. code:: python
@property
def config_contexts(self) -> List[sunbeam_ctxts.ConfigContext]:
"""Configuration contexts for the operator."""
contexts = super().config_contexts
contexts.append(
sunbeam_ctxts.CephConfigurationContext(self, "ceph_config"))
return contexts
@property
def container_configs(self) -> List[sunbeam_core.ContainerConfigFile]:
"""Container configurations for the operator."""
_cconfigs = super().container_configs
_cconfigs.extend(
[
sunbeam_core.ContainerConfigFile(
[self.service_name],
self.ceph_conf,
self.service_user,
self.service_group,
),
]
)
return _cconfigs
def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = super().get_relation_handlers()
self.ceph = sunbeam_rhandlers.CephClientHandler(
self,
"ceph",
self.configure_charm,
allow_ec_overwrites=True,
app_name='rbd'
)
In the `config_contexts` `sunbeam_ctxts.CephConfigurationContext` is added to the list
of config contexts. This will look after transalting some of the charms
configuration options into Ceph configuration.
In `container_configs` the `ceph.conf` is added to the list of configuration
files to be rendered in containers.
Finally in `get_relation_handlers` the relation handler for the `ceph` relation is
added.
OpenStack Endpoints
~~~~~~~~~~~~~~~~~~~
`OSBaseOperatorAPICharm` makes assumptions based on the self.service_name but a few
of these are broken as there is a mix between `glance` and `glance_api`. Finally the
charm needs to specify what endpoint should be registered in the keystone catalgue
each charm needs to explicitly state this as there is a lot of variation between
services
.. code:: python
@property
def service_conf(self) -> str:
"""Service default configuration file."""
return f"/etc/glance/glance-api.conf"
@property
def service_user(self) -> str:
"""Service user file and directory ownership."""
return 'glance'
@property
def service_group(self) -> str:
"""Service group file and directory ownership."""
return 'glance'
@property
def service_endpoints(self):
return [
{
'service_name': 'glance',
'type': 'image',
'description': "OpenStack Image",
'internal_url': f'{self.internal_url}',
'public_url': f'{self.public_url}',
'admin_url': f'{self.admin_url}'}]
@property
return 9292
Bootstrap
~~~~~~~~~
Currently ASO does not support database migrations, this will be fixed soon but until
then add a db sync to the bootstrap process.
.. code:: python
def _do_bootstrap(self):
"""
Starts the appropriate services in the order they are needed.
If the service has not yet been bootstrapped, then this will
1. Create the database
"""
super()._do_bootstrap()
try:
container = self.unit.get_container(self.wsgi_container_name)
logger.info("Syncing database...")
out = sunbeam_cprocess.check_output(
container,
[
'sudo', '-u', 'glance',
'glance-manage', '--config-dir',
'/etc/glance', 'db', 'sync'],
service_name='keystone-db-sync',
timeout=180)
logging.debug(f'Output from database sync: \n{out}')
except sunbeam_cprocess.ContainerProcessError:
logger.exception('Failed to bootstrap')
self._state.bootstrapped = False
return
Configure Charm
~~~~~~~~~~~~~~~
The container used by this charm should include `ceph-common` but it currently does
not. To work around this install it in the container. As glance communicates with Ceph
another specialisation is needed to run `ceph-authtool`.
.. code:: python
def configure_charm(self, event) -> None:
"""Catchall handler to cconfigure charm services."""
if not self.relation_handlers_ready():
logging.debug("Defering configuration, charm relations not ready")
return
for ph in self.pebble_handlers:
if ph.pebble_ready:
container = self.unit.get_container(
ph.container_name
)
sunbeam_cprocess.check_call(
container,
['apt', 'update'])
sunbeam_cprocess.check_call(
container,
['apt', 'install', '-y', 'ceph-common'])
try:
sunbeam_cprocess.check_call(
container,
['ceph-authtool',
f'/etc/ceph/ceph.client.{self.app.name}.keyring',
'--create-keyring',
f'--name=client.{self.app.name}',
f'--add-key={self.ceph.key}']
)
except sunbeam_cprocess.ContainerProcessError:
pass
ph.init_service(self.contexts())
super().configure_charm(event)
# Restarting services after bootstrap should be in aso
if self._state.bootstrapped:
for handler in self.pebble_handlers:
handler.start_service()
OpenStack Release
~~~~~~~~~~~~~~~~~
This charm is spefic to a particular release so the final step is to add a
release specific class.
.. code:: python
class GlanceWallabyOperatorCharm(GlanceOperatorCharm):
openstack_release = 'wallaby'
if __name__ == "__main__":
# Note: use_juju_for_storage=True required per
# https://github.com/canonical/operator/issues/506
main(GlanceWallabyOperatorCharm, use_juju_for_storage=True)

76
shared_code/aso-charm-init.py Executable file
View File

@ -0,0 +1,76 @@
#!/usr/bin/python3
import shutil
import yaml
import argparse
import tempfile
import os
import glob
from cookiecutter.main import cookiecutter
import subprocess
from datetime import datetime
import sys
def start_msg():
print("This tool is designed to be used after 'charmcraft init' was initially run")
def cookie(output_dir, extra_context):
cookiecutter(
'aso_charm/',
extra_context=extra_context,
output_dir=output_dir)
def arg_parser():
parser = argparse.ArgumentParser(description='Process some integers.')
parser.add_argument('charm_path', help='path to charm')
return parser.parse_args()
def read_metadata_file(charm_dir):
with open(f'{charm_dir}/metadata.yaml', 'r') as f:
metadata = yaml.load(f, Loader=yaml.FullLoader)
return metadata
def switch_dir():
abspath = os.path.abspath(__file__)
dname = os.path.dirname(abspath)
os.chdir(dname)
def get_extra_context(charm_dir):
metadata = read_metadata_file(charm_dir)
charm_name = metadata['name']
service_name = charm_name.replace('sunbeam-', '')
service_name = service_name.replace('-operator', '')
ctxt = {
'service_name': service_name,
'charm_name': charm_name}
# XXX REMOVE
ctxt['db_sync_command'] = 'ironic-dbsync --config-file /etc/ironic/ironic.conf create_schema'
ctxt['ingress_port'] = 6385
return ctxt
def sync_code(src_dir, target_dir):
cmd = ['rsync', '-r', '-v', f'{src_dir}/', target_dir]
subprocess.check_call(cmd)
def main() -> int:
"""Echo the input arguments to standard output"""
start_msg()
args = arg_parser()
charm_dir = args.charm_path
switch_dir()
with tempfile.TemporaryDirectory() as tmpdirname:
extra_context = get_extra_context(charm_dir)
service_name = extra_context['service_name']
cookie(
tmpdirname,
extra_context)
src_dir = f"{tmpdirname}/{service_name}"
shutil.copyfile(
f'{src_dir}/src/templates/wsgi-template.conf.j2',
f'{src_dir}/src/templates/wsgi-{service_name}-api.conf')
sync_code(src_dir, charm_dir)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,9 @@
{
"service_name": "",
"charm_name": "",
"ingress_port": "",
"db_sync_command": "",
"_copy_without_render": [
"src/templates"
]
}

View File

@ -0,0 +1,2 @@
# NOTE: no actions yet!
{ }

View File

@ -0,0 +1,17 @@
type: "charm"
bases:
- build-on:
- name: "ubuntu"
channel: "20.04"
run-on:
- name: "ubuntu"
channel: "20.04"
parts:
charm:
build-packages:
- git
- libffi-dev
- libssl-dev
charm-python-packages:
- setuptools < 58
- cryptography < 3.4

View File

@ -0,0 +1,27 @@
options:
debug:
default: False
description: Enable debug logging.
type: boolean
os-admin-hostname:
default: glance.juju
description: |
The hostname or address of the admin endpoints that should be advertised
in the glance image provider.
type: string
os-internal-hostname:
default: glance.juju
description: |
The hostname or address of the internal endpoints that should be advertised
in the glance image provider.
type: string
os-public-hostname:
default: glance.juju
description: |
The hostname or address of the internal endpoints that should be advertised
in the glance image provider.
type: string
region:
default: RegionOne
description: Space delimited list of OpenStack regions
type: string

View File

@ -0,0 +1,42 @@
name: {{ cookiecutter.charm_name }}
summary: OpenStack {{ cookiecutter.service_name }} service
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
description: |
OpenStack {{ cookiecutter.service_name }} provides an HTTP service for managing, selecting,
and claiming providers of classes of inventory representing available
resources in a cloud.
.
version: 3
bases:
- name: ubuntu
channel: 20.04/stable
tags:
- openstack
containers:
{{ cookiecutter.service_name }}-api:
resource: {{ cookiecutter.service_name }}-api-image
resources:
{{ cookiecutter.service_name }}-api-image:
type: oci-image
description: OCI image for OpenStack {{ cookiecutter.service_name }}
requires:
shared-db:
interface: mysql_datastore
limit: 1
identity-service:
interface: keystone
ingress:
interface: ingress
amqp:
interface: rabbitmq
provides:
{{ cookiecutter.service_name }}:
interface: {{ cookiecutter.service_name }}
peers:
peers:
interface: {{ cookiecutter.service_name }}-peer

View File

@ -1,6 +1,5 @@
# ops >= 1.2.0
ops
jinja2
git+https://github.com/canonical/operator@2875e73e#egg=ops
git+https://opendev.org/openstack/charm-ops-openstack#egg=ops_openstack
git+https://github.com/openstack-charmers/advanced-sunbeam-openstack#egg=advanced_sunbeam_openstack
lightkube

View File

@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""{{ cookiecutter.service_name[0]|upper}}{{cookiecutter.service_name[1:] }} Operator Charm.
This charm provide {{ cookiecutter.service_name[0]|upper}}{{cookiecutter.service_name[1:] }} services as part of an OpenStack deployment
"""
import logging
from ops.framework import StoredState
from ops.main import main
import advanced_sunbeam_openstack.charm as sunbeam_charm
logger = logging.getLogger(__name__)
class {{ cookiecutter.service_name[0]|upper}}{{cookiecutter.service_name[1:] }}OperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm):
"""Charm the service."""
_state = StoredState()
service_name = "{{ cookiecutter.service_name }}-api"
wsgi_admin_script = '/usr/bin/{{ cookiecutter.service_name }}-api-wsgi'
wsgi_public_script = '/usr/bin/{{ cookiecutter.service_name }}-api-wsgi'
db_sync_cmds = [
{{ cookiecutter.db_sync_command.split() }}
]
@property
def service_conf(self) -> str:
"""Service default configuration file."""
return f"/etc/{{ cookiecutter.service_name }}/{{ cookiecutter.service_name }}.conf"
@property
def service_user(self) -> str:
"""Service user file and directory ownership."""
return '{{ cookiecutter.service_name }}'
@property
def service_group(self) -> str:
"""Service group file and directory ownership."""
return '{{ cookiecutter.service_name }}'
@property
def service_endpoints(self):
return [
{
'service_name': '{{ cookiecutter.service_name }}',
'type': '{{ cookiecutter.service_name }}',
'description': "OpenStack {{ cookiecutter.service_name[0]|upper}}{{cookiecutter.service_name[1:] }} API",
'internal_url': f'{self.internal_url}',
'public_url': f'{self.public_url}',
'admin_url': f'{self.admin_url}'}]
@property
def default_public_ingress_port(self):
return {{ cookiecutter.ingress_port }}
class {{ cookiecutter.service_name[0]|upper}}{{cookiecutter.service_name[1:] }}WallabyOperatorCharm({{ cookiecutter.service_name[0]|upper}}{{cookiecutter.service_name[1:] }}OperatorCharm):
openstack_release = 'wallaby'
if __name__ == "__main__":
# Note: use_juju_for_storage=True required per
# https://github.com/canonical/operator/issues/506
main({{ cookiecutter.service_name[0]|upper}}{{cookiecutter.service_name[1:] }}WallabyOperatorCharm, use_juju_for_storage=True)

View File

@ -0,0 +1,2 @@
[keystone_authtoken]
{% include "parts/identity-data" %}

1
shared_code/templates Symbolic link
View File

@ -0,0 +1 @@
aso_charm/{{cookiecutter.service_name}}/src/templates

View File

@ -1,2 +0,0 @@
[keystone_authtoken]
{% include "parts/identity-connection" %}

View File

@ -42,6 +42,11 @@ deps =
commands =
./fetch-libs.sh
[testenv:cookie]
basepython = python3
deps = -r{toxinidir}/cookie-requirements.txt
commands = /bin/true
[testenv:py3.8]
basepython = python3.8
deps = -r{toxinidir}/requirements.txt

154
writing-OS-API-charm.rst Normal file
View File

@ -0,0 +1,154 @@
=============
New API Charm
=============
The example below will walk through the creation of a basic API charm for the
OpenStack `Ironic <https://wiki.openstack.org/wiki/Ironic>`__ service designed
to run on kubernetes.
Create the skeleton charm
=========================
Prerequisite
~~~~~~~~~~~~
Build a base geneeric charm with the `charmcraft` tool.
.. code:: bash
mkdir charm-ironic-operator
cd charm-ironic-operator
charmcraft init --name sunbeam-ironic-operator
Add ASO common files to new charm. The script will ask a few basic questions:
.. code:: bash
git clone https://github.com/openstack-charmers/advanced-sunbeam-openstack
cd advanced-sunbeam-openstack/shared_code
./aso-charm-init.sh ~/branches/charm-ironic-operator
This tool is designed to be used after 'charmcraft init' was initially run
service_name [ironic]: ironic
charm_name [sunbeam-ironic-operator]: sunbeam-ironic-operator
ingress_port []: 6385
db_sync_command [] ironic-dbsync --config-file /etc/ironic/ironic.conf create_schema:
Fetch interface libs corresponding to the requires interfaces:
.. code:: bash
cd charm-ironic-operator
charmcraft login
charmcraft fetch-lib charms.nginx_ingress_integrator.v0.ingress
charmcraft fetch-lib charms.sunbeam_mysql_k8s.v0.mysql
charmcraft fetch-lib charms.sunbeam_keystone_operator.v0.identity_service
charmcraft fetch-lib charms.sunbeam_rabbitmq_operator.v0.amqp
charmcraft fetch-lib charms.observability_libs.v0.kubernetes_service_patch
Templates
=========
Much of the service configuration is covered by common templates which were copied
into the charm in the previous step. The only additional template for this charm
is for `ironic.conf`. Add the following into `./src/templates/ironic.conf.j2`
.. code::
[DEFAULT]
debug = {{ options.debug }}
auth_strategy=keystone
transport_url = {{ amqp.transport_url }}
[keystone_authtoken]
{% include "parts/identity-data" %}
[database]
{% include "parts/database-connection" %}
[neutron]
{% include "parts/identity-data" %}
[glance]
{% include "parts/identity-data" %}
[cinder]
{% include "parts/identity-data" %}
[service_catalog]
{% include "parts/identity-data" %}
Make charm deployable
=====================
The next step is to pack the charm into a deployable format
.. code:: bash
cd charm-ironic-operator
charmcraft pack
Deploy Charm
============
The charm can now be deployed. The Kolla project has images that can be used to
run the service. Juju can pull the image directly from dockerhub.
.. code:: bash
juju deploy ./sunbeam-ironic-operator_ubuntu-20.04-amd64.charm --resource ironic-api-image=kolla/ubuntu-binary-ironic-api:wallaby ironic
juju add-relation ironic mysql
juju add-relation ironic keystone
juju add-relation ironic rabbitmq
Test Service
============
Check that the juju status shows the charms is active and no error messages are
preset. Then check the ironic api service is reponding.
.. code:: bash
$ juju status ironic
Model Controller Cloud/Region Version SLA Timestamp
ks micro microk8s/localhost 2.9.22 unsupported 13:31:41Z
App Version Status Scale Charm Store Channel Rev OS Address Message
ironic active 1 sunbeam-ironic-operator local 0 kubernetes 10.152.183.73
Unit Workload Agent Address Ports Message
ironic/0* active idle 10.1.155.106
$ curl http://10.1.155.106:6385 | jq '.'
{
"name": "OpenStack Ironic API",
"description": "Ironic is an OpenStack project which aims to provision baremetal machines.",
"default_version": {
"id": "v1",
"links": [
{
"href": "http://10.1.155.106:6385/v1/",
"rel": "self"
}
],
"status": "CURRENT",
"min_version": "1.1",
"version": "1.72"
},
"versions": [
{
"id": "v1",
"links": [
{
"href": "http://10.1.155.106:6385/v1/",
"rel": "self"
}
],
"status": "CURRENT",
"min_version": "1.1",
"version": "1.72"
}
]
}