commit 955c89fb54ae75476d9b09bd384a55a69507929c Author: Liam Young Date: Wed Aug 9 14:44:02 2023 +0000 First cut diff --git a/charms/ceilometer-k8s/.gitignore b/charms/ceilometer-k8s/.gitignore new file mode 100644 index 00000000..24ff2e41 --- /dev/null +++ b/charms/ceilometer-k8s/.gitignore @@ -0,0 +1,11 @@ +venv/ +build/ +*.charm +.tox/ +.coverage +__pycache__/ +*.py[cod] +.idea +.vscode/ +*.swp +.stestr/ diff --git a/charms/ceilometer-k8s/.gitreview b/charms/ceilometer-k8s/.gitreview new file mode 100644 index 00000000..1b4bcda6 --- /dev/null +++ b/charms/ceilometer-k8s/.gitreview @@ -0,0 +1,5 @@ +[gerrit] +host=review.opendev.org +port=29418 +project=openstack/charm-ceilometer-k8s.git +defaultbranch=main diff --git a/charms/ceilometer-k8s/.stestr.conf b/charms/ceilometer-k8s/.stestr.conf new file mode 100644 index 00000000..e4750de4 --- /dev/null +++ b/charms/ceilometer-k8s/.stestr.conf @@ -0,0 +1,3 @@ +[DEFAULT] +test_path=./tests/unit +top_dir=./tests diff --git a/charms/ceilometer-k8s/CONTRIBUTING.md b/charms/ceilometer-k8s/CONTRIBUTING.md new file mode 100644 index 00000000..2969183e --- /dev/null +++ b/charms/ceilometer-k8s/CONTRIBUTING.md @@ -0,0 +1,33 @@ +# Contributing + +To make contributions to this charm, you'll need a working [development setup](https://juju.is/docs/sdk/dev-setup). + +You can use the environments created by `tox` for development: + +```shell +tox --notest -e unit +source .tox/unit/bin/activate +``` + +## Testing + +This project uses `tox` for managing test environments. There are some pre-configured environments +that can be used for linting and formatting code when you're preparing contributions to the charm: + +```shell +tox -e fmt # update your code according to linting rules +tox -e lint # code style +tox -e unit # unit tests +tox -e integration # integration tests +tox # runs 'lint' and 'unit' environments +``` + +## Build the charm + +Build the charm in this git repository using: + +```shell +charmcraft pack +``` + + + +# ceilometer-k8s + +Charmhub package name: operator-template +More information: https://charmhub.io/ceilometer-k8s + +Describe your charm in one or two sentences. + +## Other resources + + + +- [Read more](https://example.com) + +- [Contributing](CONTRIBUTING.md) + +- See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms. diff --git a/charms/ceilometer-k8s/actions.yaml b/charms/ceilometer-k8s/actions.yaml new file mode 100644 index 00000000..88e6195d --- /dev/null +++ b/charms/ceilometer-k8s/actions.yaml @@ -0,0 +1,2 @@ +# NOTE: no actions yet! +{ } diff --git a/charms/ceilometer-k8s/charmcraft.yaml b/charms/ceilometer-k8s/charmcraft.yaml new file mode 100644 index 00000000..ac49568b --- /dev/null +++ b/charms/ceilometer-k8s/charmcraft.yaml @@ -0,0 +1,30 @@ +type: "charm" +bases: + - build-on: + - name: "ubuntu" + channel: "22.04" + run-on: + - name: "ubuntu" + channel: "22.04" +parts: + update-certificates: + plugin: nil + override-build: | + apt update + apt install -y ca-certificates + update-ca-certificates + + charm: + after: [update-certificates] + build-packages: + - git + - libffi-dev + - libssl-dev + - rustc + - cargo + - pkg-config + charm-binary-python-packages: + - cryptography + - jsonschema + - jinja2 + - git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam diff --git a/charms/ceilometer-k8s/config.yaml b/charms/ceilometer-k8s/config.yaml new file mode 100644 index 00000000..ac6a1965 --- /dev/null +++ b/charms/ceilometer-k8s/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/charms/ceilometer-k8s/fetch-libs.sh b/charms/ceilometer-k8s/fetch-libs.sh new file mode 100755 index 00000000..f7433018 --- /dev/null +++ b/charms/ceilometer-k8s/fetch-libs.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +echo "INFO: Fetching libs from charmhub." +# charmcraft fetch-lib charms.data_platform_libs.v0.database_requires +# charmcraft fetch-lib charms.keystone_k8s.v1.identity_service +# charmcraft fetch-lib charms.rabbitmq_k8s.v0.rabbitmq +# charmcraft fetch-lib charms.traefik_k8s.v1.ingress diff --git a/charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py b/charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py new file mode 100644 index 00000000..e3f4565d --- /dev/null +++ b/charms/ceilometer-k8s/lib/charms/keystone_k8s/v0/identity_credentials.py @@ -0,0 +1,458 @@ +"""IdentityCredentialsProvides and Requires module. + + +This library contains the Requires and Provides classes for handling +the identity_credentials interface. + +Import `IdentityCredentialsRequires` in your charm, with the charm object and the +relation name: + - self + - "identity_credentials" + +Also provide additional parameters to the charm object: + - service + - internal_url + - public_url + - admin_url + - region + - username + - vhost + +Two events are also available to respond to: + - connected + - ready + - goneaway + +A basic example showing the usage of this relation follows: + +``` +from charms.keystone_k8s.v0.identity_credentials import IdentityCredentialsRequires + +class IdentityCredentialsClientCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + # IdentityCredentials Requires + self.identity_credentials = IdentityCredentialsRequires( + self, "identity_credentials", + service = "my-service" + internal_url = "http://internal-url" + public_url = "http://public-url" + admin_url = "http://admin-url" + region = "region" + ) + self.framework.observe( + self.identity_credentials.on.connected, self._on_identity_credentials_connected) + self.framework.observe( + self.identity_credentials.on.ready, self._on_identity_credentials_ready) + self.framework.observe( + self.identity_credentials.on.goneaway, self._on_identity_credentials_goneaway) + + def _on_identity_credentials_connected(self, event): + '''React to the IdentityCredentials connected event. + + This event happens when IdentityCredentials relation is added to the + model before credentials etc have been provided. + ''' + # Do something before the relation is complete + pass + + def _on_identity_credentials_ready(self, event): + '''React to the IdentityCredentials ready event. + + The IdentityCredentials interface will use the provided config for the + request to the identity server. + ''' + # IdentityCredentials Relation is ready. Do something with the completed relation. + pass + + def _on_identity_credentials_goneaway(self, event): + '''React to the IdentityCredentials goneaway event. + + This event happens when an IdentityCredentials relation is removed. + ''' + # IdentityCredentials Relation has goneaway. shutdown services or suchlike + pass +``` +""" + +import logging + +from ops.framework import ( + StoredState, + EventBase, + ObjectEvents, + EventSource, + Object, +) +from ops.model import ( + Relation, + SecretNotFoundError, +) + +# The unique Charmhub library identifier, never change it +LIBID = "b5fa18d4427c4ab9a269c3a2fbed545c" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 3 + +logger = logging.getLogger(__name__) + + +class IdentityCredentialsConnectedEvent(EventBase): + """IdentityCredentials connected Event.""" + + pass + + +class IdentityCredentialsReadyEvent(EventBase): + """IdentityCredentials ready for use Event.""" + + pass + + +class IdentityCredentialsGoneAwayEvent(EventBase): + """IdentityCredentials relation has gone-away Event""" + + pass + + +class IdentityCredentialsServerEvents(ObjectEvents): + """Events class for `on`""" + + connected = EventSource(IdentityCredentialsConnectedEvent) + ready = EventSource(IdentityCredentialsReadyEvent) + goneaway = EventSource(IdentityCredentialsGoneAwayEvent) + + +class IdentityCredentialsRequires(Object): + """ + IdentityCredentialsRequires class + """ + + on = IdentityCredentialsServerEvents() + _stored = StoredState() + + def __init__(self, charm, relation_name: str): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.framework.observe( + self.charm.on[relation_name].relation_joined, + self._on_identity_credentials_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_identity_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_departed, + self._on_identity_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_identity_credentials_relation_broken, + ) + + def _on_identity_credentials_relation_joined(self, event): + """IdentityCredentials relation joined.""" + logging.debug("IdentityCredentials on_joined") + self.on.connected.emit() + self.request_credentials() + + def _on_identity_credentials_relation_changed(self, event): + """IdentityCredentials relation changed.""" + logging.debug("IdentityCredentials on_changed") + try: + self.on.ready.emit() + except (AttributeError, KeyError): + logger.exception('Error when emitting event') + + def _on_identity_credentials_relation_broken(self, event): + """IdentityCredentials relation broken.""" + logging.debug("IdentityCredentials on_broken") + self.on.goneaway.emit() + + @property + def _identity_credentials_rel(self) -> Relation: + """The IdentityCredentials relation.""" + return self.framework.model.get_relation(self.relation_name) + + def get_remote_app_data(self, key: str) -> str: + """Return the value for the given key from remote app data.""" + data = self._identity_credentials_rel.data[self._identity_credentials_rel.app] + return data.get(key) + + @property + def api_version(self) -> str: + """Return the api_version.""" + return self.get_remote_app_data('api-version') + + @property + def auth_host(self) -> str: + """Return the auth_host.""" + return self.get_remote_app_data('auth-host') + + @property + def auth_port(self) -> str: + """Return the auth_port.""" + return self.get_remote_app_data('auth-port') + + @property + def auth_protocol(self) -> str: + """Return the auth_protocol.""" + return self.get_remote_app_data('auth-protocol') + + @property + def internal_host(self) -> str: + """Return the internal_host.""" + return self.get_remote_app_data('internal-host') + + @property + def internal_port(self) -> str: + """Return the internal_port.""" + return self.get_remote_app_data('internal-port') + + @property + def internal_protocol(self) -> str: + """Return the internal_protocol.""" + return self.get_remote_app_data('internal-protocol') + + @property + def credentials(self) -> str: + return self.get_remote_app_data('credentials') + + @property + def username(self) -> str: + credentials_id = self.get_remote_app_data('credentials') + if not credentials_id: + return None + + try: + credentials = self.charm.model.get_secret(id=credentials_id) + return credentials.get_content().get("username") + except SecretNotFoundError: + logger.warning(f"Secret {credentials_id} not found") + return None + + @property + def password(self) -> str: + credentials_id = self.get_remote_app_data('credentials') + if not credentials_id: + return None + + try: + credentials = self.charm.model.get_secret(id=credentials_id) + return credentials.get_content().get("password") + except SecretNotFoundError: + logger.warning(f"Secret {credentials_id} not found") + return None + + @property + def project_name(self) -> str: + """Return the project name.""" + return self.get_remote_app_data('project-name') + + @property + def project_id(self) -> str: + """Return the project id.""" + return self.get_remote_app_data('project-id') + + @property + def user_domain_name(self) -> str: + """Return the name of the user domain.""" + return self.get_remote_app_data('user-domain-name') + + @property + def user_domain_id(self) -> str: + """Return the id of the user domain.""" + return self.get_remote_app_data('user-domain-id') + + @property + def project_domain_name(self) -> str: + """Return the name of the project domain.""" + return self.get_remote_app_data('project-domain-name') + + @property + def project_domain_id(self) -> str: + """Return the id of the project domain.""" + return self.get_remote_app_data('project-domain-id') + + @property + def region(self) -> str: + """Return the region for the auth urls.""" + return self.get_remote_app_data('region') + + @property + def internal_endpoint(self) -> str: + """Return the region for the internal auth url.""" + return self.get_remote_app_data('internal-endpoint') + + @property + def public_endpoint(self) -> str: + """Return the region for the public auth url.""" + return self.get_remote_app_data('public-endpoint') + + @property + def admin_role(self) -> str: + """Return the admin_role.""" + return self.get_remote_app_data('admin-role') + + def request_credentials(self) -> None: + """Request credentials from the IdentityCredentials server.""" + if self.model.unit.is_leader(): + logging.debug(f'Requesting credentials for {self.charm.app.name}') + app_data = self._identity_credentials_rel.data[self.charm.app] + app_data['username'] = self.charm.app.name + + +class HasIdentityCredentialsClientsEvent(EventBase): + """Has IdentityCredentialsClients Event.""" + + pass + + +class ReadyIdentityCredentialsClientsEvent(EventBase): + """IdentityCredentialsClients Ready Event.""" + + def __init__(self, handle, relation_id, relation_name, username): + super().__init__(handle) + self.relation_id = relation_id + self.relation_name = relation_name + self.username = username + + def snapshot(self): + return { + "relation_id": self.relation_id, + "relation_name": self.relation_name, + "username": self.username, + } + + def restore(self, snapshot): + super().restore(snapshot) + self.relation_id = snapshot["relation_id"] + self.relation_name = snapshot["relation_name"] + self.username = snapshot["username"] + + +class IdentityCredentialsClientsGoneAwayEvent(EventBase): + """Has IdentityCredentialsClientsGoneAwayEvent Event.""" + + pass + + +class IdentityCredentialsClientEvents(ObjectEvents): + """Events class for `on`""" + + has_identity_credentials_clients = EventSource( + HasIdentityCredentialsClientsEvent + ) + ready_identity_credentials_clients = EventSource( + ReadyIdentityCredentialsClientsEvent + ) + identity_credentials_clients_gone = EventSource( + IdentityCredentialsClientsGoneAwayEvent + ) + + +class IdentityCredentialsProvides(Object): + """ + IdentityCredentialsProvides class + """ + + on = IdentityCredentialsClientEvents() + _stored = StoredState() + + def __init__(self, charm, relation_name): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.framework.observe( + self.charm.on[relation_name].relation_joined, + self._on_identity_credentials_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_identity_credentials_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_identity_credentials_relation_broken, + ) + + def _on_identity_credentials_relation_joined(self, event): + """Handle IdentityCredentials joined.""" + logging.debug("IdentityCredentialsProvides on_joined") + self.on.has_identity_credentials_clients.emit() + + def _on_identity_credentials_relation_changed(self, event): + """Handle IdentityCredentials changed.""" + logging.debug("IdentityCredentials on_changed") + REQUIRED_KEYS = ['username'] + + values = [ + event.relation.data[event.relation.app].get(k) + for k in REQUIRED_KEYS + ] + # Validate data on the relation + if all(values): + username = event.relation.data[event.relation.app]['username'] + self.on.ready_identity_credentials_clients.emit( + event.relation.id, + event.relation.name, + username, + ) + + def _on_identity_credentials_relation_broken(self, event): + """Handle IdentityCredentials broken.""" + logging.debug("IdentityCredentialsProvides on_departed") + self.on.identity_credentials_clients_gone.emit() + + def set_identity_credentials(self, relation_name: int, + relation_id: str, + api_version: str, + auth_host: str, + auth_port: str, + auth_protocol: str, + internal_host: str, + internal_port: str, + internal_protocol: str, + credentials: str, + project_name: str, + project_id: str, + user_domain_name: str, + user_domain_id: str, + project_domain_name: str, + project_domain_id: str, + region: str, + admin_role: str): + logging.debug("Setting identity_credentials connection information.") + _identity_credentials_rel = None + for relation in self.framework.model.relations[relation_name]: + if relation.id == relation_id: + _identity_credentials_rel = relation + if not _identity_credentials_rel: + # Relation has disappeared so don't send the data + return + app_data = _identity_credentials_rel.data[self.charm.app] + app_data["api-version"] = api_version + app_data["auth-host"] = auth_host + app_data["auth-port"] = str(auth_port) + app_data["auth-protocol"] = auth_protocol + app_data["internal-host"] = internal_host + app_data["internal-port"] = str(internal_port) + app_data["internal-protocol"] = internal_protocol + app_data["credentials"] = credentials + app_data["project-name"] = project_name + app_data["project-id"] = project_id + app_data["user-domain-name"] = user_domain_name + app_data["user-domain-id"] = user_domain_id + app_data["project-domain-name"] = project_domain_name + app_data["project-domain-id"] = project_domain_id + app_data["region"] = region + app_data["internal-endpoint"] = self.charm.internal_endpoint + app_data["public-endpoint"] = self.charm.public_endpoint + app_data["admin-role"] = admin_role diff --git a/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py b/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py new file mode 100644 index 00000000..c7df2409 --- /dev/null +++ b/charms/ceilometer-k8s/lib/charms/rabbitmq_k8s/v0/rabbitmq.py @@ -0,0 +1,286 @@ +"""RabbitMQProvides and Requires module. + +This library contains the Requires and Provides classes for handling +the rabbitmq interface. + +Import `RabbitMQRequires` in your charm, with the charm object and the +relation name: + - self + - "amqp" + +Also provide two additional parameters to the charm object: + - username + - vhost + +Two events are also available to respond to: + - connected + - ready + - goneaway + +A basic example showing the usage of this relation follows: + +``` +from charms.rabbitmq_k8s.v0.rabbitmq import RabbitMQRequires + +class RabbitMQClientCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + # RabbitMQ Requires + self.amqp = RabbitMQRequires( + self, "amqp", + username="myusername", + vhost="vhostname" + ) + self.framework.observe( + self.amqp.on.connected, self._on_amqp_connected) + self.framework.observe( + self.amqp.on.ready, self._on_amqp_ready) + self.framework.observe( + self.amqp.on.goneaway, self._on_amqp_goneaway) + + def _on_amqp_connected(self, event): + '''React to the RabbitMQ connected event. + + This event happens when n RabbitMQ relation is added to the + model before credentials etc have been provided. + ''' + # Do something before the relation is complete + pass + + def _on_amqp_ready(self, event): + '''React to the RabbitMQ ready event. + + The RabbitMQ interface will use the provided username and vhost for the + request to the rabbitmq server. + ''' + # RabbitMQ Relation is ready. Do something with the completed relation. + pass + + def _on_amqp_goneaway(self, event): + '''React to the RabbitMQ goneaway event. + + This event happens when an RabbitMQ relation is removed. + ''' + # RabbitMQ Relation has goneaway. shutdown services or suchlike + pass +``` +""" + +# The unique Charmhub library identifier, never change it +LIBID = "45622352791142fd9cf87232e3bd6f2a" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 + +import logging + +from ops.framework import ( + StoredState, + EventBase, + ObjectEvents, + EventSource, + Object, +) + +from ops.model import Relation + +from typing import List + +logger = logging.getLogger(__name__) + + +class RabbitMQConnectedEvent(EventBase): + """RabbitMQ connected Event.""" + + pass + + +class RabbitMQReadyEvent(EventBase): + """RabbitMQ ready for use Event.""" + + pass + + +class RabbitMQGoneAwayEvent(EventBase): + """RabbitMQ relation has gone-away Event""" + + pass + + +class RabbitMQServerEvents(ObjectEvents): + """Events class for `on`""" + + connected = EventSource(RabbitMQConnectedEvent) + ready = EventSource(RabbitMQReadyEvent) + goneaway = EventSource(RabbitMQGoneAwayEvent) + + +class RabbitMQRequires(Object): + """ + RabbitMQRequires class + """ + + on = RabbitMQServerEvents() + + def __init__(self, charm, relation_name: str, username: str, vhost: str): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.username = username + self.vhost = vhost + self.framework.observe( + self.charm.on[relation_name].relation_joined, + self._on_amqp_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_amqp_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_departed, + self._on_amqp_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_amqp_relation_broken, + ) + + def _on_amqp_relation_joined(self, event): + """RabbitMQ relation joined.""" + logging.debug("RabbitMQRabbitMQRequires on_joined") + self.on.connected.emit() + self.request_access(self.username, self.vhost) + + def _on_amqp_relation_changed(self, event): + """RabbitMQ relation changed.""" + logging.debug("RabbitMQRabbitMQRequires on_changed/departed") + if self.password: + self.on.ready.emit() + + def _on_amqp_relation_broken(self, event): + """RabbitMQ relation broken.""" + logging.debug("RabbitMQRabbitMQRequires on_broken") + self.on.goneaway.emit() + + @property + def _amqp_rel(self) -> Relation: + """The RabbitMQ relation.""" + return self.framework.model.get_relation(self.relation_name) + + @property + def password(self) -> str: + """Return the RabbitMQ password from the server side of the relation.""" + return self._amqp_rel.data[self._amqp_rel.app].get("password") + + @property + def hostname(self) -> str: + """Return the hostname from the RabbitMQ relation""" + return self._amqp_rel.data[self._amqp_rel.app].get("hostname") + + @property + def ssl_port(self) -> str: + """Return the SSL port from the RabbitMQ relation""" + return self._amqp_rel.data[self._amqp_rel.app].get("ssl_port") + + @property + def ssl_ca(self) -> str: + """Return the SSL port from the RabbitMQ relation""" + return self._amqp_rel.data[self._amqp_rel.app].get("ssl_ca") + + @property + def hostnames(self) -> List[str]: + """Return a list of remote RMQ hosts from the RabbitMQ relation""" + _hosts = [] + for unit in self._amqp_rel.units: + _hosts.append(self._amqp_rel.data[unit].get("ingress-address")) + return _hosts + + def request_access(self, username: str, vhost: str) -> None: + """Request access to the RabbitMQ server.""" + if self.model.unit.is_leader(): + logging.debug("Requesting RabbitMQ user and vhost") + self._amqp_rel.data[self.charm.app]["username"] = username + self._amqp_rel.data[self.charm.app]["vhost"] = vhost + + +class HasRabbitMQClientsEvent(EventBase): + """Has RabbitMQClients Event.""" + + pass + + +class ReadyRabbitMQClientsEvent(EventBase): + """RabbitMQClients Ready Event.""" + + pass + + +class RabbitMQClientEvents(ObjectEvents): + """Events class for `on`""" + + has_amqp_clients = EventSource(HasRabbitMQClientsEvent) + ready_amqp_clients = EventSource(ReadyRabbitMQClientsEvent) + + +class RabbitMQProvides(Object): + """ + RabbitMQProvides class + """ + + on = RabbitMQClientEvents() + + def __init__(self, charm, relation_name, callback): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.callback = callback + self.framework.observe( + self.charm.on[relation_name].relation_joined, + self._on_amqp_relation_joined, + ) + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_amqp_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_amqp_relation_broken, + ) + + def _on_amqp_relation_joined(self, event): + """Handle RabbitMQ joined.""" + logging.debug("RabbitMQRabbitMQProvides on_joined data={}" + .format(event.relation.data[event.relation.app])) + self.on.has_amqp_clients.emit() + + def _on_amqp_relation_changed(self, event): + """Handle RabbitMQ changed.""" + logging.debug("RabbitMQRabbitMQProvides on_changed data={}" + .format(event.relation.data[event.relation.app])) + # Validate data on the relation + if self.username(event) and self.vhost(event): + self.on.ready_amqp_clients.emit() + if self.charm.unit.is_leader(): + self.callback(event, self.username(event), self.vhost(event)) + else: + logging.warning("Received RabbitMQ changed event without the " + "expected keys ('username', 'vhost') in the " + "application data bag. Incompatible charm in " + "other end of relation?") + + def _on_amqp_relation_broken(self, event): + """Handle RabbitMQ broken.""" + logging.debug("RabbitMQRabbitMQProvides on_departed") + # TODO clear data on the relation + + def username(self, event): + """Return the RabbitMQ username from the client side of the relation.""" + return event.relation.data[event.relation.app].get("username") + + def vhost(self, event): + """Return the RabbitMQ vhost from the client side of the relation.""" + return event.relation.data[event.relation.app].get("vhost") diff --git a/charms/ceilometer-k8s/metadata.yaml b/charms/ceilometer-k8s/metadata.yaml new file mode 100644 index 00000000..6ac72a1b --- /dev/null +++ b/charms/ceilometer-k8s/metadata.yaml @@ -0,0 +1,46 @@ +name: ceilometer-k8s +summary: OpenStack ceilometer service +maintainer: OpenStack Charmers +description: | + OpenStack ceilometer 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: 22.04/stable +assumes: + - k8s-api + - juju >= 3.2 +tags: +- openstack +source: https://opendev.org/openstack/charm-ceilometer-k8s +issues: https://bugs.launchpad.net/charm-ceilometer-k8s + +containers: + ceilometer-central: + resource: ceilometer-central-image + ceilometer-notification: + resource: ceilometer-notification-image + +resources: + ceilometer-central-image: + type: oci-image + description: OCI image for OpenStack ceilometer + upstream-source: kolla/ubuntu-binary-ceilometer-central:yoga + ceilometer-notification-image: + type: oci-image + description: OCI image for OpenStack ceilometer + upstream-source: kolla/ubuntu-binary-ceilometer-notification:yoga + +requires: + amqp: + interface: rabbitmq + identity-credentials: + interface: keystone-credentials + limit: 1 + +peers: + peers: + interface: ceilometer-peer diff --git a/charms/ceilometer-k8s/osci.yaml b/charms/ceilometer-k8s/osci.yaml new file mode 100644 index 00000000..48757511 --- /dev/null +++ b/charms/ceilometer-k8s/osci.yaml @@ -0,0 +1,10 @@ +- project: + templates: + - charm-publish-jobs + vars: + needs_charm_build: true + charm_build_name: ceilometer-k8s + build_type: charmcraft + publish_charm: true + charmcraft_channel: 2.0/stable + publish_channel: 2023.1/edge diff --git a/charms/ceilometer-k8s/pyproject.toml b/charms/ceilometer-k8s/pyproject.toml new file mode 100644 index 00000000..2edc519a --- /dev/null +++ b/charms/ceilometer-k8s/pyproject.toml @@ -0,0 +1,33 @@ +# Testing tools configuration +[tool.coverage.run] +branch = true + +[tool.coverage.report] +show_missing = true + +[tool.pytest.ini_options] +minversion = "6.0" +log_cli_level = "INFO" + +# Formatting tools configuration +[tool.black] +line-length = 99 +target-version = ["py38"] + +[tool.isort] +line_length = 99 +profile = "black" + +# Linting tools configuration +[tool.flake8] +max-line-length = 99 +max-doc-length = 99 +max-complexity = 10 +exclude = [".git", "__pycache__", ".tox", "build", "dist", "*.egg_info", "venv"] +select = ["E", "W", "F", "C", "N", "R", "D", "H"] +# Ignore W503, E501 because using black creates errors with this +# Ignore D107 Missing docstring in __init__ +ignore = ["W503", "E501", "D107"] +# D100, D101, D102, D103: Ignore missing docstrings in tests +per-file-ignores = ["tests/*:D100,D101,D102,D103,D104"] +docstring-convention = "google" diff --git a/charms/ceilometer-k8s/rename.sh b/charms/ceilometer-k8s/rename.sh new file mode 100755 index 00000000..d0c35c97 --- /dev/null +++ b/charms/ceilometer-k8s/rename.sh @@ -0,0 +1,13 @@ +#!/bin/bash +charm=$(grep "charm_build_name" osci.yaml | awk '{print $2}') +echo "renaming ${charm}_*.charm to ${charm}.charm" +echo -n "pwd: " +pwd +ls -al +echo "Removing bad downloaded charm maybe?" +if [[ -e "${charm}.charm" ]]; +then + rm "${charm}.charm" +fi +echo "Renaming charm here." +mv ${charm}_*.charm ${charm}.charm diff --git a/charms/ceilometer-k8s/requirements.txt b/charms/ceilometer-k8s/requirements.txt new file mode 100644 index 00000000..20a477f7 --- /dev/null +++ b/charms/ceilometer-k8s/requirements.txt @@ -0,0 +1,8 @@ +ops +jinja2 +git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam +lightkube + +# Uncomment below if charm relates to ceph +# git+https://github.com/openstack/charm-ops-interface-ceph-client#egg=interface_ceph_client +# git+https://github.com/juju/charm-helpers.git#egg=charmhelpers diff --git a/charms/ceilometer-k8s/src/charm.py b/charms/ceilometer-k8s/src/charm.py new file mode 100755 index 00000000..83416218 --- /dev/null +++ b/charms/ceilometer-k8s/src/charm.py @@ -0,0 +1,141 @@ +#!/usr/bin/env python3 +"""Ceilometer Operator Charm. + +This charm provide Ceilometer services as part of an OpenStack deployment +""" + +import logging +import uuid +from typing import List + +import ops.framework +import ops_sunbeam.charm as sunbeam_charm +import ops_sunbeam.container_handlers as container_handlers +import ops_sunbeam.core as core +from ops.main import main + +logger = logging.getLogger(__name__) + +CEILOMETER_CENTRAL_CONTAINER = "ceilometer-central" +CEILOMETER_NOTIFICATION_CONTAINER = "ceilometer-notification" + + +class CeilometerCentralPebbleHandler(container_handlers.ServicePebbleHandler): + """Pebble handler for ceilometer-central service.""" + + def get_layer(self) -> dict: + """ceilometer-central service pebble layer. + + :returns: pebble layer configuration for ceilometer-central service + :rtype: dict + """ + return { + "summary": "ceilometer-central layer", + "description": "pebble config layer for ceilometer-central service", + "services": { + "ceilometer-central": { + "override": "replace", + "summary": "ceilometer-central", + "command": "/usr/bin/ceilometer-polling --config-file=/etc/ceilometer/ceilometer.conf --polling-namespaces central --use-syslog", + "startup": "enabled", + "user": "ceilometer", + "group": "ceilometer", + }, + }, + } + + +class CeilometerNotificationPebbleHandler(container_handlers.ServicePebbleHandler): + """Pebble handler for ceilometer-notification service.""" + + def get_layer(self) -> dict: + """ceilometer-notification service pebble layer. + + :returns: pebble layer configuration for ceilometer-notification service + :rtype: dict + """ + return { + "summary": "ceilometer-notification layer", + "description": "pebble config layer for ceilometer-notification service", + "services": { + "ceilometer-notification": { + "override": "replace", + "summary": "ceilometer-notification", + "command": "/usr/bin/ceilometer-agent-notification --config-file=/etc/ceilometer/ceilometer.conf --use-syslog", + "startup": "enabled", + "user": "ceilometer", + "group": "ceilometer", + }, + }, + } + + +class CeilometerOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S): + """Charm the service.""" + + service_name = "ceilometer" + shared_metering_secret_key = "shared-metering-secret" + + mandatory_relations = {"amqp", "identity-credentials"} + + def get_shared_meteringsecret(self): + """Return the shared metering secret.""" + return self.leader_get(self.shared_metering_secret_key) + + def set_shared_meteringsecret(self): + """Store the shared metering secret.""" + self.leader_set({self.shared_metering_secret_key: str(uuid.uuid1())}) + + def configure_charm(self, event: ops.framework.EventBase) -> None: + """Callback handler for nova operator configuration.""" + if not self.peers.ready: + return + metering_secret = self.get_shared_meteringsecret() + if metering_secret: + logger.debug("Found metering secret in leader DB") + else: + if self.unit.is_leader(): + logger.debug("Creating metering secret") + self.set_shared_meteringsecret() + else: + logger.debug("Metadata secret not ready") + return + super().configure_charm(event) + + @property + def container_configs(self) -> List[core.ContainerConfigFile]: + """Container configurations for the operator.""" + _cconfigs = [ + core.ContainerConfigFile( + "/etc/ceilometer/ceilometer.conf", + "root", + "ceilometer", + 0o640, + ), + ] + return _cconfigs + + def get_pebble_handlers(self) -> List[container_handlers.PebbleHandler]: + """Pebble handlers for the operator.""" + return [ + CeilometerCentralPebbleHandler( + self, + CEILOMETER_CENTRAL_CONTAINER, + "ceilometer-central", + self.container_configs, + self.template_dir, + self.configure_charm, + ), + CeilometerNotificationPebbleHandler( + self, + CEILOMETER_NOTIFICATION_CONTAINER, + "ceilometer-notification", + self.container_configs, + self.template_dir, + self.configure_charm, + ), + ] + + +if __name__ == "__main__": + main(CeilometerOperatorCharm) diff --git a/charms/ceilometer-k8s/src/templates/ceilometer.conf b/charms/ceilometer-k8s/src/templates/ceilometer.conf new file mode 100644 index 00000000..f406d734 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/ceilometer.conf @@ -0,0 +1,31 @@ +[DEFAULT] +debug = {{ options.debug }} +# event_pipeline_cfg_file = /etc/ceilometer/event_pipeline.yaml + +meter_dispatchers = gnocchi +event_dispatchers = gnocchi + +{% if amqp.transport_url -%} +transport_url = {{ amqp.transport_url }} +{%- endif %} + +[notification] +{% if amqp.transport_url -%} +messaging_urls = {{ amqp.transport_url }} +{% endif %} + +[polling] +batch_size = 50 + + +[publisher] +telemetry_secret = {{ peers.shared_metering_secret }} + +[gnocchi] +filter_service_activity = False +archive_policy = low + +[keystone_authtoken] +{% include "parts/identity-data-id-creds" %} + +{% include "parts/section-service-user-id-creds" %} diff --git a/charms/ceilometer-k8s/src/templates/ceph.conf.j2 b/charms/ceilometer-k8s/src/templates/ceph.conf.j2 new file mode 100644 index 00000000..c293ae90 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/ceph.conf.j2 @@ -0,0 +1,22 @@ +############################################################################### +# [ WARNING ] +# ceph configuration file maintained in aso +# local changes may be overwritten. +############################################################################### +[global] +{% if ceph.auth -%} +auth_supported = {{ ceph.auth }} +mon host = {{ ceph.mon_hosts }} +{% endif -%} +keyring = /etc/ceph/$cluster.$name.keyring +log to syslog = false +err to syslog = false +clog to syslog = false +{% if ceph.rbd_features %} +rbd default features = {{ ceph.rbd_features }} +{% endif %} + +[client] +{% if ceph_config.rbd_default_data_pool -%} +rbd default data pool = {{ ceph_config.rbd_default_data_pool }} +{% endif %} diff --git a/charms/ceilometer-k8s/src/templates/parts/database-connection b/charms/ceilometer-k8s/src/templates/parts/database-connection new file mode 100644 index 00000000..1fd70ce2 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/database-connection @@ -0,0 +1,3 @@ +{% if database.connection -%} +connection = {{ database.connection }} +{% endif -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data b/charms/ceilometer-k8s/src/templates/parts/identity-data new file mode 100644 index 00000000..706d9d13 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/identity-data @@ -0,0 +1,23 @@ +{% if identity_service.admin_auth_url -%} +auth_url = {{ identity_service.admin_auth_url }} +interface = admin +{% elif identity_service.internal_auth_url -%} +auth_url = {{ identity_service.internal_auth_url }} +interface = internal +{% elif identity_service.internal_host -%} +auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} +interface = internal +{% endif -%} +{% if identity_service.public_auth_url -%} +www_authenticate_uri = {{ identity_service.public_auth_url }} +{% elif identity_service.internal_host -%} +www_authenticate_uri = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} +{% endif -%} +auth_type = password +project_domain_name = {{ identity_service.service_domain_name }} +user_domain_name = {{ identity_service.service_domain_name }} +project_name = {{ identity_service.service_project_name }} +username = {{ identity_service.service_user_name }} +password = {{ identity_service.service_password }} +service_token_roles = {{ identity_service.admin_role }} +service_token_roles_required = True diff --git a/charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds b/charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds new file mode 100644 index 00000000..e396178a --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/identity-data-id-creds @@ -0,0 +1,23 @@ +{% if identity_credentials.admin_auth_url -%} +auth_url = {{ identity_credentials.admin_auth_url }} +interface = admin +{% elif identity_credentials.internal_auth_url -%} +auth_url = {{ identity_credentials.internal_auth_url }} +interface = internal +{% elif identity_credentials.internal_host -%} +auth_url = {{ identity_credentials.internal_protocol }}://{{ identity_credentials.internal_host }}:{{ identity_credentials.internal_port }} +interface = internal +{% endif -%} +{% if identity_credentials.public_auth_url -%} +www_authenticate_uri = {{ identity_credentials.public_auth_url }} +{% elif identity_credentials.internal_host -%} +www_authenticate_uri = {{ identity_credentials.internal_protocol }}://{{ identity_credentials.internal_host }}:{{ identity_credentials.internal_port }} +{% endif -%} +auth_type = password +project_domain_name = {{ identity_credentials.project_domain_name }} +user_domain_name = {{ identity_credentials.user_domain_name }} +project_name = {{ identity_credentials.project_name }} +username = {{ identity_credentials.username }} +password = {{ identity_credentials.password }} +service_token_roles = {{ identity_credentials.admin_role }} +service_token_roles_required = True diff --git a/charms/ceilometer-k8s/src/templates/parts/section-database b/charms/ceilometer-k8s/src/templates/parts/section-database new file mode 100644 index 00000000..986d9b10 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/section-database @@ -0,0 +1,3 @@ +[database] +{% include "parts/database-connection" %} +connection_recycle_time = 200 diff --git a/charms/ceilometer-k8s/src/templates/parts/section-federation b/charms/ceilometer-k8s/src/templates/parts/section-federation new file mode 100644 index 00000000..65ee99ed --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/section-federation @@ -0,0 +1,10 @@ +{% if trusted_dashboards %} +[federation] +{% for dashboard_url in trusted_dashboards -%} +trusted_dashboard = {{ dashboard_url }} +{% endfor -%} +{% endif %} +{% for sp in fid_sps -%} +[{{ sp['protocol-name'] }}] +remote_id_attribute = {{ sp['remote-id-attribute'] }} +{% endfor -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-identity b/charms/ceilometer-k8s/src/templates/parts/section-identity new file mode 100644 index 00000000..7568a9a4 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/section-identity @@ -0,0 +1,2 @@ +[keystone_authtoken] +{% include "parts/identity-data" %} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-middleware b/charms/ceilometer-k8s/src/templates/parts/section-middleware new file mode 100644 index 00000000..e65f1d98 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/section-middleware @@ -0,0 +1,6 @@ +{% for section in sections -%} +[{{section}}] +{% for key, value in sections[section].items() -%} +{{ key }} = {{ value }} +{% endfor %} +{%- endfor %} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user b/charms/ceilometer-k8s/src/templates/parts/section-service-user new file mode 100644 index 00000000..165fbe71 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/section-service-user @@ -0,0 +1,15 @@ +{% if identity_service.service_domain_id -%} +[service_user] +{% if identity_service.internal_auth_url -%} +auth_url = {{ identity_service.internal_auth_url }} +{% elif identity_service.internal_host -%} +auth_url = {{ identity_service.internal_protocol }}://{{ identity_service.internal_host }}:{{ identity_service.internal_port }} +{% endif -%} +send_service_user_token = true +auth_type = password +project_domain_id = {{ identity_service.service_domain_id }} +user_domain_id = {{ identity_service.service_domain_id }} +project_name = {{ identity_service.service_project_name }} +username = {{ identity_service.service_user_name }} +password = {{ identity_service.service_password }} +{% endif -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds b/charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds new file mode 100644 index 00000000..bd32c5e3 --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/section-service-user-id-creds @@ -0,0 +1,15 @@ +{% if identity_credentials.project_domain_id -%} +[service_user] +{% if identity_credentials.internal_auth_url -%} +auth_url = {{ identity_credentials.internal_auth_url }} +{% elif identity_credentials.internal_host -%} +auth_url = {{ identity_credentials.internal_protocol }}://{{ identity_credentials.internal_host }}:{{ identity_credentials.internal_port }} +{% endif -%} +send_service_user_token = true +auth_type = password +project_domain_id = {{ identity_credentials.project_domain_id }} +user_domain_id = {{ identity_credentials.user_domain_id }} +project_name = {{ identity_credentials.project_name }} +username = {{ identity_credentials.username }} +password = {{ identity_credentials.password }} +{% endif -%} diff --git a/charms/ceilometer-k8s/src/templates/parts/section-signing b/charms/ceilometer-k8s/src/templates/parts/section-signing new file mode 100644 index 00000000..cb7d69ae --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/parts/section-signing @@ -0,0 +1,15 @@ +{% if enable_signing -%} +[signing] +{% if certfile -%} +certfile = {{ certfile }} +{% endif -%} +{% if keyfile -%} +keyfile = {{ keyfile }} +{% endif -%} +{% if ca_certs -%} +ca_certs = {{ ca_certs }} +{% endif -%} +{% if ca_key -%} +ca_key = {{ ca_key }} +{% endif -%} +{% endif -%} \ No newline at end of file diff --git a/charms/ceilometer-k8s/src/templates/wsgi-ceilometer-api.conf b/charms/ceilometer-k8s/src/templates/wsgi-ceilometer-api.conf new file mode 100644 index 00000000..c9def84b --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/wsgi-ceilometer-api.conf @@ -0,0 +1,28 @@ +Listen {{ wsgi_config.public_port }} + + + WSGIDaemonProcess {{ wsgi_config.group }} processes=3 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ wsgi_config.group }} + {% if ingress_public.ingress_path -%} + WSGIScriptAlias {{ ingress_public.ingress_path }} {{ wsgi_config.wsgi_public_script }} + {% endif -%} + WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog {{ wsgi_config.error_log }} + CustomLog {{ wsgi_config.custom_log }} combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + diff --git a/charms/ceilometer-k8s/src/templates/wsgi-template.conf.j2 b/charms/ceilometer-k8s/src/templates/wsgi-template.conf.j2 new file mode 100644 index 00000000..c9def84b --- /dev/null +++ b/charms/ceilometer-k8s/src/templates/wsgi-template.conf.j2 @@ -0,0 +1,28 @@ +Listen {{ wsgi_config.public_port }} + + + WSGIDaemonProcess {{ wsgi_config.group }} processes=3 threads=1 user={{ wsgi_config.user }} group={{ wsgi_config.group }} \ + display-name=%{GROUP} + WSGIProcessGroup {{ wsgi_config.group }} + {% if ingress_public.ingress_path -%} + WSGIScriptAlias {{ ingress_public.ingress_path }} {{ wsgi_config.wsgi_public_script }} + {% endif -%} + WSGIScriptAlias / {{ wsgi_config.wsgi_public_script }} + WSGIApplicationGroup %{GLOBAL} + WSGIPassAuthorization On + = 2.4> + ErrorLogFormat "%{cu}t %M" + + ErrorLog {{ wsgi_config.error_log }} + CustomLog {{ wsgi_config.custom_log }} combined + + + = 2.4> + Require all granted + + + Order allow,deny + Allow from all + + + diff --git a/charms/ceilometer-k8s/test-requirements.txt b/charms/ceilometer-k8s/test-requirements.txt new file mode 100644 index 00000000..d1a61d34 --- /dev/null +++ b/charms/ceilometer-k8s/test-requirements.txt @@ -0,0 +1,9 @@ +# This file is managed centrally. If you find the need to modify this as a +# one-off, please don't. Intead, consult #openstack-charms and ask about +# requirements management in charms via bot-control. Thank you. + +coverage +mock +flake8 +stestr +ops diff --git a/charms/ceilometer-k8s/tests/config.yaml b/charms/ceilometer-k8s/tests/config.yaml new file mode 120000 index 00000000..e84e89a8 --- /dev/null +++ b/charms/ceilometer-k8s/tests/config.yaml @@ -0,0 +1 @@ +../config.yaml \ No newline at end of file diff --git a/charms/ceilometer-k8s/tests/integration/test_charm.py b/charms/ceilometer-k8s/tests/integration/test_charm.py new file mode 100644 index 00000000..18f24d39 --- /dev/null +++ b/charms/ceilometer-k8s/tests/integration/test_charm.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +# Copyright 2023 liam +# See LICENSE file for licensing details. + +import asyncio +import logging +from pathlib import Path + +import pytest +import yaml +from pytest_operator.plugin import OpsTest + +logger = logging.getLogger(__name__) + +METADATA = yaml.safe_load(Path("./metadata.yaml").read_text()) +APP_NAME = METADATA["name"] + + +@pytest.mark.abort_on_fail +async def test_build_and_deploy(ops_test: OpsTest): + """Build the charm-under-test and deploy it together with related charms. + + Assert on the unit status before any relations/configurations take place. + """ + # Build and deploy charm from local source folder + charm = await ops_test.build_charm(".") + resources = {"httpbin-image": METADATA["resources"]["httpbin-image"]["upstream-source"]} + + # Deploy the charm and wait for active/idle status + await asyncio.gather( + ops_test.model.deploy(await charm, resources=resources, application_name=APP_NAME), + ops_test.model.wait_for_idle( + apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000 + ), + ) diff --git a/charms/ceilometer-k8s/tests/tests.yaml b/charms/ceilometer-k8s/tests/tests.yaml new file mode 100644 index 00000000..34e47f18 --- /dev/null +++ b/charms/ceilometer-k8s/tests/tests.yaml @@ -0,0 +1,18 @@ +gate_bundles: + - smoke +smoke_bundles: + - smoke +configure: + - zaza.openstack.charm_tests.keystone.setup.add_tempest_roles +tests: [] +tests_options: + trust: + - smoke + ignore_hard_deploy_errors: + - smoke + + tempest: + default: + smoke: True + +target_deploy_status: [] diff --git a/charms/ceilometer-k8s/tests/unit/__init__.py b/charms/ceilometer-k8s/tests/unit/__init__.py new file mode 100644 index 00000000..47fac14d --- /dev/null +++ b/charms/ceilometer-k8s/tests/unit/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 + +# Copyright 2023 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Unit tests for ceilometer operator.""" diff --git a/charms/ceilometer-k8s/tests/unit/test_charm.py b/charms/ceilometer-k8s/tests/unit/test_charm.py new file mode 100644 index 00000000..cef35b38 --- /dev/null +++ b/charms/ceilometer-k8s/tests/unit/test_charm.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 + +# Copyright 2021 Canonical Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for gnocchi charm.""" + +import ops_sunbeam.test_utils as test_utils + +import charm + + +class _CeilometerOperatorCharm(charm.CeilometerOperatorCharm): + def __init__(self, framework): + self.seen_events = [] + super().__init__(framework) + + def _log_event(self, event): + self.seen_events.append(type(event).__name__) + + def configure_charm(self, event): + super().configure_charm(event) + self._log_event(event) + + +class TestCeilometerOperatorCharm(test_utils.CharmTestCase): + """Class for testing gnocchi charm.""" + + PATCHES = [] + + def setUp(self): + """Run setup for unit tests.""" + super().setUp(charm, self.PATCHES) + self.harness = test_utils.get_harness( + _CeilometerOperatorCharm, container_calls=self.container_calls + ) + + def test_pebble_ready_handler(self): + """Test Pebble ready event is captured.""" + self.harness.begin() + self.assertEqual(self.harness.charm.seen_events, []) + test_utils.set_all_pebbles_ready(self.harness) + self.assertEqual(len(self.harness.charm.seen_events), 2) + + def test_all_relations(self): + """Test all the charms relations.""" + self.harness.begin_with_initial_hooks() + self.harness.set_leader() + test_utils.set_all_pebbles_ready(self.harness) + test_utils.add_complete_identity_credentials_relation(self.harness) + test_utils.add_complete_amqp_relation(self.harness) + + for c in ["ceilometer-central", "ceilometer-notification"]: + self.check_file(c, "/etc/ceilometer/ceilometer.conf") diff --git a/charms/ceilometer-k8s/tox.ini b/charms/ceilometer-k8s/tox.ini new file mode 100644 index 00000000..0bc536c1 --- /dev/null +++ b/charms/ceilometer-k8s/tox.ini @@ -0,0 +1,161 @@ +# Operator charm (with zaza): tox.ini + +[tox] +skipsdist = True +envlist = pep8,py3 +sitepackages = False +skip_missing_interpreters = False +minversion = 3.18.0 + +[vars] +src_path = {toxinidir}/src/ +tst_path = {toxinidir}/tests/ +lib_path = {toxinidir}/lib/ +pyproject_toml = {toxinidir}/pyproject.toml +all_path = {[vars]src_path} {[vars]tst_path} + +[testenv] +basepython = python3 +setenv = + PYTHONPATH = {toxinidir}:{[vars]lib_path}:{[vars]src_path} +passenv = + HOME + PYTHONPATH +install_command = + pip install {opts} {packages} +commands = stestr run --slowest {posargs} +allowlist_externals = + git + charmcraft + {toxinidir}/fetch-libs.sh + {toxinidir}/rename.sh +deps = + -r{toxinidir}/test-requirements.txt + +[testenv:fmt] +description = Apply coding style standards to code +deps = + black + isort +commands = + isort {[vars]all_path} --skip-glob {[vars]lib_path} --skip {toxinidir}/.tox + black --config {[vars]pyproject_toml} {[vars]all_path} --exclude {[vars]lib_path} + +[testenv:build] +basepython = python3 +deps = +commands = + charmcraft -v pack + {toxinidir}/rename.sh + +[testenv:fetch] +basepython = python3 +deps = +commands = + {toxinidir}/fetch-libs.sh + +[testenv:py3] +basepython = python3 +deps = + {[testenv]deps} + -r{toxinidir}/requirements.txt + +[testenv:py38] +basepython = python3.8 +deps = {[testenv:py3]deps} + +[testenv:py39] +basepython = python3.9 +deps = {[testenv:py3]deps} + +[testenv:py310] +basepython = python3.10 +deps = {[testenv:py3]deps} + +[testenv:cover] +basepython = python3 +deps = {[testenv:py3]deps} +setenv = + {[testenv]setenv} + PYTHON=coverage run +commands = + coverage erase + stestr run --slowest {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml + coverage report + +[testenv:pep8] +description = Alias for lint +deps = {[testenv:lint]deps} +commands = {[testenv:lint]commands} + +[testenv:lint] +description = Check code against coding style standards +deps = + black + flake8<6 # Pin version until https://github.com/savoirfairelinux/flake8-copyright/issues/19 is merged + flake8-docstrings + flake8-copyright + flake8-builtins + pyproject-flake8 + pep8-naming + isort + codespell +commands = + codespell {[vars]all_path} + # pflake8 wrapper supports config from pyproject.toml + pflake8 --exclude {[vars]lib_path} --config {toxinidir}/pyproject.toml {[vars]all_path} + isort --check-only --diff {[vars]all_path} --skip-glob {[vars]lib_path} + black --config {[vars]pyproject_toml} --check --diff {[vars]all_path} --exclude {[vars]lib_path} + +[testenv:func-noop] +basepython = python3 +deps = + git+https://github.com/openstack-charmers/zaza.git@libjuju-3.1#egg=zaza + git+https://github.com/openstack-charmers/zaza-openstack-tests.git#egg=zaza.openstack + git+https://opendev.org/openstack/tempest.git#egg=tempest +commands = + functest-run-suite --help + +[testenv:func] +basepython = python3 +deps = {[testenv:func-noop]deps} +commands = + functest-run-suite --keep-model + +[testenv:func-smoke] +basepython = python3 +deps = {[testenv:func-noop]deps} +setenv = + TEST_MODEL_SETTINGS = automatically-retry-hooks=true + TEST_MAX_RESOLVE_COUNT = 5 +commands = + functest-run-suite --keep-model --smoke + +[testenv:func-dev] +basepython = python3 +deps = {[testenv:func-noop]deps} +commands = + functest-run-suite --keep-model --dev + +[testenv:func-target] +basepython = python3 +deps = {[testenv:func-noop]deps} +commands = + functest-run-suite --keep-model --bundle {posargs} + +[coverage:run] +branch = True +concurrency = multiprocessing +parallel = True +source = + . +omit = + .tox/* + tests/* + src/templates/* + +[flake8] +ignore=E226,W504