Setup admin account, observability relations
This commit is contained in:
parent
b51b266d93
commit
97d0472c80
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
@ -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
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
@ -1 +0,0 @@
|
||||
{{ certificates.ca_cert }}
|
1
charms/openstack-exporter-k8s/tests/config.yaml
Symbolic link
1
charms/openstack-exporter-k8s/tests/config.yaml
Symbolic link
@ -0,0 +1 @@
|
||||
../config.yaml
|
@ -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
|
||||
),
|
||||
)
|
15
charms/openstack-exporter-k8s/tests/unit/__init__.py
Normal file
15
charms/openstack-exporter-k8s/tests/unit/__init__.py
Normal 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."""
|
@ -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)
|
101
charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py
Normal file
101
charms/openstack-exporter-k8s/tests/unit/test_os_exporter.py
Normal 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)
|
Loading…
x
Reference in New Issue
Block a user