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
4 changes: 2 additions & 2 deletions snap-wrappers/completions/generate_completion_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
import sys

from sunbeam.commands import configure as configure_cmds
from sunbeam.commands import dashboard_url as dashboard_url_cmds
from sunbeam.commands import dashboard as dashboard_cmds
from sunbeam.commands import generate_cloud_config as generate_cloud_config_cmds
from sunbeam.commands import juju_utils as juju_cmds
from sunbeam.commands import launch as launch_cmds
Expand Down Expand Up @@ -95,7 +95,7 @@ def register_commands():
cli.add_command(generate_cloud_config_cmds.cloud_config)
cli.add_command(launch_cmds.launch)
cli.add_command(openrc_cmds.openrc)
cli.add_command(dashboard_url_cmds.dashboard_url)
cli.add_command(dashboard_cmds.dashboard)
cli.add_command(identity_group)
identity_group.add_command(provider_group)
identity_group.add_command(sso_cmd.set_saml_x509)
Expand Down
134 changes: 134 additions & 0 deletions sunbeam-python/sunbeam/commands/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# SPDX-FileCopyrightText: 2026 - Canonical Ltd
# SPDX-License-Identifier: Apache-2.0

import logging

import click
from rich.console import Console

from sunbeam.core import juju
from sunbeam.core.checks import (
JujuLoginCheck,
VerifyBootstrappedCheck,
run_preflight_checks,
)
from sunbeam.core.common import (
PromptMode,
run_plan,
)
from sunbeam.core.deployment import Deployment
from sunbeam.core.juju import JujuHelper
from sunbeam.core.openstack import OPENSTACK_MODEL
from sunbeam.core.questions import write_answers
from sunbeam.core.terraform import TerraformInitStep
from sunbeam.steps.horizon import THEME_CONFIG_SECTION, AttachHorizonThemeStep
from sunbeam.utils import click_option_show_hints

LOG = logging.getLogger(__name__)
console = Console()


def retrieve_dashboard_url(jhelper: juju.JujuHelper) -> str:
"""Retrieve dashboard URL from Horizon service."""
model = OPENSTACK_MODEL
app = "horizon"
action_cmd = "get-dashboard-url"
try:
unit = jhelper.get_leader_unit(app, model)
except juju.LeaderNotFoundException:
raise ValueError(f"Unable to get {app} leader")
try:
action_result = jhelper.run_action(unit, model, action_cmd)
except juju.ActionFailedException:
_message = "Unable to retrieve URL from Horizon service"
raise ValueError(_message)
return action_result["url"]


@click.group()
@click.pass_context
def dashboard(ctx: click.Context) -> None:
"""Manage OpenStack Dashboard."""


@dashboard.command("url")
@click.pass_context
def dashboard_url(ctx: click.Context) -> None:
"""Retrieve OpenStack Dashboard URL."""
deployment: Deployment = ctx.obj
preflight_checks = [
VerifyBootstrappedCheck(deployment.get_client()),
JujuLoginCheck(deployment.juju_account),
]
run_preflight_checks(preflight_checks, console)

jhelper = juju.JujuHelper(deployment.juju_controller)

with console.status("Retrieving dashboard URL from Horizon service ... "):
try:
console.print(retrieve_dashboard_url(jhelper))
except Exception as e:
raise click.ClickException(str(e))


@click.group()
@click.pass_context
def theme(ctx: click.Context) -> None:
"""Manage Horizon themes."""


dashboard.add_command(theme)


@theme.command("set")
@click_option_show_hints
@click.pass_context
def set_theme(ctx: click.Context, show_hints: bool) -> None:
"""Set a custom Horizon theme interactively."""
deployment: Deployment = ctx.obj
client = deployment.get_client()
jhelper = JujuHelper(deployment.juju_controller)
manifest = deployment.get_manifest()
tfhelper = deployment.get_tfhelper("openstack-plan")

plan = [
TerraformInitStep(tfhelper),
AttachHorizonThemeStep(
client=client,
jhelper=jhelper,
tfhelper=tfhelper,
manifest=manifest,
model=OPENSTACK_MODEL,
prompt_mode=PromptMode.FORCE,
),
]
run_plan(plan, console, show_hints)
console.print("Custom theme applied.")


@theme.command("clear")
@click_option_show_hints
@click.pass_context
def clear_theme(ctx: click.Context, show_hints: bool) -> None:
"""Clear custom Horizon theme and restore defaults."""
deployment: Deployment = ctx.obj
client = deployment.get_client()
jhelper = JujuHelper(deployment.juju_controller)
manifest = deployment.get_manifest()
tfhelper = deployment.get_tfhelper("openstack-plan")

write_answers(client, THEME_CONFIG_SECTION, {"enable_custom_theme": False})

plan = [
TerraformInitStep(tfhelper),
AttachHorizonThemeStep(
client=client,
jhelper=jhelper,
tfhelper=tfhelper,
manifest=manifest,
model=OPENSTACK_MODEL,
prompt_mode=PromptMode.NEVER,
),
]
run_plan(plan, console, show_hints)
console.print("Custom theme cleared.")
56 changes: 0 additions & 56 deletions sunbeam-python/sunbeam/commands/dashboard_url.py

This file was deleted.

8 changes: 8 additions & 0 deletions sunbeam-python/sunbeam/core/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,14 @@ def __init__(self, result_type: ResultType = ResultType.COMPLETED, **kwargs):
self.__setattr__(key, value)


class PromptMode(enum.Enum):
"""Controls whether a step prompts the user interactively."""

AUTO = "auto"
FORCE = "force"
NEVER = "never"


@dataclass
class StepContext:
"""Cross-cutting concerns passed to every step during plan execution."""
Expand Down
29 changes: 29 additions & 0 deletions sunbeam-python/sunbeam/core/juju.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,35 @@ def destroy_model(
except ModelNotFoundException:
LOG.debug("Model %s not found", model)

def attach_resource(
self,
model: str,
application: str,
resource: str,
filepath: str,
):
"""Attach a file resource to a juju application.

:model: Name of the model
:application: Name of the application
:resource: Name of the resource
:filepath: Local filepath to the file to be attached
:returns: Revision number of the attached resource
"""
try:
with self._model(model):
self.cli(
"attach-resource",
application,
f"{resource}={filepath}",
json_format=False,
include_controller=False,
)
except jubilant.CLIError as e:
raise JujuException(
f"Failed to attach resource {resource} to {application}: {str(e)}"
)

def integrate(
self,
model: str,
Expand Down
12 changes: 12 additions & 0 deletions sunbeam-python/sunbeam/core/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,17 @@ class _PCI(pydantic.BaseModel):
# Excluded PCI addresses per node.
excluded_devices: dict[str, list[str]] | None = None

class _HorizonConfig(pydantic.BaseModel):
class _Resources(pydantic.BaseModel):
custom_theme: Path | None = None

enable_custom_theme: bool | None = None
custom_theme_name: str | None = None
theme_path: str | None = None
disable_default_themes: bool | None = None
disable_ubuntu_theme: bool | None = None
resources: _Resources | None = None

class _Endpoints(pydantic.BaseModel):
class _Endpoint(pydantic.BaseModel):
hostname: str | None = None
Expand Down Expand Up @@ -304,6 +315,7 @@ class _DPDK(pydantic.BaseModel):
)
microceph_config: pydantic.RootModel[dict[str, _HostMicroCephConfig]] | None = None
pci: _PCI | None = None
horizon: _HorizonConfig | None = None
dpdk: _DPDK | None = None


Expand Down
4 changes: 2 additions & 2 deletions sunbeam-python/sunbeam/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from sunbeam import log
from sunbeam.commands import configure as configure_cmds
from sunbeam.commands import dashboard_url as dasboard_url_cmds
from sunbeam.commands import dashboard as dashboard_cmds
from sunbeam.commands import generate_cloud_config as generate_cloud_config_cmds
from sunbeam.commands import juju_utils as juju_cmds
from sunbeam.commands import launch as launch_cmds
Expand Down Expand Up @@ -122,7 +122,7 @@ def main():
cli.add_command(generate_cloud_config_cmds.cloud_config)
cli.add_command(launch_cmds.launch)
cli.add_command(openrc_cmds.openrc)
cli.add_command(dasboard_url_cmds.dashboard_url)
cli.add_command(dashboard_cmds.dashboard)

# Add identity group
cli.add_command(identity_group)
Expand Down
12 changes: 11 additions & 1 deletion sunbeam-python/sunbeam/provider/local/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
UserOpenRCStep,
retrieve_admin_credentials,
)
from sunbeam.commands.dashboard_url import retrieve_dashboard_url
from sunbeam.commands.dashboard import retrieve_dashboard_url
from sunbeam.commands.proxy import PromptForProxyStep
from sunbeam.core import ovn
from sunbeam.core.checks import (
Expand Down Expand Up @@ -117,6 +117,7 @@
PromptCheckNodeExistStep,
SaveManagementCidrStep,
)
from sunbeam.steps.horizon import AttachHorizonThemeStep
from sunbeam.steps.hypervisor import (
DeployHypervisorApplicationStep,
ReapplyHypervisorOptionalIntegrationsStep,
Expand Down Expand Up @@ -906,6 +907,15 @@ def bootstrap( # noqa: C901
is_region_controller=is_region_controller,
)
)
plan1.append(
AttachHorizonThemeStep(
client=client,
jhelper=jhelper,
tfhelper=openstack_tfhelper,
manifest=manifest,
model=OPENSTACK_MODEL,
)
)
plan1.append(
SetKeystoneSAMLCertAndKeyStep(
deployment=deployment,
Expand Down
12 changes: 11 additions & 1 deletion sunbeam-python/sunbeam/provider/maas/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
UserOpenRCStep,
retrieve_admin_credentials,
)
from sunbeam.commands.dashboard_url import retrieve_dashboard_url
from sunbeam.commands.dashboard import retrieve_dashboard_url
from sunbeam.commands.proxy import PromptForProxyStep
from sunbeam.core import ovn
from sunbeam.core.checks import (
Expand Down Expand Up @@ -134,6 +134,7 @@
DeploySunbeamClusterdApplicationStep,
)
from sunbeam.steps.features import DisableEnabledFeatures
from sunbeam.steps.horizon import AttachHorizonThemeStep
from sunbeam.steps.hypervisor import (
DeployHypervisorApplicationStep,
DestroyHypervisorApplicationStep,
Expand Down Expand Up @@ -854,6 +855,15 @@ def deploy(
is_region_controller=bool(nb_region_controllers),
)
)
plan2.append(
AttachHorizonThemeStep(
client=client,
jhelper=jhelper,
tfhelper=tfhelper_openstack_deploy,
manifest=manifest,
model=OPENSTACK_MODEL,
)
)
if microovn_necessary:
plan2.append(
ReapplyMicroOVNOptionalIntegrationsStep(
Expand Down
Loading
Loading