Skip to content
Merged
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
12 changes: 8 additions & 4 deletions actions/hypervisor.downtime.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,14 @@ parameters:
type: string
description: Hostname of the hypervisor, e.g. hvxyz.nubes.rl.ac.uk
required: true
duration_hours:
type: integer
description: Duration of downtime in hours, CANNOT be a decimal
required: true
end_time:
type: string
description: "Enter the end date of the downtime period, starting now, as a UTC timestamp in the format YYYY-MM-DD HH:MM. NOTE: if this field is left empty, then field 'duration' is required."
required: false
duration:
type: string
description: "Enter the duration of the downtime, starting now, as a number of days (d) and/or hours (h) (examples: 2d, 24h, 3d 12h). NOTE: if this field is left empty, then field 'end_time' is required."
required: false
comment:
type: string
description: Downtime reason
Expand Down
120 changes: 118 additions & 2 deletions lib/workflows/hypervisor_downtime.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from datetime import datetime, timedelta

import re
import pytz

from apis.icinga_api.downtime import schedule_downtime
Expand All @@ -13,19 +13,135 @@
from apis.alertmanager_api.structs.silence_details import SilenceDetails


def get_number_of_hours(start_dt, end_time_str, duration):
"""
Get the total number of hours between a start time and an end time

:param start_dt: start date
:type start_dt: datetime object

:param end_time_str: a datetime string in format "YYYY-MM-DD HH:MM"
:type end_time_str: str

:param duration: a duration string like "5d 12h", "3d", "24h", etc.
:type duration: str

:return: the number of hours to end date
:rtype: int

:raises ValueError: if the input string is invalid or contains negative values
"""
end_time_str = end_time_str.strip()
duration = duration.strip()
if not end_time_str and not duration:
raise ValueError("Input strings cannot be both empty at the same time")

if end_time_str and duration:
raise ValueError("Input strings cannot be both valid at the same time")

if end_time_str:
return _get_number_of_hours_from_absolute_datetime(start_dt, end_time_str)

if duration:
return _get_number_of_hours_from_duration(duration)

# if we are still here is because no return statement has been issued
# meaning the input did not match any of the valid formats
raise ValueError(
"Invalid input format. Expected either 'YYYY-MM-DD HH:MM' "
f"or duration format like '5d', '12h', '2d 6h'. Got: '{end_time_str}'"
)


def _get_number_of_hours_from_absolute_datetime(start_dt, datetime_str):
"""
Get the total number of hours from a start time and an end time

:param start_dt: start time
:type start_dt: datetime object

:param datetime_str: datetime string in format "YYYY-MM-DD HH:MM"
:type end_time_str: str

:return: the number of hours to end date
:rtype: int

:raises ValueError: if the input string is invalid
:raises ValueError: if the end date is earlier that start date
"""
try:
end_dt = datetime.strptime(datetime_str, "%Y-%m-%d %H:%M").replace(
tzinfo=pytz.utc
)
except ValueError as exc:
raise ValueError(
f"Invalid datetime format. Expected 'YYYY-MM-DD HH:MM', got '{datetime_str}'"
) from exc
if start_dt >= end_dt:
raise ValueError("end time cannot be earlier than now")
duration = end_dt - start_dt
# Convert total seconds to hours
hours = int(duration.total_seconds() / 3600)
return hours


def _get_number_of_hours_from_duration(duration_str):
"""
Get the total number of hours from a duration string

:param duration_str: duration string like "5d 12h", "3d", "24h", etc.
:type duration_str: str

:return: the number of hours to end date
:rtype: int

:raises ValueError: if the input string has wrong format
"""
duration_str = duration_str.strip()
# Check for negative signs
if "-" in duration_str:
raise ValueError(
"Negative durations are not allowed. Only positive numbers are permitted."
)

days = 0
hours = 0

# Search for days pattern
days_match = re.search(r"(\d+)d", duration_str, re.IGNORECASE)
if days_match:
days = int(days_match.group(1))

# Search for hours pattern
hours_match = re.search(r"(\d+)h", duration_str, re.IGNORECASE)
if hours_match:
hours = int(hours_match.group(1))

# Validate that at least one unit was found
if days == 0 and hours == 0:
raise ValueError(
f"Invalid duration format. Expected format like '5d', '12h', or '2d 6h', got '{duration_str}'"
)

total_hours = days * 24 + hours
return total_hours


# pylint:disable=too-many-locals
def schedule_hypervisor_downtime(
icinga_account: IcingaAccount,
alertmanager_account: AlertManagerAccount,
hypervisor_name: str,
comment: str,
duration_hours: int,
end_time: str,
duration: str,
set_silence: bool,
set_downtime: bool,
):

# Local UK time to Unix timestamp
start_datetime = datetime.now(pytz.utc)
duration_hours = get_number_of_hours(start_datetime, end_time, duration)
end_datetime = start_datetime + timedelta(hours=duration_hours)

start_timestamp = int(start_datetime.timestamp())
Expand Down
Loading