Skip to content

Commit

Permalink
Filter hypervisors. (#229)
Browse files Browse the repository at this point in the history
- do the nova-compute instance-count to find empty hypervisors
- integrate the force flag to include non-empty hypervisors
  • Loading branch information
gabrielcocenza committed Feb 6, 2024
1 parent 12dedbe commit 1b137f9
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 148 deletions.
11 changes: 10 additions & 1 deletion cou/apps/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
DEFAULT_WAITING_TIMEOUT = 5 * 60 # 5 min


@dataclass
@dataclass(frozen=True)
class ApplicationUnit:
"""Representation of a single unit of application."""

Expand All @@ -58,6 +58,14 @@ class ApplicationUnit:
machine: Machine
workload_version: str = ""

def __repr__(self) -> str:
"""Representation of the application unit.
:return: Representation of the application unit
:rtype: str
"""
return f"Unit[{self.name}]-Machine[{self.machine.machine_id}]"


@dataclass
class OpenStackApplication:
Expand Down Expand Up @@ -337,6 +345,7 @@ def current_os_release(self) -> OpenStackRelease:
f"'{openstack_release.codename}': {units}"
for openstack_release, units in os_versions.items()
]

raise MismatchedOpenStackVersions(
f"Units of application {self.name} are running mismatched OpenStack versions: "
f"{', '.join(mismatched_repr)}. This is not currently handled."
Expand Down
53 changes: 53 additions & 0 deletions cou/steps/plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from cou.apps.base import OpenStackApplication
from cou.apps.channel_based import OpenStackChannelBasedApplication # noqa: F401
from cou.apps.core import Keystone, Octavia # noqa: F401
from cou.apps.machine import Machine
from cou.apps.subordinate import ( # noqa: F401
OpenStackSubordinateApplication,
SubordinateBaseClass,
Expand All @@ -50,6 +51,7 @@
from cou.steps.analyze import Analysis
from cou.steps.backup import backup
from cou.utils.juju_utils import DEFAULT_TIMEOUT
from cou.utils.nova_compute import get_empty_hypervisors
from cou.utils.openstack import LTS_TO_OS_RELEASE, OpenStackRelease

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -327,6 +329,8 @@ async def generate_plan(analysis_result: Analysis, args: CLIargs) -> UpgradePlan
:rtype: UpgradePlan
"""
pre_plan_sanity_checks(args, analysis_result)
hypervisors = await filter_hypervisors_machines(args, analysis_result)
logger.info("Hypervisors selected: %s", hypervisors)
target = determine_upgrade_target(analysis_result)

plan = UpgradePlan(
Expand Down Expand Up @@ -375,6 +379,55 @@ async def generate_plan(analysis_result: Analysis, args: CLIargs) -> UpgradePlan
return plan


async def filter_hypervisors_machines(args: CLIargs, analysis_result: Analysis) -> list[Machine]:
"""Filter the hypervisors to generate plan and upgrade.
:param args: CLI arguments
:type args: CLIargs
:param analysis_result: Analysis result
:type analysis_result: Analysis
:return: hypervisors filtered to generate plan and upgrade.
:rtype: list[Machine]
"""
hypervisors_machines = await _get_upgradable_hypervisors_machines(args.force, analysis_result)

if cli_machines := args.machines:
return [machine for machine in hypervisors_machines if machine.machine_id in cli_machines]

if cli_hostnames := args.hostnames:
return [machine for machine in hypervisors_machines if machine.hostname in cli_hostnames]

if cli_azs := args.availability_zones:
return [machine for machine in hypervisors_machines if machine.az in cli_azs]

return hypervisors_machines


async def _get_upgradable_hypervisors_machines(
cli_force: bool, analysis_result: Analysis
) -> list[Machine]:
"""Get the hypervisors that are possible to upgrade.
:param cli_force: If force is used, it gets all hypervisors, otherwise just the empty ones
:type cli_force: bool
:param analysis_result: Analysis result
:type analysis_result: Analysis
:return: List of nova-compute units to upgrade
:rtype: list[Machine]
"""
nova_compute_units = [
unit
for app in analysis_result.apps_data_plane
for unit in app.units
if app.charm == "nova-compute"
]

if cli_force:
return [unit.machine for unit in nova_compute_units]

return await get_empty_hypervisors(nova_compute_units, analysis_result.model)


async def create_upgrade_group(
apps: list[OpenStackApplication],
target: OpenStackRelease,
Expand Down
25 changes: 0 additions & 25 deletions cou/utils/app_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,31 +50,6 @@ async def upgrade_packages(
await model.run_on_unit(unit_name=unit, command=command, timeout=600)


async def get_instance_count(unit: str, model: COUModel) -> int:
"""Get instance count on a nova-compute unit.
:param unit: Name of the nova-compute unit where the action runs on.
:type unit: str
:param model: COUModel object
:type model: COUModel
:return: Instance count of the nova-compute unit
:rtype: int
:raises ValueError: When the action result is not valid.
"""
action_name = "instance-count"
action = await model.run_action(unit_name=unit, action_name=action_name)

if (
instance_count := action.results.get("instance-count", "").strip()
) and instance_count.isdigit():
return int(instance_count)

raise ValueError(
f"No valid instance count value found in the result of {action_name} action "
f"running on '{unit}': {action.results}"
)


async def set_require_osd_release_option(unit: str, model: COUModel) -> None:
"""Check and set the correct value for require-osd-release on a ceph-mon unit.
Expand Down
62 changes: 62 additions & 0 deletions cou/utils/nova_compute.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2023 Canonical Limited
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Nova Compute utilities."""

import asyncio

from cou.apps.base import ApplicationUnit
from cou.apps.machine import Machine
from cou.utils.juju_utils import COUModel


async def get_empty_hypervisors(units: list[ApplicationUnit], model: COUModel) -> list[Machine]:
"""Get the empty hypervisors in the model.
:param units: all nova-compute units.
:type units: list[ApplicationUnit]
:param model: COUModel object
:type model: COUModel
:return: List with just the empty hypervisors machines.
:rtype: list[Machine]
"""
tasks = [get_instance_count(unit.name, model) for unit in units]
instances = await asyncio.gather(*tasks)
units_instances = zip(units, instances)
return [unit.machine for unit, instances in units_instances if instances == 0]


async def get_instance_count(unit: str, model: COUModel) -> int:
"""Get instance count on a nova-compute unit.
:param unit: Name of the nova-compute unit where the action runs on.
:type unit: str
:param model: COUModel object
:type model: COUModel
:return: Instance count of the nova-compute unit
:rtype: int
:raises ValueError: When the action result is not valid.
"""
action_name = "instance-count"
action = await model.run_action(unit_name=unit, action_name=action_name)

if (
instance_count := action.results.get("instance-count", "").strip()
) and instance_count.isdigit():
return int(instance_count)

raise ValueError(
f"No valid instance count value found in the result of {action_name} action "
f"running on '{unit}': {action.results}"
)
9 changes: 9 additions & 0 deletions tests/unit/apps/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@

import pytest

from cou.apps.base import ApplicationUnit
from cou.apps.core import Keystone
from cou.apps.machine import Machine
from cou.exceptions import (
ApplicationError,
HaltUpgradePlanGeneration,
Expand All @@ -32,6 +34,13 @@
from tests.unit.apps.utils import add_steps


def test_repr_ApplicationUnit():
app_unit = ApplicationUnit(
"keystone/0", OpenStackRelease("ussuri"), Machine("0", "juju-cef38-0", "zone-1"), "17.0.1"
)
assert repr(app_unit) == "Unit[keystone/0]-Machine[0]"


def test_application_eq(status, config, model, apps_machines):
"""Name of the app is used as comparison between Applications objects."""
status_keystone_1 = status["keystone_focal_ussuri"]
Expand Down
19 changes: 19 additions & 0 deletions tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from cou.apps.machine import Machine
from cou.apps.subordinate import OpenStackSubordinateApplication
from cou.commands import CLIargs
from cou.steps.analyze import Analysis
from cou.utils.openstack import OpenStackRelease

STANDARD_AZS = ["zone-1", "zone-2", "zone-3"]
Expand Down Expand Up @@ -592,6 +593,16 @@ def model(config, apps_machines):
return model


@pytest.fixture
def analysis_result(model, apps):
"""Generate a simple analysis result to be used on unit-tests."""
return Analysis(
model=model,
apps_control_plane=[apps["keystone_focal_ussuri"]],
apps_data_plane=[apps["nova_focal_ussuri"]],
)


@pytest.fixture
def apps(status, config, model, apps_machines):
keystone_focal_ussuri_status = status["keystone_focal_ussuri"]
Expand Down Expand Up @@ -682,3 +693,11 @@ def cli_args() -> MagicMock:
"""
# spec_set needs an instantiated class to be strict with the fields.
return MagicMock(spec_set=CLIargs(command="plan"))()


def generate_mock_machine(machine_id, hostname, az):
mock_machine = MagicMock(spec_set=Machine(machine_id, hostname, az))
mock_machine.machine_id = machine_id
mock_machine.hostname = hostname
mock_machine.az = az
return mock_machine
Loading

0 comments on commit 1b137f9

Please sign in to comment.