Skip to content

Commit

Permalink
Sanity checks for machines id, hostnames and availability zones
Browse files Browse the repository at this point in the history
- verify if the parameters passed exist and if they are data-plane
  related
  • Loading branch information
gabrielcocenza committed Jan 25, 2024
1 parent 8aedbe6 commit 2ae2550
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 2 deletions.
4 changes: 4 additions & 0 deletions cou/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ class RunUpgradeError(COUException):
"""Exception raised when an upgrade fails."""


class DataPlaneMachineFilterError(COUException):
"""Exception raised when filtering data-plane machines fails."""


class ActionFailed(COUException):
"""Exception raised when action fails."""

Expand Down
36 changes: 36 additions & 0 deletions cou/steps/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from cou.apps.base import OpenStackApplication
from cou.apps.factory import AppFactory
from cou.apps.machine import Machine
from cou.utils import juju_utils
from cou.utils.openstack import DATA_PLANE_CHARMS, UPGRADE_ORDER, OpenStackRelease

Expand Down Expand Up @@ -203,3 +204,38 @@ def _get_minimum_cloud_series(self) -> Optional[str]:
(app.series for app in self.apps_control_plane + self.apps_data_plane),
default=None,
)

@property
def data_plane_machines(self) -> dict[str, Machine]:
"""Data-plane machines of the model.
:return: Data-plane machines of the model.
:rtype: dict[str, Machine]
"""
return {
machine_id: app.machines[machine_id]
for app in self.apps_data_plane
for machine_id in app.machines
}

@property
def control_plane_machines(self) -> dict[str, Machine]:
"""Control-plane machines of the model.
:return: Control-plane machines of the model.
:rtype: dict[str, Machine]
"""
return {
machine_id: app.machines[machine_id]
for app in self.apps_control_plane
for machine_id in app.machines
}

@property
def machines(self) -> dict[str, Machine]:
"""All machines of the model.
:return: All machines of the model.
:rtype: dict[str, Machine]
"""
return {**self.data_plane_machines, **self.control_plane_machines}
136 changes: 135 additions & 1 deletion cou/steps/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"""Upgrade planning utilities."""

import logging
from typing import Callable
from typing import Callable, Optional, cast

# NOTE we need to import the modules to register the charms with the register_application
# decorator
Expand All @@ -40,6 +40,7 @@
from cou.commands import CLIargs
from cou.exceptions import (
DataPlaneCannotUpgrade,
DataPlaneMachineFilterError,
HaltUpgradePlanGeneration,
HighestReleaseAchieved,
NoTargetError,
Expand Down Expand Up @@ -67,6 +68,7 @@ def pre_plan_sanity_checks(args: CLIargs, analysis_result: Analysis) -> None:

if args.is_data_plane_command:
verify_data_plane_ready_to_upgrade(analysis_result)
verify_data_plane_cli_input(args, analysis_result)


def verify_supported_series(analysis_result: Analysis) -> None:
Expand Down Expand Up @@ -172,6 +174,138 @@ def determine_upgrade_target(analysis_result: Analysis) -> OpenStackRelease:
return target


def verify_data_plane_cli_input(args: CLIargs, analysis_result: Analysis) -> None:
"""Sane checks from the parameters passed in the cli to upgrade data-plane.
:param args: CLI arguments
:type args: CLIargs
:param analysis_result: Analysis result
:type analysis_result: Analysis
"""
if cli_machines := args.machines:
verify_data_plane_cli_machines(cli_machines, analysis_result)

elif cli_hostnames := args.hostnames:
verify_data_plane_cli_hostnames(cli_hostnames, analysis_result)

elif cli_azs := args.availability_zones:
verify_data_plane_cli_azs(cli_azs, analysis_result)


def verify_data_plane_cli_machines(cli_machines: list[str], analysis_result: Analysis) -> None:
"""Verify if the machines passed from the CLI are valid.
:param cli_machines: Machines passed to the CLI as arguments
:type cli_machines: list[str]
:param analysis_result: Analysis result
:type analysis_result: Analysis
"""
data_plane_membership_check(
all_options=set(analysis_result.machines.keys()),
data_plane_options=set(analysis_result.data_plane_machines.keys()),
cli_input=parametrize_cli_inputs(cli_machines),
parameter_type="Machine(s)",
)


def verify_data_plane_cli_hostnames(cli_hostnames: list[str], analysis_result: Analysis) -> None:
"""Verify if the hostnames passed from the CLI are valid.
:param cli_hostnames: Hostnames passed to the CLI as arguments
:type cli_hostnames: list[str]
:param analysis_result: Analysis result
:type analysis_result: Analysis
"""
all_hostnames = {machine.hostname for machine in analysis_result.machines.values()}
data_plane_hostnames = {
machine.hostname for machine in analysis_result.data_plane_machines.values()
}

data_plane_membership_check(
all_options=all_hostnames,
data_plane_options=data_plane_hostnames,
cli_input=parametrize_cli_inputs(cli_hostnames),
parameter_type="Hostname(s)",
)


def verify_data_plane_cli_azs(cli_azs: list[str], analysis_result: Analysis) -> None:
"""Verify if the availability zones passed from the CLI are valid.
:param cli_azs: AZs passed to the CLI as arguments
:type cli_azs: list[str]
:param analysis_result: Analysis result
:type analysis_result: Analysis
:raises DataPlaneMachineFilterError: When the cloud does not have availability zones.
"""
all_azs_uncast: set[Optional[str]] = {
machine.az for machine in analysis_result.machines.values()
}
data_plane_azs_uncast: set[Optional[str]] = {
machine.az for machine in analysis_result.data_plane_machines.values()
}

# NOTE(gabrielcocenza) simple deployments may not have AZs. mypy does not understand
# that None is being removed from set, so the type goes from set[Optional[str]] to
# set[str]. As we can be sure of that, we can cast with the correct type to mypy.
all_azs_uncast.discard(None)
data_plane_azs_uncast.discard(None)

all_azs = cast(set[str], all_azs_uncast)
data_plane_azs = cast(set[str], data_plane_azs_uncast)

if data_plane_azs and all_azs:
data_plane_membership_check(
all_options=all_azs,
data_plane_options=data_plane_azs,
cli_input=parametrize_cli_inputs(cli_azs),
parameter_type="Availability Zone(s)",
)

raise DataPlaneMachineFilterError(
"Cannot find Availability Zone(s). Is this a valid OpenStack cloud?"
)


def parametrize_cli_inputs(cli_input: list[str]) -> set[str]:
"""Parametrize the cli inputs.
:param cli_input: cli inputs.
:type cli_input: list[str]
:return: A set of elements passed in the cli.
:rtype: set[str]
"""
return {raw_item.strip() for raw_items in cli_input for raw_item in raw_items.split(",")}


def data_plane_membership_check(
all_options: set[str],
data_plane_options: set[str],
cli_input: set[str],
parameter_type: str,
) -> None:
"""Check if the parameter passed are member of data-plane.
:param all_options: All possible options for a parameter.
:type all_options: set[str]
:param data_plane_options: All data-plane possible options for a parameter.
:type data_plane_options: set[str]
:param cli_input: The input that come from the cli
:type cli_input: set[str]
:param parameter_type: Type of the parameter passed (az, hostname or machine).
:type parameter_type: str
:raises DataPlaneMachineFilterError: When the value passed from the user is not sane.
"""
if not cli_input.issubset(all_options):
raise DataPlaneMachineFilterError(
f"{parameter_type}: {cli_input - all_options} don't exist."
)
if not cli_input.issubset(data_plane_options):
raise DataPlaneMachineFilterError(
f"{parameter_type}: {cli_input - data_plane_options} are not considered as data-plane."
)


def _get_os_release_and_series(analysis_result: Analysis) -> tuple[OpenStackRelease, str]:
"""Get the current OpenStack release and series of the cloud.
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/steps/test_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ def test_step_add_step():

def test_step_add_step_failed():
"""Test BaseStep adding sub steps failing."""
exp_error_msg = "only steps that are derived from BaseStep are supported"
exp_error_msg = "Cannot add an upgrade step that is not derived from BaseStep"
plan = BaseStep(description="plan")

with pytest.raises(TypeError, match=exp_error_msg):
Expand Down

0 comments on commit 2ae2550

Please sign in to comment.