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
|
# openstack-exporter-k8s
|
||||||
|
|
||||||
Charmhub package name: operator-template
|
## Description
|
||||||
More information: https://charmhub.io/openstack-exporter-k8s
|
|
||||||
|
|
||||||
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
|
apt install -y ca-certificates
|
||||||
update-ca-certificates
|
update-ca-certificates
|
||||||
|
|
||||||
dashboards:
|
|
||||||
plugin: dump
|
|
||||||
source: https://github.com/openstack-exporter/grafana-dashboards
|
|
||||||
source-type: git
|
|
||||||
source-depth: 1
|
|
||||||
|
|
||||||
charm:
|
charm:
|
||||||
after: [update-certificates, dashboards]
|
after: [update-certificates]
|
||||||
build-packages:
|
build-packages:
|
||||||
- git
|
- git
|
||||||
- libffi-dev
|
- libffi-dev
|
||||||
@ -34,4 +28,4 @@ parts:
|
|||||||
- jsonschema
|
- jsonschema
|
||||||
- pydantic<2.0
|
- pydantic<2.0
|
||||||
- jinja2
|
- 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:
|
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:
|
region:
|
||||||
default: RegionOne
|
default: RegionOne
|
||||||
description: Space delimited list of OpenStack regions
|
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
|
# Increment this PATCH version before using `charmcraft publish-lib` or reset
|
||||||
# to 0 if you are raising the major API version
|
# to 0 if you are raising the major API version
|
||||||
LIBPATCH = 3
|
LIBPATCH = 4
|
||||||
|
|
||||||
|
|
||||||
REQUEST_NOT_SENT = 1
|
REQUEST_NOT_SENT = 1
|
||||||
@ -367,10 +367,11 @@ class IdentityResourceProvides(Object):
|
|||||||
self, event: RelationChangedEvent
|
self, event: RelationChangedEvent
|
||||||
):
|
):
|
||||||
"""Handle IdentityResource changed."""
|
"""Handle IdentityResource changed."""
|
||||||
request = event.relation.data[event.relation.app].get("request", {})
|
request = event.relation.data[event.relation.app].get("request")
|
||||||
self.on.process_op.emit(
|
if request is not None:
|
||||||
event.relation.id, event.relation.name, request
|
self.on.process_op.emit(
|
||||||
)
|
event.relation.id, event.relation.name, request
|
||||||
|
)
|
||||||
|
|
||||||
def set_ops_response(
|
def set_ops_response(
|
||||||
self, relation_id: str, relation_name: str, ops_response: dict
|
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
|
summary: OpenStack openstack-exporter service
|
||||||
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
|
maintainer: OpenStack Charmers <openstack-charmers@lists.ubuntu.com>
|
||||||
description: |
|
description: |
|
||||||
OpenStack openstack-exporter provides endpoint metrics ma boi
|
OpenStack openstack-exporter provides endpoint metrics for OpenStack services.
|
||||||
version: 3
|
version: 3
|
||||||
bases:
|
bases:
|
||||||
- name: ubuntu
|
- name: ubuntu
|
||||||
@ -23,15 +23,17 @@ resources:
|
|||||||
openstack-exporter-image:
|
openstack-exporter-image:
|
||||||
type: oci-image
|
type: oci-image
|
||||||
description: OCI image for OpenStack openstack-exporter
|
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:
|
requires:
|
||||||
# identity-service:
|
|
||||||
# interface: keystone
|
|
||||||
identity-ops:
|
identity-ops:
|
||||||
interface: keystone-resources
|
interface: keystone-resources
|
||||||
# certificates:
|
|
||||||
# interface: tls-certificates
|
provides:
|
||||||
|
metrics-endpoint:
|
||||||
|
interface: prometheus_scrape
|
||||||
|
grafana-dashboard:
|
||||||
|
interface: grafana_dashboard
|
||||||
|
|
||||||
peers:
|
peers:
|
||||||
peers:
|
peers:
|
||||||
|
@ -7,4 +7,4 @@
|
|||||||
build_type: charmcraft
|
build_type: charmcraft
|
||||||
publish_charm: true
|
publish_charm: true
|
||||||
charmcraft_channel: 2.0/stable
|
charmcraft_channel: 2.0/stable
|
||||||
publish_channel: 2023.1/edge
|
publish_channel: 2023.2/edge
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
ops
|
ops
|
||||||
jinja2
|
jinja2
|
||||||
git+https://github.com/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
git+https://opendev.org/openstack/charm-ops-sunbeam#egg=ops_sunbeam
|
||||||
pwgen
|
# COS requirement
|
||||||
|
cosl
|
||||||
|
@ -1,29 +1,45 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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.
|
"""Openstack-exporter Operator Charm.
|
||||||
|
|
||||||
This charm provide Openstack-exporter services as part of an OpenStack deployment
|
This charm provide Openstack-exporter services as part of an OpenStack deployment
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import hashlib
|
|
||||||
import json
|
|
||||||
import logging
|
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 ops
|
||||||
import pwgen
|
import ops.charm
|
||||||
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_sunbeam.charm as sunbeam_charm
|
import ops_sunbeam.charm as sunbeam_charm
|
||||||
import ops_sunbeam.config_contexts as sunbeam_config_contexts
|
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.core as sunbeam_core
|
||||||
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
import ops_sunbeam.relation_handlers as sunbeam_rhandlers
|
||||||
|
from ops.main import (
|
||||||
|
main,
|
||||||
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
CREDENTIALS_SECRET_PREFIX = "credentials_"
|
CONFIGURE_SECRET_PREFIX = "configure-"
|
||||||
CONTAINER = "openstack-exporter"
|
CONTAINER = "openstack-exporter"
|
||||||
|
|
||||||
|
|
||||||
@ -36,21 +52,32 @@ class OSExporterConfigurationContext(sunbeam_config_contexts.ConfigContext):
|
|||||||
@property
|
@property
|
||||||
def ready(self) -> bool:
|
def ready(self) -> bool:
|
||||||
"""Whether the context has all the data is needs."""
|
"""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:
|
def context(self) -> dict:
|
||||||
"""OS Exporter configuration context."""
|
"""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 {
|
return {
|
||||||
"domain_name": self.charm.domain,
|
"domain_name": self.charm.domain,
|
||||||
"project_name": self.charm.project,
|
"project_name": self.charm.project,
|
||||||
"username": username,
|
"username": username,
|
||||||
"password": password,
|
"password": password,
|
||||||
"auth_url": self.charm.auth_url,
|
"auth_url": auth_url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OSExporterPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
class OSExporterPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
||||||
|
"""Pebble handler for the container."""
|
||||||
|
|
||||||
def get_layer(self) -> dict:
|
def get_layer(self) -> dict:
|
||||||
"""Pebble configuration layer for the container."""
|
"""Pebble configuration layer for the container."""
|
||||||
return {
|
return {
|
||||||
@ -63,19 +90,64 @@ class OSExporterPebbleHandler(sunbeam_chandlers.ServicePebbleHandler):
|
|||||||
"command": (
|
"command": (
|
||||||
"openstack-exporter"
|
"openstack-exporter"
|
||||||
" --os-client-config /etc/os-exporter/clouds.yaml"
|
" --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",
|
"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):
|
class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
|
||||||
"""Charm the service."""
|
"""Charm the service."""
|
||||||
|
|
||||||
mandatory_relations = {
|
mandatory_relations = {
|
||||||
# "certificates",
|
|
||||||
"identity-ops",
|
"identity-ops",
|
||||||
}
|
}
|
||||||
service_name = "openstack-exporter"
|
service_name = "openstack-exporter"
|
||||||
@ -89,11 +161,6 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
|
|||||||
"_daemon_",
|
"_daemon_",
|
||||||
"_daemon_",
|
"_daemon_",
|
||||||
),
|
),
|
||||||
# sunbeam_core.ContainerConfigFile(
|
|
||||||
# "/etc/ssl/ca.pem",
|
|
||||||
# "_daemon_",
|
|
||||||
# "_daemon_",
|
|
||||||
# ),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -131,42 +198,92 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
|
|||||||
@property
|
@property
|
||||||
def domain(self):
|
def domain(self):
|
||||||
"""Domain name for openstack-exporter."""
|
"""Domain name for openstack-exporter."""
|
||||||
return "default"
|
return "admin_domain"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def project(self):
|
def project(self):
|
||||||
"""Project name for openstack-exporter."""
|
"""Project name for openstack-exporter."""
|
||||||
return "services"
|
return "admin"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def user_credentials(self) -> tuple:
|
def _scrape_jobs(self) -> list:
|
||||||
"""Credentials for domain admin user."""
|
return [
|
||||||
credentials_id = self._get_os_exporter_credentials_secret()
|
# # params not supported by prometheus_scrape interface
|
||||||
credentials = self.model.get_secret(id=credentials_id)
|
# {
|
||||||
username = credentials.get_content().get("username")
|
# "job_name": "openstack-cloud-metrics",
|
||||||
user_password = credentials.get_content().get("password")
|
# "metrics_path": "/probe",
|
||||||
return (username, user_password)
|
# # "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
|
@property
|
||||||
def auth_url(self) -> Optional[str]:
|
def auth_url(self) -> Optional[str]:
|
||||||
"""Auth url for openstack-exporter."""
|
"""Auth url for openstack-exporter."""
|
||||||
for op in self.id_ops.interface.response.get("ops"):
|
label = CONFIGURE_SECRET_PREFIX + "auth-url"
|
||||||
if op.get("name") != "list_endpoint":
|
secret_id = self.leader_get(label)
|
||||||
continue
|
if not secret_id:
|
||||||
for endpoint in op.get("value", []):
|
return None
|
||||||
return endpoint.get("url")
|
secret = self.model.get_secret(id=secret_id)
|
||||||
return None
|
return secret.get_content()["auth-url"]
|
||||||
|
|
||||||
def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]:
|
def get_relation_handlers(self) -> List[sunbeam_rhandlers.RelationHandler]:
|
||||||
"""Relation handlers for the service."""
|
"""Relation handlers for the service."""
|
||||||
handlers = super().get_relation_handlers()
|
handlers = super().get_relation_handlers()
|
||||||
self.id_ops = sunbeam_rhandlers.IdentityResourceRequiresHandler(
|
self.user_id_ops = (
|
||||||
self,
|
sunbeam_rhandlers.UserIdentityResourceRequiresHandler(
|
||||||
"identity-ops",
|
self,
|
||||||
self.handle_keystone_ops,
|
"identity-ops",
|
||||||
mandatory="identity-ops" in self.mandatory_relations,
|
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
|
return handlers
|
||||||
|
|
||||||
def get_pebble_handlers(
|
def get_pebble_handlers(
|
||||||
@ -184,106 +301,52 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
def hash_ops(self, ops: list) -> str:
|
def _get_list_endpoint_ops(self) -> list:
|
||||||
"""Return the sha1 of the requested ops."""
|
"""Generate ops request for list endpoint."""
|
||||||
return hashlib.sha1(json.dumps(ops).encode()).hexdigest()
|
return [
|
||||||
|
{
|
||||||
|
"name": "list_endpoint",
|
||||||
|
"params": {"name": "keystone", "interface": "admin"},
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
def _grant_os_exporter_credentials_secret(
|
def _retrieve_or_set_config_secret(
|
||||||
self,
|
self,
|
||||||
relation: ops.Relation,
|
key: str,
|
||||||
) -> None:
|
value: str,
|
||||||
"""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,
|
|
||||||
rotate: ops.SecretRotate = ops.SecretRotate.NEVER,
|
rotate: ops.SecretRotate = ops.SecretRotate.NEVER,
|
||||||
add_suffix_to_username: bool = False,
|
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Retrieve or create a secret."""
|
"""Retrieve or create a secret."""
|
||||||
label = f"{CREDENTIALS_SECRET_PREFIX}{username}"
|
label = CONFIGURE_SECRET_PREFIX + key
|
||||||
credentials_id = self.peers.get_app_data(label)
|
credentials_id = self.leader_get(label)
|
||||||
if credentials_id:
|
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
|
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(
|
credentials_secret = self.model.app.add_secret(
|
||||||
{"username": username, "password": password},
|
{key: value},
|
||||||
label=label,
|
label=label,
|
||||||
rotate=rotate,
|
rotate=rotate,
|
||||||
)
|
)
|
||||||
self.peers.set_app_data(
|
self.leader_set(
|
||||||
{
|
{
|
||||||
label: credentials_secret.id,
|
label: credentials_secret.id,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return credentials_secret.id
|
return credentials_secret.id
|
||||||
|
|
||||||
def _get_os_exporter_credentials_secret(self) -> str:
|
def _handle_list_endpoint_response(
|
||||||
"""Get domain admin secret."""
|
self, event: ops.EventBase, response: dict
|
||||||
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,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Handle domain setup response from identity-ops."""
|
"""Handle response from identity-ops."""
|
||||||
|
logger.info("%r", response)
|
||||||
if {
|
if {
|
||||||
op.get("return-code")
|
op.get("return-code")
|
||||||
for op in self.id_ops.interface.response.get(
|
for op in response.get(
|
||||||
"ops",
|
"ops",
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
@ -292,40 +355,16 @@ class OSExporterOperatorCharm(sunbeam_charm.OSBaseOperatorCharmK8S):
|
|||||||
"Initial openstack exporter user setup commands completed,"
|
"Initial openstack exporter user setup commands completed,"
|
||||||
" running configure charm"
|
" running configure charm"
|
||||||
)
|
)
|
||||||
self.configure_charm(event)
|
for op in response.get("ops", []):
|
||||||
|
if op.get("name") != "list_endpoint":
|
||||||
def handle_keystone_ops(self, event: ops.RelationEvent) -> None:
|
continue
|
||||||
"""Event handler for identity ops."""
|
for endpoint in op.get("value", []):
|
||||||
if isinstance(event, identity_resource.IdentityOpsProviderReadyEvent):
|
url = endpoint.get("url")
|
||||||
self._state.identity_ops_ready = True
|
logger.info("url %r", url)
|
||||||
|
if url is not None:
|
||||||
if not self.unit.is_leader():
|
self._retrieve_or_set_config_secret("auth-url", url)
|
||||||
return
|
self.configure_charm(event)
|
||||||
|
break
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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