Skip to content
Open
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
161 changes: 158 additions & 3 deletions cloud/etc/deploy-microovn/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ data "juju_model" "machine_model" {
uuid = var.machine_model_uuid
}

locals {
microovn_machine_ids_by_architecture = merge(
{
amd64 = var.microovn_machine_ids
arm64 = var.microovn_arm64_machine_ids
},
var.microovn_machine_ids_by_architecture,
)
microovn_machine_ids = lookup(local.microovn_machine_ids_by_architecture, "amd64", [])
microovn_arm64_machine_ids = lookup(local.microovn_machine_ids_by_architecture, "arm64", [])
}

resource "juju_application" "openstack-network-agents" {
name = "openstack-network-agents"
model_uuid = data.juju_model.machine_model.uuid
Expand All @@ -36,6 +48,25 @@ resource "juju_application" "openstack-network-agents" {
endpoint_bindings = var.openstack_network_agents_endpoint_bindings
}

resource "juju_application" "openstack-network-agents-arm64" {
count = length(local.microovn_arm64_machine_ids) > 0 ? 1 : 0
name = "openstack-network-agents-arm64"
model_uuid = data.juju_model.machine_model.uuid

charm {
name = "openstack-network-agents"
channel = var.charm_openstack_network_agents_channel
base = "ubuntu@24.04"
revision = var.charm_openstack_network_agents_arm64_revision
}

config = merge({
use-data-binding = true
}, var.charm_openstack_network_agents_config)

endpoint_bindings = var.openstack_network_agents_endpoint_bindings
}

resource "juju_application" "microcluster-token-distributor" {
name = "microcluster-token-distributor"
model_uuid = data.juju_model.machine_model.uuid
Expand All @@ -55,8 +86,8 @@ resource "juju_application" "microcluster-token-distributor" {
resource "juju_application" "microovn" {
name = "microovn"
model_uuid = data.juju_model.machine_model.uuid
machines = length(var.microovn_machine_ids) == 0 ? null : toset(var.microovn_machine_ids)
units = length(var.microovn_machine_ids) == 0 ? 1 : null
machines = length(local.microovn_machine_ids) == 0 ? null : toset(local.microovn_machine_ids)
units = length(local.microovn_machine_ids) == 0 ? 0 : null

charm {
name = "microovn"
Expand All @@ -70,6 +101,25 @@ resource "juju_application" "microovn" {
endpoint_bindings = var.endpoint_bindings
}

resource "juju_application" "microovn_arm64" {
count = length(local.microovn_arm64_machine_ids) > 0 ? 1 : 0
name = "microovn-arm64"
model_uuid = data.juju_model.machine_model.uuid
machines = length(local.microovn_arm64_machine_ids) == 0 ? null : toset(local.microovn_arm64_machine_ids)
constraints = "arch=arm64"

charm {
name = "microovn"
channel = var.charm_microovn_channel
base = "ubuntu@24.04"
# Arm64 publishes a different revision than amd64 on the same channel.
}

config = var.charm_microovn_config

endpoint_bindings = var.endpoint_bindings
}

resource "juju_application" "sunbeam-ovn-proxy" {
name = "sunbeam-ovn-proxy"
model_uuid = data.juju_model.machine_model.uuid
Expand Down Expand Up @@ -160,8 +210,109 @@ resource "juju_integration" "role-distributor-microovn" {
}
}

resource "juju_integration" "microovn_arm64_microcluster_token_distributor" {
count = length(local.microovn_arm64_machine_ids) > 0 ? 1 : 0
model_uuid = data.juju_model.machine_model.uuid

application {
name = juju_application.microovn_arm64[0].name
endpoint = "cluster"
}

application {
name = juju_application.microcluster-token-distributor.name
endpoint = "microcluster-cluster"
}
}

resource "juju_integration" "microovn_arm64_openstack_network_agents" {
count = length(local.microovn_arm64_machine_ids) > 0 ? 1 : 0
model_uuid = data.juju_model.machine_model.uuid

application {
name = juju_application.microovn_arm64[0].name
endpoint = "juju-info"
}

application {
name = juju_application.openstack-network-agents-arm64[0].name
endpoint = "juju-info"
}
}

resource "juju_integration" "role-distributor-microovn-arm64" {
count = (
var.role_distributor_application_name != null
&& length(local.microovn_arm64_machine_ids) > 0
) ? 1 : 0
model_uuid = data.juju_model.machine_model.uuid

application {
name = var.role_distributor_application_name
endpoint = "role-assignment"
}

application {
name = juju_application.microovn_arm64[0].name
endpoint = "role-assignment"
}
}

resource "juju_integration" "microovn_arm64_certs" {
count = (var.ca-offer-url != null && length(local.microovn_arm64_machine_ids) > 0) ? 1 : 0
model_uuid = data.juju_model.machine_model.uuid

application {
name = juju_application.microovn_arm64[0].name
endpoint = "certificates"
}

application {
offer_url = var.ca-offer-url
}
}

resource "juju_integration" "microovn_arm64_ovsdb_cms" {
count = (var.ovn-relay-offer-url != null && length(local.microovn_arm64_machine_ids) > 0) ? 1 : 0
model_uuid = data.juju_model.machine_model.uuid

application {
name = juju_application.microovn_arm64[0].name
endpoint = "ovsdb-external"
}

application {
offer_url = var.ovn-relay-offer-url
}
}

resource "juju_integration" "microovn_arm64_to_ovn_proxy" {
count = (
length(local.microovn_arm64_machine_ids) > 0
&& length(local.microovn_machine_ids) == 0
&& length(juju_application.sunbeam-ovn-proxy.*.name) > 0
) ? 1 : 0
model_uuid = data.juju_model.machine_model.uuid

application {
name = juju_application.microovn_arm64[0].name
endpoint = "ovsdb"
}

application {
name = juju_application.sunbeam-ovn-proxy[0].name
endpoint = "ovsdb"
}
}

resource "juju_integration" "microovn-to-ovn-proxy" {
count = length(juju_application.sunbeam-ovn-proxy.*.name) > 0 ? 1 : 0
count = (
length(juju_application.sunbeam-ovn-proxy.*.name) > 0
&& (
length(local.microovn_machine_ids) > 0
|| length(local.microovn_arm64_machine_ids) == 0
)
) ? 1 : 0
model_uuid = data.juju_model.machine_model.uuid

application {
Expand All @@ -186,6 +337,10 @@ output "microovn-application-name" {
value = juju_application.microovn.name
}

output "microovn-arm64-application-name" {
value = try(juju_application.microovn_arm64[0].name, null)
}

output "ovsdb-cms-offer" {
value = try(juju_offer.ovsdb-cms[0].url, null)
}
20 changes: 19 additions & 1 deletion cloud/etc/deploy-microovn/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ variable "charm_openstack_network_agents_revision" {
default = null
}

variable "charm_openstack_network_agents_arm64_revision" {
description = "Operator channel revision for arm64 openstack-network-agents deployment"
type = number
default = null
}

variable "charm_openstack_network_agents_config" {
description = "Operator config for openstack-network-agents deployment"
type = map(string)
Expand Down Expand Up @@ -82,11 +88,23 @@ variable "charm_sunbeam_ovn_proxy_config" {
}

variable "microovn_machine_ids" {
description = "List of machine ids to include"
description = "List of amd64 machine ids to include"
type = list(string)
default = []
}

variable "microovn_arm64_machine_ids" {
description = "List of arm64 machine ids to include (e.g. DPU network nodes)"
type = list(string)
default = []
}

variable "microovn_machine_ids_by_architecture" {
description = "MicroOVN machine ids grouped by architecture"
type = map(list(string))
default = {}
}

variable "token_distributor_machine_ids" {
description = "List of machine ids to include"
type = list(string)
Expand Down
1 change: 1 addition & 0 deletions sunbeam-python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ ignore = [
"S606", # same as above
"S605", # process with a shell - possible security issue; this should be investigated by the project for possible exploitation
"S607", #start process with a partial path - this should be a project level decision
"D421", # property docstrings commonly use verb-style wording in this codebase
]
extend-ignore = ["E203"]

Expand Down
67 changes: 57 additions & 10 deletions sunbeam-python/sunbeam/core/ovn.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class OvnConfig(pydantic.BaseModel):


DEFAULT_PROVIDER = OvnProvider.OVN_K8S
DEFAULT_ARCHITECTURE = "amd64"
ARM64_ARCHITECTURE = "arm64"


def load_provider_config(client: Client) -> OvnConfig:
Expand Down Expand Up @@ -106,22 +108,67 @@ def is_microovn_necessary_maas(
else:
return nb_network > 0

def get_machines(self) -> list[str]:
"""Get the list of machine IDs for the OVN provider.

:return: list of machine IDs as strings
"""
def _list_microovn_nodes(self) -> list[dict]:
"""Collect cluster nodes that should run MicroOVN."""
nodes = self.client.cluster.list_nodes_by_role("network")
if self.get_provider() == OvnProvider.MICROOVN:
nodes += self.client.cluster.list_nodes_by_role("compute")
nodes += self.client.cluster.list_nodes_by_role("control")
machine_ids = {
str(node.get("machineid"))
for node in nodes
if node.get("machineid") not in (-1, None)
}
return nodes

def get_token_distributor_machines(
self, provider: OvnProvider | None = None
) -> list[str]:
"""Get machine IDs for MicroOVN helper applications."""
provider = provider or self.get_provider()
roles = [Role.NETWORK]
if provider == OvnProvider.MICROOVN:
roles = [Role.CONTROL, Role.COMPUTE, Role.NETWORK]

for role in roles:
machine_ids: set[str] = set()
for node in self.client.cluster.list_nodes_by_role(role.name.lower()):
machineid = node.get("machineid")
if machineid in (-1, None):
continue
arch = node.get("arch") or DEFAULT_ARCHITECTURE
if arch == DEFAULT_ARCHITECTURE:
machine_ids.add(str(machineid))
if machine_ids:
return sorted(machine_ids)

return []

def get_machines(self, architecture: str | None = None) -> list[str]:
"""Get machine IDs for MicroOVN, optionally filtered by architecture.

:param architecture: if set, only return machines with this arch
:return: list of machine IDs as strings
"""
machine_ids: set[str] = set()
for node in self._list_microovn_nodes():
machineid = node.get("machineid")
if machineid in (-1, None):
continue
arch = node.get("arch") or DEFAULT_ARCHITECTURE
if architecture is None or arch == architecture:
machine_ids.add(str(machineid))
return sorted(machine_ids)

def get_machines_by_architecture(self) -> dict[str, list[str]]:
"""Get MicroOVN machine IDs grouped by architecture."""
machine_ids_by_arch: dict[str, set[str]] = {}
for node in self._list_microovn_nodes():
machineid = node.get("machineid")
if machineid in (-1, None):
continue
arch = node.get("arch") or DEFAULT_ARCHITECTURE
machine_ids_by_arch.setdefault(arch, set()).add(str(machineid))
return {
arch: sorted(machine_ids)
for arch, machine_ids in machine_ids_by_arch.items()
}

def get_control_plane_tfvars(
self, deployment: Deployment, jhelper: JujuHelper
) -> dict:
Expand Down
20 changes: 18 additions & 2 deletions sunbeam-python/sunbeam/provider/maas/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ def _partition_nodes_by_dpu(nodes: list[dict]) -> tuple[list[dict], list[dict]]:
return non_dpu, dpu


def _node_matches_juju_machine(node: dict, machine) -> bool:
"""Return True if a clusterd node corresponds to a Juju machine.

Custom DPU images may report a template hostname (e.g. packer-ubuntu)
while clusterd stores the MAAS hostname. Fall back to system ID and
Juju display name when hostnames differ.
"""
if node["name"] == machine.hostname:
return True
systemid = node.get("systemid") or ""
if systemid and systemid == machine.instance_id:
return True
display_name = getattr(machine, "display_name", None)
return bool(display_name and node["name"] == display_name)


class AddMaasDeployment(BaseStep):
"""Perform various checks and add MAAS-backed deployment."""

Expand Down Expand Up @@ -1582,7 +1598,7 @@ def is_skip(self, context: StepContext) -> Result:
nodes_to_deploy.remove(node)
continue
for id, machine in juju_machines.items():
if node["name"] == machine.hostname:
if _node_matches_juju_machine(node, machine):
if int(id) != node_machine_id and node_machine_id != -1:
return Result(
ResultType.FAILED,
Expand Down Expand Up @@ -1666,7 +1682,7 @@ def _sync_updated_node_machine_ids(self) -> None:
for node in self.nodes_to_update:
LOG.debug("Updating machine %s in model %s", node["name"], self.model)
for machine_id, machine in machines.items():
if machine.hostname == node["name"]:
if _node_matches_juju_machine(node, machine):
self.client.cluster.update_node_info(
node["name"], machineid=int(machine_id)
)
Expand Down
Loading
Loading