diff --git a/ops-sunbeam/README.rst b/ops-sunbeam/README.rst index 4c179be0..cc1bb52b 100644 --- a/ops-sunbeam/README.rst +++ b/ops-sunbeam/README.rst @@ -1,5 +1,30 @@ -Advanced Sunbeam OpenStack +======================================== +Advanced Sunbeam OpenStack Documentation +======================================== + +Tuturials +######### + +`Writing an OpenStack API charm with ASO `_. + +How-Tos +####### + + +`How-To write a pebble handler `_. + +`How-To write a relation handler `_. + +`How-To write a charm context `_. + + +Reference +######### + + + +Concepts +######## -`How to Write a charm with ASO `_. `ASO Concepts `_. diff --git a/ops-sunbeam/aso-charm-init.sh b/ops-sunbeam/aso-charm-init.sh new file mode 100755 index 00000000..c23ea208 --- /dev/null +++ b/ops-sunbeam/aso-charm-init.sh @@ -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 $@ diff --git a/ops-sunbeam/concepts.rst b/ops-sunbeam/concepts.rst index 38aa4f52..f58faee9 100644 --- a/ops-sunbeam/concepts.rst +++ b/ops-sunbeam/concepts.rst @@ -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. diff --git a/ops-sunbeam/cookie-requirements.txt b/ops-sunbeam/cookie-requirements.txt new file mode 100644 index 00000000..c8e988bc --- /dev/null +++ b/ops-sunbeam/cookie-requirements.txt @@ -0,0 +1 @@ +cookiecutter diff --git a/ops-sunbeam/howto-config-context.rst b/ops-sunbeam/howto-config-context.rst new file mode 100644 index 00000000..6681fd14 --- /dev/null +++ b/ops-sunbeam/howto-config-context.rst @@ -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 diff --git a/ops-sunbeam/howto-pebble-handler.rst b/ops-sunbeam/howto-pebble-handler.rst new file mode 100644 index 00000000..6967de8c --- /dev/null +++ b/ops-sunbeam/howto-pebble-handler.rst @@ -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, + ) + ] + diff --git a/ops-sunbeam/howto-relation-handler.rst b/ops-sunbeam/howto-relation-handler.rst new file mode 100644 index 00000000..02f8c6f9 --- /dev/null +++ b/ops-sunbeam/howto-relation-handler.rst @@ -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 + + diff --git a/ops-sunbeam/howto-write-charm.rst b/ops-sunbeam/howto-write-charm.rst deleted file mode 100644 index 98ad5d75..00000000 --- a/ops-sunbeam/howto-write-charm.rst +++ /dev/null @@ -1,432 +0,0 @@ -============= -New API Charm -============= - -The example below will walk through the creation of a basic API charm for the -OpenStack `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 - 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) diff --git a/ops-sunbeam/shared_code/aso-charm-init.py b/ops-sunbeam/shared_code/aso-charm-init.py new file mode 100755 index 00000000..71d0599d --- /dev/null +++ b/ops-sunbeam/shared_code/aso-charm-init.py @@ -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()) diff --git a/ops-sunbeam/shared_code/aso_charm/cookiecutter.json b/ops-sunbeam/shared_code/aso_charm/cookiecutter.json new file mode 100644 index 00000000..a1d09a95 --- /dev/null +++ b/ops-sunbeam/shared_code/aso_charm/cookiecutter.json @@ -0,0 +1,9 @@ +{ + "service_name": "", + "charm_name": "", + "ingress_port": "", + "db_sync_command": "", + "_copy_without_render": [ + "src/templates" + ] +} diff --git a/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/actions.yaml b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/actions.yaml new file mode 100644 index 00000000..88e6195d --- /dev/null +++ b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/actions.yaml @@ -0,0 +1,2 @@ +# NOTE: no actions yet! +{ } diff --git a/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/charmcraft.yaml b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/charmcraft.yaml new file mode 100644 index 00000000..08ce6ba5 --- /dev/null +++ b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/charmcraft.yaml @@ -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 diff --git a/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/config.yaml b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/config.yaml new file mode 100644 index 00000000..606b5357 --- /dev/null +++ b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/config.yaml @@ -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 diff --git a/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/metadata.yaml b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/metadata.yaml new file mode 100644 index 00000000..a2844ce4 --- /dev/null +++ b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/metadata.yaml @@ -0,0 +1,42 @@ +name: {{ cookiecutter.charm_name }} +summary: OpenStack {{ cookiecutter.service_name }} service +maintainer: OpenStack Charmers +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 diff --git a/ops-sunbeam/shared_code/requirements.txt b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/requirements.txt similarity index 86% rename from ops-sunbeam/shared_code/requirements.txt rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/requirements.txt index 32a3d3b9..e40a3ee7 100644 --- a/ops-sunbeam/shared_code/requirements.txt +++ b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/requirements.txt @@ -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 diff --git a/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/charm.py b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/charm.py new file mode 100644 index 00000000..e7dd6538 --- /dev/null +++ b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/charm.py @@ -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) diff --git a/ops-sunbeam/shared_code/templates/ceph.conf.j2 b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/ceph.conf.j2 similarity index 100% rename from ops-sunbeam/shared_code/templates/ceph.conf.j2 rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/ceph.conf.j2 diff --git a/ops-sunbeam/shared_code/templates/parts/database-connection b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/database-connection similarity index 100% rename from ops-sunbeam/shared_code/templates/parts/database-connection rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/database-connection diff --git a/ops-sunbeam/shared_code/templates/parts/identity-connection b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/identity-data similarity index 100% rename from ops-sunbeam/shared_code/templates/parts/identity-connection rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/identity-data diff --git a/ops-sunbeam/shared_code/templates/parts/section-database b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-database similarity index 100% rename from ops-sunbeam/shared_code/templates/parts/section-database rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-database diff --git a/ops-sunbeam/shared_code/templates/parts/section-federation b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-federation similarity index 100% rename from ops-sunbeam/shared_code/templates/parts/section-federation rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-federation diff --git a/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-identity b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-identity new file mode 100644 index 00000000..7568a9a4 --- /dev/null +++ b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-identity @@ -0,0 +1,2 @@ +[keystone_authtoken] +{% include "parts/identity-data" %} diff --git a/ops-sunbeam/shared_code/templates/parts/section-middleware b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-middleware similarity index 100% rename from ops-sunbeam/shared_code/templates/parts/section-middleware rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-middleware diff --git a/ops-sunbeam/shared_code/templates/parts/section-signing b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-signing similarity index 100% rename from ops-sunbeam/shared_code/templates/parts/section-signing rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/parts/section-signing diff --git a/ops-sunbeam/shared_code/templates/wsgi-template.conf.j2 b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/wsgi-template.conf.j2 similarity index 100% rename from ops-sunbeam/shared_code/templates/wsgi-template.conf.j2 rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/src/templates/wsgi-template.conf.j2 diff --git a/ops-sunbeam/shared_code/test-requirements.txt b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/test-requirements.txt similarity index 100% rename from ops-sunbeam/shared_code/test-requirements.txt rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/test-requirements.txt diff --git a/ops-sunbeam/shared_code/tox.ini b/ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/tox.ini similarity index 100% rename from ops-sunbeam/shared_code/tox.ini rename to ops-sunbeam/shared_code/aso_charm/{{cookiecutter.service_name}}/tox.ini diff --git a/ops-sunbeam/shared_code/templates b/ops-sunbeam/shared_code/templates new file mode 120000 index 00000000..c6df8ed5 --- /dev/null +++ b/ops-sunbeam/shared_code/templates @@ -0,0 +1 @@ +aso_charm/{{cookiecutter.service_name}}/src/templates \ No newline at end of file diff --git a/ops-sunbeam/shared_code/templates/parts/section-identity b/ops-sunbeam/shared_code/templates/parts/section-identity deleted file mode 100644 index ad591939..00000000 --- a/ops-sunbeam/shared_code/templates/parts/section-identity +++ /dev/null @@ -1,2 +0,0 @@ -[keystone_authtoken] -{% include "parts/identity-connection" %} diff --git a/ops-sunbeam/tox.ini b/ops-sunbeam/tox.ini index adb24f74..028fa55a 100644 --- a/ops-sunbeam/tox.ini +++ b/ops-sunbeam/tox.ini @@ -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 diff --git a/ops-sunbeam/writing-OS-API-charm.rst b/ops-sunbeam/writing-OS-API-charm.rst new file mode 100644 index 00000000..bc1a0ede --- /dev/null +++ b/ops-sunbeam/writing-OS-API-charm.rst @@ -0,0 +1,154 @@ +============= +New API Charm +============= + +The example below will walk through the creation of a basic API charm for the +OpenStack `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" + } + ] + } \ No newline at end of file