Setup admin account, observability relations

This commit is contained in:
Guillaume Boutry 2023-10-17 09:03:23 +02:00
parent b51b266d93
commit 97d0472c80
18 changed files with 6707 additions and 325 deletions

View File

@ -1,26 +1,61 @@
<!--
Avoid using this README file for information that is maintained or published elsewhere, e.g.:
* metadata.yaml > published on Charmhub
* documentation > published on (or linked to from) Charmhub
* detailed contribution guide > documentation or CONTRIBUTING.md
Use links instead.
-->
# openstack-exporter-k8s
Charmhub package name: operator-template
More information: https://charmhub.io/openstack-exporter-k8s
## Description
Describe your charm in one or two sentences.
openstack-exporter-k8s is an operator to manage an openstack exporter on a Kubernetes based environment.
## Other resources
## Usage
<!-- If your charm is documented somewhere else other than Charmhub, provide a link separately. -->
### Deployment
- [Read more](https://example.com)
openstack-exporter-k8s is deployed using below command:
- [Contributing](CONTRIBUTING.md) <!-- or link to other contribution documentation -->
juju deploy openstack-exporter-k8s openstack-exporter
Now connect the openstack exporter operator to existing keystone identity operators:
juju relate keystone:identity-ops openstack-exporter:identity-ops
### Configuration
This section covers common and/or important configuration options. See file
`config.yaml` for the full list of options, along with their descriptions and
default values. See the [Juju documentation][juju-docs-config-apps] for details
on configuring applications.
### Actions
This section covers Juju [actions][juju-docs-actions] supported by the charm.
Actions allow specific operations to be performed on a per-unit basis. To
display action descriptions run `juju actions openstack-exporter`. If the charm is not
deployed then see file `actions.yaml`.
## Relations
openstack-exporter-k8s requires the following relations:
`identity-ops`: To create admin user
## OCI Images
The charm by default uses following images:
`ghcr.io/canonical/openstack-exporter:1.6.0-7533071`
## Contributing
Please see the [Juju SDK docs](https://juju.is/docs/sdk) for guidelines
on enhancements to this charm following best practice guidelines, and
[CONTRIBUTING.md](contributors-guide) for developer guidance.
## Bugs
Please report bugs on [Launchpad][lp-bugs-charm-openstack-exporter-k8s].
<!-- LINKS -->
[contributors-guide]: https://opendev.org/openstack/charm-openstack-exporter-k8s/src/branch/main/CONTRIBUTING.md
[juju-docs-actions]: https://jaas.ai/docs/actions
[juju-docs-config-apps]: https://juju.is/docs/configuring-applications
[lp-bugs-charm-openstack-exporter-k8s]: https://bugs.launchpad.net/charm-openstack-exporter-k8s/+filebug
- See the [Juju SDK documentation](https://juju.is/docs/sdk) for more information about developing and improving charms.

View File

@ -14,14 +14,8 @@ parts:
apt install -y ca-certificates
update-ca-certificates
dashboards:
plugin: dump
source: https://github.com/openstack-exporter/grafana-dashboards
source-type: git
source-depth: 1
charm:
after: [update-certificates, dashboards]
after: [update-certificates]
build-packages:
- git
- libffi-dev
@ -34,4 +28,4 @@ parts:
- jsonschema
- pydantic<2.0
- jinja2
- git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
- git+https://github.com/gboutry/charm-ops-sunbeam@feat/user-id-ops#egg=ops_sunbeam

View File

@ -1,26 +1,4 @@
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

View File

@ -1,5 +0,0 @@
#!/bin/bash
echo "INFO: Fetching libs from charmhub."
charmcraft fetch-lib charms.keystone_k8s.v0.identity_resource
charmcraft fetch-lib charms.tls_certificates_interface.v1.tls_certificates

File diff suppressed because it is too large Load Diff

View File

@ -117,7 +117,7 @@ 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
LIBPATCH = 4
REQUEST_NOT_SENT = 1
@ -367,10 +367,11 @@ class IdentityResourceProvides(Object):
self, event: RelationChangedEvent
):
"""Handle IdentityResource changed."""
request = event.relation.data[event.relation.app].get("request", {})
self.on.process_op.emit(
event.relation.id, event.relation.name, request
)
request = event.relation.data[event.relation.app].get("request")
if request is not None:
self.on.process_op.emit(
event.relation.id, event.relation.name, request
)
def set_ops_response(
self, relation_id: str, relation_name: str, ops_response: dict

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ name: openstack-exporter-k8s
summary: OpenStack openstack-exporter service
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
description: |
OpenStack openstack-exporter provides endpoint metrics ma boi
OpenStack openstack-exporter provides endpoint metrics for OpenStack services.
version: 3
bases:
- name: ubuntu
@ -23,15 +23,17 @@ resources:
openstack-exporter-image:
type: oci-image
description: OCI image for OpenStack openstack-exporter
upstream-source: ghcr.io/canonical/openstack-exporter:2023.2
upstream-source: ghcr.io/canonical/openstack-exporter:1.6.0-7533071
requires:
# identity-service:
# interface: keystone
identity-ops:
interface: keystone-resources
# certificates:
# interface: tls-certificates
provides:
metrics-endpoint:
interface: prometheus_scrape
grafana-dashboard:
interface: grafana_dashboard
peers:
peers:

View File

@ -7,4 +7,4 @@
build_type: charmcraft
publish_charm: true
charmcraft_channel: 2.0/stable
publish_channel: 2023.1/edge
publish_channel: 2023.2/edge

View File

@ -1,4 +1,5 @@
ops
jinja2
git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
pwgen
git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
# COS requirement
cosl

View File

@ -1,29 +1,45 @@
#!/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.
"""Openstack-exporter Operator Charm.
This charm provide Openstack-exporter services as part of an OpenStack deployment
"""
import hashlib
import json
import logging
from typing import TYPE_CHECKING, List, Optional
from typing import (
TYPE_CHECKING,
List,
Optional,
)
import charms.grafana_k8s.v0.grafana_dashboard as grafana_dashboard
import charms.prometheus_k8s.v0.prometheus_scrape as prometheus_scrape
import ops
import pwgen
from ops.main import main
import charms.keystone_k8s.v0.identity_resource as identity_resource
import ops_sunbeam.container_handlers as sunbeam_chandlers
import ops.charm
import ops_sunbeam.charm as sunbeam_charm
import ops_sunbeam.config_contexts as sunbeam_config_contexts
import ops_sunbeam.container_handlers as sunbeam_chandlers
import ops_sunbeam.core as sunbeam_core
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
from ops.main import (
main,
)
logger = logging.getLogger(__name__)
CREDENTIALS_SECRET_PREFIX = "credentials_"
CONFIGURE_SECRET_PREFIX = "configure-"
CONTAINER = "openstack-exporter"
@ -36,21 +52,32 @@ class OSExporterConfigurationContext(sunbeam_config_contexts.ConfigContext):
@property
def ready(self) -> bool:
"""Whether the context has all the data is needs."""
return self.charm.auth_url is not None
return all(
(
self.charm.auth_url is not None,
self.charm.user_id_ops.ready,
)
)
def context(self) -> dict:
"""OS Exporter configuration context."""
username, password = self.charm.user_credentials
credentials = self.charm.user_id_ops.get_config_credentials()
auth_url = self.charm.auth_url
if credentials is None or auth_url is None:
return {}
username, password = credentials
return {
"domain_name": self.charm.domain,
"project_name": self.charm.project,
"username": username,
"password": password,
"auth_url": self.charm.auth_url,
"auth_url": auth_url,
}
class OSExporterPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"""Pebble handler for the container."""
def get_layer(self) -> dict:
"""Pebble configuration layer for the container."""
return {
@ -63,19 +90,64 @@ class OSExporterPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
"command": (
"openstack-exporter"
" --os-client-config /etc/os-exporter/clouds.yaml"
" --multi-cloud"
# Using legacy mode as params are not
# supported by prometheus_scrape interface
" default"
),
"user": "_daemon_",
"group": "_daemon_",
"startup": "disabled",
},
},
}
class MetricsEndpointRelationHandler(sunbeam_rhandlers.RelationHandler):
"""Relation handler for Metrics Endpoint relation."""
if TYPE_CHECKING:
charm: "OSExporterOperatorCharm"
interface: prometheus_scrape.MetricsEndpointProvider
def setup_event_handler(self) -> ops.Object:
"""Configure event handlers for the relation."""
logger.debug("Setting up Metrics Endpoint event handler")
interface = prometheus_scrape.MetricsEndpointProvider(
self.charm, jobs=self.charm._scrape_jobs
)
return interface
@property
def ready(self) -> bool:
"""Determine with the relation is ready for use."""
return True
class GrafanaDashboardsRelationHandler(sunbeam_rhandlers.RelationHandler):
"""Relation handler for Grafana Dashboards relation."""
interface: grafana_dashboard.GrafanaDashboardProvider
def setup_event_handler(self) -> ops.Object:
"""Configure event handlers for the relation."""
logger.debug("Setting up Grafana Dashboard event handler")
interface = grafana_dashboard.GrafanaDashboardProvider(
self.charm,
)
return interface
@property
def ready(self) -> bool:
"""Determine with the relation is ready for use."""
return True
class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
"""Charm the service."""
mandatory_relations = {
# "certificates",
"identity-ops",
}
service_name = "openstack-exporter"
@ -89,11 +161,6 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
"_daemon_",
"_daemon_",
),
# sunbeam_core.ContainerConfigFile(
# "/etc/ssl/ca.pem",
# "_daemon_",
# "_daemon_",
# ),
]
@property
@ -131,42 +198,92 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
@property
def domain(self):
"""Domain name for openstack-exporter."""
return "default"
return "admin_domain"
@property
def project(self):
"""Project name for openstack-exporter."""
return "services"
return "admin"
@property
def user_credentials(self) -> tuple:
"""Credentials for domain admin user."""
credentials_id = self._get_os_exporter_credentials_secret()
credentials = self.model.get_secret(id=credentials_id)
username = credentials.get_content().get("username")
user_password = credentials.get_content().get("password")
return (username, user_password)
def _scrape_jobs(self) -> list:
return [
# # params not supported by prometheus_scrape interface
# {
# "job_name": "openstack-cloud-metrics",
# "metrics_path": "/probe",
# # "params": {
# # "cloud": ["default"],
# # },
# "scrape_timeout": "30s",
# "static_configs": [
# {
# "targets": [
# f"*:{self.default_public_ingress_port}",
# ]
# }
# ],
# },
{
# this will become the internal exporter metrics when
# probe can be configured with params
"job_name": "openstack-cloud-metrics",
"scrape_timeout": "60s",
"static_configs": [
{
"targets": [
f"*:{self.default_public_ingress_port}",
]
}
],
},
]
@property
def auth_url(self) -> Optional[str]:
"""Auth url for openstack-exporter."""
for op in self.id_ops.interface.response.get("ops"):
if op.get("name") != "list_endpoint":
continue
for endpoint in op.get("value", []):
return endpoint.get("url")
return None
label = CONFIGURE_SECRET_PREFIX + "auth-url"
secret_id = self.leader_get(label)
if not secret_id:
return None
secret = self.model.get_secret(id=secret_id)
return secret.get_content()["auth-url"]
def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]:
"""Relation handlers for the service."""
handlers = super().get_relation_handlers()
self.id_ops = sunbeam_rhandlers.IdentityResourceRequiresHandler(
self,
"identity-ops",
self.handle_keystone_ops,
mandatory="identity-ops" in self.mandatory_relations,
self.user_id_ops = (
sunbeam_rhandlers.UserIdentityResourceRequiresHandler(
self,
"identity-ops",
self.configure_charm,
mandatory="identity-ops" in self.mandatory_relations,
name=self.os_exporter_user,
domain=self.domain,
project=self.project,
project_domain=self.domain,
role="admin",
add_suffix=True,
rotate=ops.SecretRotate.MONTHLY,
extra_ops=self._get_list_endpoint_ops(),
extra_ops_process=self._handle_list_endpoint_response,
)
)
handlers.append(self.id_ops)
handlers.append(self.user_id_ops)
self.metrics_endpoint = MetricsEndpointRelationHandler(
self,
"metrics-endpoint",
self.configure_charm,
mandatory="metrics-endpoint" in self.mandatory_relations,
)
handlers.append(self.metrics_endpoint)
self.grafana_dashboard = GrafanaDashboardsRelationHandler(
self,
"grafana-dashboard",
self.configure_charm,
mandatory="grafana-dashboard" in self.mandatory_relations,
)
handlers.append(self.grafana_dashboard)
return handlers
def get_pebble_handlers(
@ -184,106 +301,52 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
),
]
def hash_ops(self, ops: list) -> str:
"""Return the sha1 of the requested ops."""
return hashlib.sha1(json.dumps(ops).encode()).hexdigest()
def _get_list_endpoint_ops(self) -> list:
"""Generate ops request for list endpoint."""
return [
{
"name": "list_endpoint",
"params": {"name": "keystone", "interface": "admin"},
}
]
def _grant_os_exporter_credentials_secret(
def _retrieve_or_set_config_secret(
self,
relation: ops.Relation,
) -> None:
"""Grant secret access to the related units."""
credentials_id = None
try:
credentials_id = self._get_os_exporter_credentials_secret()
secret = self.model.get_secret(id=credentials_id)
logger.debug(
f"Granting access to secret {credentials_id} for relation "
f"{relation.app.name} {relation.name}/{relation.id}"
)
secret.grant(relation)
except (ops.ModelError, ops.SecretNotFoundError) as e:
logger.debug(
f"Error during granting access to secret {credentials_id} for "
f"relation {relation.app.name} {relation.name}/{relation.id}: "
f"{str(e)}"
)
def _retrieve_or_set_secret(
self,
username: str,
key: str,
value: str,
rotate: ops.SecretRotate = ops.SecretRotate.NEVER,
add_suffix_to_username: bool = False,
) -> str:
"""Retrieve or create a secret."""
label = f"{CREDENTIALS_SECRET_PREFIX}{username}"
credentials_id = self.peers.get_app_data(label)
label = CONFIGURE_SECRET_PREFIX + key
credentials_id = self.leader_get(label)
if credentials_id:
secret = self.model.get_secret(id=credentials_id)
content = secret.get_content()
if content[key] != value:
content[key] = value
secret.set_content(content)
return credentials_id
password = str(pwgen.pwgen(12))
if add_suffix_to_username:
suffix = pwgen.pwgen(6)
username = f"{username}-{suffix}"
credentials_secret = self.model.app.add_secret(
{"username": username, "password": password},
{key: value},
label=label,
rotate=rotate,
)
self.peers.set_app_data(
self.leader_set(
{
label: credentials_secret.id,
}
)
return credentials_secret.id
def _get_os_exporter_credentials_secret(self) -> str:
"""Get domain admin secret."""
label = f"{CREDENTIALS_SECRET_PREFIX}{self.os_exporter_user}"
credentials_id = self.peers.get_app_data(label)
if not credentials_id:
credentials_id = self._retrieve_or_set_secret(
self.os_exporter_user,
)
return credentials_id
def _get_os_exporter_user_ops(self) -> list:
"""Generate ops request for domain setup."""
credentials_id = self._get_os_exporter_credentials_secret()
ops = [
# show domain default
{
"name": "show_domain",
"params": {"name": "default"},
},
# fetch keystone endpoint
{
"name": "list_endpoint",
"params": {"name": "keystone", "interface": "admin"},
},
# Create user openstack exporter
{
"name": "create_user",
"params": {
"name": self.os_exporter_user,
"password": credentials_id,
"domain": "default",
},
},
# check with reader system scoped permissions
]
return ops
def _handle_initial_os_exporter_user_setup_response(
self,
event: ops.RelationEvent,
def _handle_list_endpoint_response(
self, event: ops.EventBase, response: dict
) -> None:
"""Handle domain setup response from identity-ops."""
"""Handle response from identity-ops."""
logger.info("%r", response)
if {
op.get("return-code")
for op in self.id_ops.interface.response.get(
for op in response.get(
"ops",
[],
)
@ -292,40 +355,16 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
"Initial openstack exporter user setup commands completed,"
" running configure charm"
)
self.configure_charm(event)
def handle_keystone_ops(self, event: ops.RelationEvent) -> None:
"""Event handler for identity ops."""
if isinstance(event, identity_resource.IdentityOpsProviderReadyEvent):
self._state.identity_ops_ready = True
if not self.unit.is_leader():
return
# Send op request only by leader unit
ops = self._get_os_exporter_user_ops()
id_ = self.hash_ops(ops)
self._grant_os_exporter_credentials_secret(event.relation)
request = {
"id": id_,
"tag": "initial_openstack_exporter_user_setup",
"ops": ops,
}
logger.debug(f"Sending ops request: {request}")
self.id_ops.interface.request_ops(request)
elif isinstance(
event,
identity_resource.IdentityOpsProviderGoneAwayEvent,
):
self._state.identity_ops_ready = False
elif isinstance(event, identity_resource.IdentityOpsResponseEvent):
if not self.unit.is_leader():
return
response = self.id_ops.interface.response
logger.debug(f"Got response from keystone: {response}")
request_tag = response.get("tag")
if request_tag == "initial_openstack_exporter_user_setup":
self._handle_initial_os_exporter_user_setup_response(event)
for op in response.get("ops", []):
if op.get("name") != "list_endpoint":
continue
for endpoint in op.get("value", []):
url = endpoint.get("url")
logger.info("url %r", url)
if url is not None:
self._retrieve_or_set_config_secret("auth-url", url)
self.configure_charm(event)
break
if __name__ == "__main__":

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
{{ certificates.ca_cert }}

View File

@ -0,0 +1 @@
../config.yaml

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
# Copyright 2023 Guillaume
# 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(charm, resources=resources, application_name=APP_NAME),
ops_test.model.wait_for_idle(
apps=[APP_NAME], status="active", raise_on_blocked=True, timeout=1000
),
)

View File

@ -0,0 +1,15 @@
# 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 OSExporter operator."""

View File

@ -1,68 +0,0 @@
# Copyright 2023 Guillaume
# See LICENSE file for licensing details.
#
# Learn more about testing at: https://juju.is/docs/sdk/testing
import unittest
import ops
import ops.testing
from charm import OpenstackExporterK8SCharm
class TestCharm(unittest.TestCase):
def setUp(self):
self.harness = ops.testing.Harness(OpenstackExporterK8SCharm)
self.addCleanup(self.harness.cleanup)
self.harness.begin()
def test_httpbin_pebble_ready(self):
# Expected plan after Pebble ready with default config
expected_plan = {
"services": {
"httpbin": {
"override": "replace",
"summary": "httpbin",
"command": "gunicorn -b 0.0.0.0:80 httpbin:app -k gevent",
"startup": "enabled",
"environment": {"GUNICORN_CMD_ARGS": "--log-level info"},
}
},
}
# Simulate the container coming up and emission of pebble-ready event
self.harness.container_pebble_ready("httpbin")
# Get the plan now we've run PebbleReady
updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
# Check we've got the plan we expected
self.assertEqual(expected_plan, updated_plan)
# Check the service was started
service = self.harness.model.unit.get_container("httpbin").get_service("httpbin")
self.assertTrue(service.is_running())
# Ensure we set an ActiveStatus with no message
self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())
def test_config_changed_valid_can_connect(self):
# Ensure the simulated Pebble API is reachable
self.harness.set_can_connect("httpbin", True)
# Trigger a config-changed event with an updated value
self.harness.update_config({"log-level": "debug"})
# Get the plan now we've run PebbleReady
updated_plan = self.harness.get_container_pebble_plan("httpbin").to_dict()
updated_env = updated_plan["services"]["httpbin"]["environment"]
# Check the config change was effective
self.assertEqual(updated_env, {"GUNICORN_CMD_ARGS": "--log-level debug"})
self.assertEqual(self.harness.model.unit.status, ops.ActiveStatus())
def test_config_changed_valid_cannot_connect(self):
# Trigger a config-changed event with an updated value
self.harness.update_config({"log-level": "debug"})
# Check the charm is in WaitingStatus
self.assertIsInstance(self.harness.model.unit.status, ops.WaitingStatus)
def test_config_changed_invalid(self):
# Ensure the simulated Pebble API is reachable
self.harness.set_can_connect("httpbin", True)
# Trigger a config-changed event with an updated value
self.harness.update_config({"log-level": "foobar"})
# Check the charm is in BlockedStatus
self.assertIsInstance(self.harness.model.unit.status, ops.BlockedStatus)

View File

@ -0,0 +1,101 @@
#!/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 Openstack Exporter operator."""
import json
import ops_sunbeam.test_utils as test_utils
from mock import (
Mock,
)
from ops.testing import (
Harness,
)
import charm
class _OSExporterTestOperatorCharm(charm.OSExporterOperatorCharm):
"""Test Operator Charm for Openstack Exporter Operator."""
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 TestOSExporterOperatorCharm(test_utils.CharmTestCase):
"""Unit tests for OSExporter Operator."""
PATCHES = []
def setUp(self):
"""Set up environment for unit test."""
super().setUp(charm, self.PATCHES)
self.harness = test_utils.get_harness(
_OSExporterTestOperatorCharm, container_calls=self.container_calls
)
self.addCleanup(self.harness.cleanup)
self.harness.begin()
def add_complete_identity_resource_relation(self, harness: Harness) -> int:
"""Add complete Identity resource relation."""
rel_id = harness.add_relation("identity-ops", "keystone")
harness.add_relation_unit(rel_id, "keystone/0")
harness.charm.user_id_ops.get_config_credentials = Mock(
return_value=("test", "test")
)
harness.update_relation_data(
rel_id,
"keystone",
{
"response": json.dumps(
{
"id": 1,
"tag": "create_user",
"ops": [{"name": "create_user", "return-code": 0}],
}
)
},
)
return rel_id
def test_pebble_ready_handler(self):
"""Test pebble ready handler."""
self.assertEqual(self.harness.charm.seen_events, [])
test_utils.set_all_pebbles_ready(self.harness)
self.assertEqual(len(self.harness.charm.seen_events), 1)
def test_all_relations(self):
"""Test all integrations for operator."""
self.harness.set_leader()
test_utils.set_all_pebbles_ready(self.harness)
# this adds all the default/common relations
self.add_complete_identity_resource_relation(self.harness)
config_files = [
"/etc/os-exporter/clouds.yaml",
]
for f in config_files:
self.check_file("openstack-exporter", f)