Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 154 additions & 36 deletions ops/ops/interface_openstack_integration/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,101 @@
import configparser
import contextlib
import io
from typing import Dict, Optional
from typing import Any, Dict, Optional

from pydantic import BaseModel, Json, SecretStr, validator
from pydantic import BaseModel, Field, Json, SecretStr, validator


class LBClassOptions(BaseModel):
"""Options for LoadBalancerClass section in cloud config."""

floating_network_id: Optional[str] = Field(
None,
alias="floating-network-id",
description="floating-network-id. The same with floating-network-id option above.",
)
floating_subnet_id: Optional[str] = Field(
None,
alias="floating-subnet-id",
description="floating-subnet-id. The same with floating-subnet-id option above.",
)
floating_subnet: Optional[str] = Field(
None,
alias="floating-subnet",
description="floating-subnet. The same with floating-subnet option above.",
)
floating_subnet_tags: Optional[str] = Field(
None,
alias="floating-subnet-tags",
description="floating-subnet-tags. The same with floating-subnet-tags option above.",
)
network_id: Optional[str] = Field(
None, alias="network-id", description="network-id. The same with network-id option above."
)
subnet_id: Optional[str] = Field(
None, alias="subnet-id", description="subnet-id. The same with subnet-id option above."
)
member_subnet_id: Optional[str] = Field(
None,
alias="member-subnet-id",
description="member-subnet-id. The same with member-subnet-id option above.",
)


class Data(BaseModel):
"""Databag for information shared over the relation."""

# Required config
# Global Config
auth_url: Json[str]
endpoint_tls_ca: Json[Optional[str]]
username: Json[str]
password: Json[SecretStr]
project_domain_name: Json[str]
project_name: Json[str]
region: Json[str]
username: Json[str]
user_domain_name: Json[str]

# Optional config
bs_version: Json[Optional[str]]
domain_id: Json[Optional[str]] = None
domain_name: Json[Optional[str]] = None
endpoint_tls_ca: Json[Optional[str]]
floating_network_id: Json[Optional[str]]
has_octavia: Json[Optional[bool]]
ignore_volume_az: Json[Optional[bool]]
internal_lb: Json[Optional[bool]]
lb_enabled: Json[Optional[bool]]
lb_method: Json[Optional[str]]
project_id: Json[Optional[str]] = None
project_name: Json[str]
project_domain_id: Json[Optional[str]] = None
project_domain_name: Json[str]
user_domain_id: Json[Optional[str]]
user_domain_name: Json[str]

# LoadBalancer config
has_octavia: Json[Optional[bool]]
lb_enabled: Json[Optional[bool]]
floating_network_id: Json[Optional[str]]
floating_subnet_id: Json[Optional[str]] = None
floating_subnet: Json[Optional[str]] = None
floating_subnet_tags: Json[Optional[str]] = None
lb_provider: Json[Optional[str]] = None
lb_method: Json[Optional[str]] = None
subnet_id: Json[Optional[str]] = None
member_subnet_id: Json[Optional[str]] = None
network_id: Json[Optional[str]] = None
manage_security_groups: Json[Optional[bool]] = None
create_monitor: Json[Optional[bool]] = None
monitor_delay: Json[Optional[int]] = None
monitor_max_retries: Json[Optional[int]] = None
monitor_max_retries_down: Json[Optional[int]] = None
monitor_timeout: Json[Optional[int]] = None
internal_lb: Json[Optional[bool]] = None
node_selector: Json[Optional[str]] = None
cascade_delete: Json[bool] = Field(default=True)
flavor_id: Json[Optional[str]] = None
availability_zone: Json[Optional[str]] = None
lb_classes: Json[Dict[str, "LBClassOptions"]] = Field(default_factory=dict)
enable_ingress_hostname: Json[Optional[bool]] = None
ingress_hostname_suffix: Json[Optional[str]] = None
default_tls_container_ref: Json[Optional[str]] = None
container_store: Json[Optional[str]] = None
max_shared_lb: Json[Optional[int]] = None
provider_requires_serial_api_calls: Json[Optional[bool]] = None

bs_version: Json[Optional[str]]
trust_device_path: Json[Optional[bool]] = None
ignore_volume_az: Json[Optional[bool]] = None

proxy_config: Json[Optional[Dict[str, str]]] = None
manage_security_groups: Json[Optional[bool]]
subnet_id: Json[Optional[str]]
trust_device_path: Json[Optional[bool]]
user_domain_id: Json[Optional[str]] = None
version: Json[Optional[int]] = None

@validator("endpoint_tls_ca")
Expand All @@ -60,10 +120,13 @@ def must_be_b64_cert(cls, s: Json[str]):
def cloud_config(self) -> str: # noqa: C901
"""Render as an openstack cloud config ini.

https://github.com/kubernetes/cloud-provider-openstack/blob/75b1fbb91a2566a869b8922ad62e1c03ab5e6eac/docs/openstack-cloud-controller-manager/using-openstack-cloud-controller-manager.md#global
https://github.com/kubernetes/cloud-provider-openstack/blob/0973c523d13210ca7499ee30ba2b564808b48d54/docs/openstack-cloud-controller-manager/using-openstack-cloud-controller-manager.md#global

https://github.com/kubernetes/cloud-provider-openstack/blob/0973c523d13210ca7499ee30ba2b564808b48d54/docs/openstack-cloud-controller-manager/using-openstack-cloud-controller-manager.md#load-balancer

"""
_global, _loadbalancer, _blockstorage = {}, {}, {}
_s: Any
if self.auth_url:
_global["auth-url"] = self.auth_url
if self.endpoint_tls_ca:
Expand Down Expand Up @@ -93,26 +156,76 @@ def cloud_config(self) -> str: # noqa: C901

if not self.lb_enabled:
_loadbalancer["enabled"] = "false"
if self.has_octavia in (True, None):
# Newer integrator charm will detect whether underlying OpenStack has
# Octavia enabled so we can set this intelligently. If we're still
# related to an older integrator, though, default to assuming Octavia
# is available.
_loadbalancer["use-octavia"] = "true"
else:
_loadbalancer["use-octavia"] = "false"
_loadbalancer["lb-provider"] = "haproxy"
if _s := self.subnet_id:
_loadbalancer["subnet-id"] = _s
if _s := self.floating_network_id:
_loadbalancer["floating-network-id"] = _s
if _s := self.lb_method:
if _s := self.floating_subnet_id:
_loadbalancer["floating-subnet-id"] = _s
if _s := self.floating_subnet:
_loadbalancer["floating-subnet"] = _s
if _s := self.floating_subnet_tags:
_loadbalancer["floating-subnet-tags"] = _s

octavia_available = self.has_octavia in (True, None)
_loadbalancer["use-octavia"] = "true" if octavia_available else "false"

if _s := self.lb_provider:
_loadbalancer["lb-provider"] = _s
else:
default_provider = "amphora" if octavia_available else "haproxy"
_loadbalancer["lb-provider"] = default_provider

if _loadbalancer["lb-provider"] == "ovn":
_loadbalancer["lb-method"] = "SOURCE_IP_PORT"
elif _s := self.lb_method:
_loadbalancer["lb-method"] = _s
if self.internal_lb:
_loadbalancer["internal-lb"] = "true"
elif _loadbalancer["lb-provider"] in ("amphora", "octavia"):
_loadbalancer["lb-method"] = "ROUND_ROBIN"

if _s := self.subnet_id:
_loadbalancer["subnet-id"] = _s
if _s := self.member_subnet_id:
_loadbalancer["member-subnet-id"] = _s
if _s := self.network_id:
_loadbalancer["network-id"] = _s
if self.manage_security_groups:
_loadbalancer["manage-security-groups"] = "true"

if self.create_monitor:
_loadbalancer["create-monitor"] = "true"
if _s := self.monitor_delay:
_loadbalancer["monitor-delay"] = str(_s)
if _s := self.monitor_max_retries:
_loadbalancer["monitor-max-retries"] = str(_s)
if _s := self.monitor_max_retries_down:
_loadbalancer["monitor-max-retries-down"] = str(_s)
if _s := self.monitor_timeout:
_loadbalancer["monitor-timeout"] = str(_s)

if self.internal_lb:
_loadbalancer["internal-lb"] = "true"

if _s := self.node_selector:
_loadbalancer["node-selector"] = _s
if not self.cascade_delete:
_loadbalancer["cascade-delete"] = "false"
if _s := self.flavor_id:
_loadbalancer["flavor-id"] = _s
if _s := self.availability_zone:
_loadbalancer["availability-zone"] = _s

if self.enable_ingress_hostname:
_loadbalancer["enable-ingress-hostname"] = "true"
if _s := self.ingress_hostname_suffix:
_loadbalancer["ingress-hostname-suffix"] = _s
if octavia_available and (_s := self.default_tls_container_ref):
_loadbalancer["default-tls-container-ref"] = _s
if _s := self.container_store:
_loadbalancer["container-store"] = _s
if _s := self.max_shared_lb:
_loadbalancer["max-shared-lb"] = str(_s)
if self.provider_requires_serial_api_calls:
_loadbalancer["provider-requires-serial-api-calls"] = "true"

if _os := self.bs_version:
_blockstorage["bs-version"] = _os
if self.trust_device_path:
Expand All @@ -123,7 +236,12 @@ def cloud_config(self) -> str: # noqa: C901
config = configparser.ConfigParser()
config["Global"] = _global
config["LoadBalancer"] = _loadbalancer
for lb_class, lb_class_opts in self.lb_classes.items():
if as_dict := {k: v for k, v in lb_class_opts.dict(by_alias=True).items() if v}:
config[f'LoadBalancerClass "{lb_class}"'] = as_dict

config["BlockStorage"] = _blockstorage

with contextlib.closing(io.StringIO()) as sio:
config.write(sio)
output_text = sio.getvalue()
Expand Down
25 changes: 18 additions & 7 deletions ops/ops/interface_openstack_integration/requires.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
from typing import Dict, Optional

import pydantic
from backports.cached_property import cached_property
from ops.charm import CharmBase, RelationBrokenEvent
from ops.framework import Object
Expand Down Expand Up @@ -50,8 +51,11 @@ def _raw_data(self):

@cached_property
def _data(self) -> Optional[Data]:
raw = self._raw_data
return Data(**raw) if raw else None
if raw := self._raw_data:
if pydantic.VERSION.startswith("1."):
return Data.parse_obj(raw)
return Data.model_validate(raw)
return None

def evaluate_relation(self, event) -> Optional[str]:
"""Determine if relation is ready."""
Expand Down Expand Up @@ -87,24 +91,31 @@ def is_ready(self):
]
)

@property
def data(self) -> Optional[Data]:
"""Return parsed data from integrator relation."""
if self.is_ready:
return self._data
return None

@property
def cloud_conf(self) -> Optional[str]:
"""Return cloud.conf from integrator relation."""
if self.is_ready and (data := self._data):
if data := self.data:
return data.cloud_config
return None

@property
def cloud_conf_b64(self) -> Optional[bytes]:
"""Return cloud.conf from integrator relation as base64-encoded bytes."""
if self.is_ready and (data := self.cloud_conf):
if data := self.cloud_conf:
return base64.b64encode(data.encode())
return None

@property
def endpoint_tls_ca(self) -> Optional[bytes]:
"""Return cloud.conf from integrator relation."""
if self.is_ready and (data := self._data):
"""Return endpoint tls ca from integrator relation."""
if data := self.data:
if data.endpoint_tls_ca:
return data.endpoint_tls_ca.encode()
return None
Expand All @@ -113,6 +124,6 @@ def endpoint_tls_ca(self) -> Optional[bytes]:
def proxy_config(self) -> Dict[str, str]:
"""Return proxy_config from integrator relation."""
config = None
if self.is_ready and (data := self._data):
if data := self.data:
config = data.proxy_config
return config or {}
4 changes: 4 additions & 0 deletions ops/tests/data/cloud_conf.ini
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,12 @@ user-domain-name = user

[LoadBalancer]
use-octavia = true
lb-provider = amphora
lb-method = ROUND_ROBIN

[LoadBalancerClass "default"]
member-subnet-id = something

[BlockStorage]
bs-version = v2
ignore-volume-az = true
Expand Down
12 changes: 12 additions & 0 deletions ops/tests/data/openstack_integration_data.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,15 @@ user_domain_name: '"user"'
user_domain_id: '"90123456789012345678901234567890"'
username: '"admin"'
version: '"3"'
lb_classes: > #intentionally a string encoding a JSON structure
{
"default": {
"floating-network-id": "",
"floating-subnet-id": "",
"floating-subnet": "",
"floating-subnet-tags": "",
"network-id": "",
"subnet-id": "",
"member-subnet-id": "something"
}
}