Passed
Push — master ( 0d5b5f...2f9122 )
by
unknown
01:39 queued 12s
created

build.models.Scheduler._unschedule()   A

Complexity

Conditions 5

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 15
nop 2
dl 0
loc 19
ccs 15
cts 15
cp 1
crap 5
rs 9.1832
c 0
b 0
f 0
1
"""Models used by the maintenance NApp.
2
3
This module define models for the maintenance window itself and the
4
scheduler.
5
"""
6 1
from datetime import datetime
7 1
from enum import Enum
8 1
from typing import NewType, Optional
9 1
from uuid import uuid4
10
11 1
import pytz
12
# pylint: disable=no-name-in-module
13 1
from pydantic import BaseModel, Field, root_validator, validator
14
15
# pylint: enable=no-name-in-module
16
17 1
TIME_FMT = "%Y-%m-%dT%H:%M:%S%z"
18
19
20 1
class Status(str, Enum):
21
    """Maintenance windows status."""
22
23 1
    PENDING = 'pending'
24 1
    RUNNING = 'running'
25 1
    FINISHED = 'finished'
26
27
28 1
MaintenanceID = NewType('MaintenanceID', str)
29
30
31 1
class MaintenanceWindow(BaseModel):
32
    """Class for structure of maintenance windows.
33
    """
34 1
    start: datetime
35 1
    end: datetime
36 1
    switches: list[str] = Field(default_factory=list)
37 1
    interfaces: list[str] = Field(default_factory=list)
38 1
    links: list[str] = Field(default_factory=list)
39 1
    id: MaintenanceID = Field(
40
        default_factory=lambda: MaintenanceID(uuid4().hex)
41
    )
42 1
    description: str = Field(default='')
43 1
    status: Status = Field(default=Status.PENDING)
44 1
    inserted_at: Optional[datetime] = Field(default=None)
45 1
    updated_at: Optional[datetime] = Field(default=None)
46
47
    # pylint: disable=no-self-argument
48
49 1
    @validator('start', 'end', pre=True)
50 1
    def convert_time(cls, time):
51
        """Convert time strings using TIME_FMT"""
52 1
        if isinstance(time, str):
53 1
            time = datetime.strptime(time, TIME_FMT)
54 1
        return time
55
56 1
    @validator('start')
57 1
    def check_start_in_past(cls, start_time):
58
        """Check if the start is set to occur before now."""
59 1
        if start_time < datetime.now(pytz.utc):
60 1
            raise ValueError('Start in the past not allowed')
61 1
        return start_time
62
63 1
    @validator('end')
64 1
    def check_end_before_start(cls, end_time, values):
65
        """Check if the end is set to occur before the start."""
66 1
        if 'start' in values and end_time <= values['start']:
67 1
            raise ValueError('End before start not allowed')
68 1
        return end_time
69
70 1
    @root_validator
71 1
    def check_items_empty(cls, values):
72
        """Check if no items are in the maintenance window."""
73 1
        no_items = all(
74
            map(
75
                lambda key: key not in values or len(values[key]) == 0,
76
                ['switches', 'links', 'interfaces']
77
            )
78
        )
79 1
        if no_items:
80 1
            raise ValueError('At least one item must be provided')
81 1
        return values
82
83
    # pylint: enable=no-self-argument
84
85 1
    def __str__(self) -> str:
86
        return f"'{self.id}'<{self.start} to {self.end}>"
87
88 1
    class Config:
89
        """Config for encoding MaintenanceWindow class"""
90 1
        json_encoders = {
91
            datetime: lambda v: v.strftime(TIME_FMT),
92
        }
93
94
95 1
class MaintenanceWindows(BaseModel):
96
    """List of Maintenance Windows for json conversion."""
97 1
    __root__: list[MaintenanceWindow]
98
99 1
    def __iter__(self):
100
        return iter(self.__root__)
101
102 1
    def __getitem__(self, item):
103
        return self.__root__[item]
104
105 1
    def __len__(self):
106
        return len(self.__root__)
107
108 1
    class Config:
109
        """Config for encoding MaintenanceWindows class"""
110 1
        json_encoders = {
111
            datetime: lambda v: v.strftime(TIME_FMT),
112
        }
113
114
115 1
class OverlapError(Exception):
116
    """
117
    Exception for when a Maintenance Windows execution
118
    period overlaps with one or more windows.
119
    """
120 1
    new_window: MaintenanceWindow
121 1
    interfering: MaintenanceWindows
122
123 1
    def __init__(
124
                self,
125
                new_window: MaintenanceWindow,
126
                interfering: MaintenanceWindows
127
            ):
128
        self.new_window = new_window
129
        self.interfering = interfering
130
131 1
    def __str__(self):
132
        return f"Maintenance Window {self.new_window} " +\
133
            "interferes with the following windows: " +\
134
            '[' +\
135
            ', '.join([
136
                f"{window}"
137
                for window in self.interfering
138
            ]) +\
139
            ']'
140