Skip to content

Commit

Permalink
Add validation, status, build clouds.yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelallan72 committed Mar 18, 2024
1 parent fc66fb9 commit 7adaf4a
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 41 deletions.
13 changes: 6 additions & 7 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,12 @@ requires:
credentials:
interface: keystone-admin

# config:
# options:
# snap_channel:
# default: "latest/stable"
# type: string
# description: |
# If install_method is set to "snap" this option controls channel name.
config:
options:
ssl_ca:
type: string
description: |
Custom SSL CA for keystone if required.
# links:
Expand Down
115 changes: 81 additions & 34 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,15 @@

import ops
import yaml
from ops import CollectStatusEvent, RelationChangedEvent
from ops.model import ActiveStatus, BlockedStatus

logger = logging.getLogger(__name__)

CLOUDS_YAML = "/etc/openstack/clouds.yaml"
# used to match entry in clouds.yaml with openstack-exporter config
CLOUD_NAME = "openstack"
SUPPORTED_API_VERSION = 3


class OpenstackExporterOperatorCharm(ops.CharmBase):
Expand All @@ -27,53 +32,95 @@ def __init__(self, *args: tuple[Any]) -> None:
"""Initialize the charm."""
super().__init__(*args)
self.framework.observe(self.on.credentials_relation_changed, self._on_credentials_changed)
self.framework.observe(self.on.collect_unit_status, self._on_collect_unit_status)

def _on_credentials_changed(self, event: TODO) -> None:
def _on_credentials_changed(self, event: RelationChangedEvent) -> None:
"""Handle updates to credentials from keystone."""
# keystone charm sets relation data like so:
# relation_data = {
# 'service_hostname': resolve_address(ADMIN),
# 'service_port': config('service-port'),
# 'service_username': config('admin-user'),
# 'service_tenant_name': config('admin-role'),
# 'service_region': config('region'),
# 'service_protocol': 'https' if https() else 'http',
# 'api_version': get_api_version(),
# }
# if relation_data['api_version'] > 2:
# relation_data['service_user_domain_name'] = ADMIN_DOMAIN
# relation_data['service_project_domain_name'] = ADMIN_DOMAIN
# relation_data['service_project_name'] = ADMIN_PROJECT
# relation_data['service_password'] = get_admin_passwd()
# relation_set(relation_id=relation_id, **relation_data)

# TODO: test and verify this is the data we want
logger.info(f"{event.relation.data = }")
data = event.relation.data

# TODO build the data dictionary from the above,
# also figure out how to handle differences between api version 2 and 3
data = event.relation.data[event.unit]

# TODO: this is pseudocode
if all_data_available:
self.update_clouds_yaml(data)
self.restart_services()
else:
try:
self._validate_keystone_credentials(data)
except ValueError:
# stop services, etc. because it's not ready
self.stop_services()

def update_clouds_yaml(creds: dict[str, str]) -> None:
data = {
self.update_clouds_yaml(data)
self.restart_services()

def _validate_keystone_credentials(self, data: dict[str, str]) -> None:
"""Validate credentials from keystone.
Raise an exception with an informative message if not valid.
"""
if data.get("api_version") != SUPPORTED_API_VERSION:
raise ValueError(
f"Keystone is using an unsupported api version (expected: 3, found: {data.get('api_version')})"
)

if not all(
data[x]
for x in [
"service_protocol",
"service_hostname",
"service_port",
"service_username",
"service_password",
"service_project_name",
"service_project_domain_name",
"service_user_domain_name",
]
# I guess service_region is optional
):
raise ValueError("Not all values are available from keystone yet.")

def update_clouds_yaml(self, data: dict[str, str]) -> None:
"""Build a clouds.yaml given the keystone credentials data.
We only handle api version 3,
since v2 was removed a long time ago (Queens release)
https://docs.openstack.org/keystone/latest/contributor/http-api.html
"""
auth_url = "{protocol}://{hostname}:{port}/v3".format(
protocol=data["service_protocol"],
hostname=data["service_hostname"],
port=data["service_port"],
)
contents = {
"clouds": {
"openstack": { # the name here must be passed to --cloud option of openstack-exporter
# TODO: populate from creds
# see https://docs.openstack.org/python-openstackclient/latest/configuration/index.html for more info
# https://docs.openstack.org/python-openstackclient/latest/configuration/index.html
CLOUD_NAME: { # the name here must be passed to --cloud option of openstack-exporter
"region_name": data["service_region"],
"identity_api_version": "3",
"identity_interface": "internal",
"auth": {
"username": data["service_username"],
"password": data["service_password"],
"project_name": data["service_project_name"],
"project_domain_name": data["service_project_domain_name"],
"user_domain_name": data["service_user_domain_name"],
"auth_url": auth_url,
},
"verify": data["service_protocol"] == "https",
"cacert": self.config["ssl_ca"],
}
}
}

os.makedirs(os.path.dirname(CLOUDS_YAML), exist_ok=True)
with open(CLOUDS_YAML, "w") as f:
yaml.dump(data, f)
yaml.dump(contents, f)

def _on_collect_unit_status(self, event: CollectStatusEvent) -> None:
# TODO: add blocked status if keystone is not related

keystone_data: dict[str, str] = {} # TODO: collect from relation data
try:
self._validate_keystone_credentials(keystone_data)
except ValueError as e:
event.add_status(BlockedStatus(str(e)))

event.add_status(ActiveStatus())


if __name__ == "__main__": # pragma: nocover
Expand Down

0 comments on commit 7adaf4a

Please sign in to comment.