Passed
Pull Request — master (#135)
by Aldo
03:45
created

MaintenanceController.check_overlap()   A

Complexity

Conditions 2

Size

Total Lines 30
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
cc 2
eloc 20
nop 3
dl 0
loc 30
ccs 6
cts 6
cp 1
crap 2
rs 9.4
c 0
b 0
f 0
1
"""MaintenanceController."""
2
3
# pylint: disable=invalid-name
4 1
from datetime import datetime
5 1
import os
6 1
import pytz
7 1
from typing import Optional
8
9 1
from bson.codec_options import CodecOptions
10 1
import pymongo
11 1
from pymongo.errors import ConnectionFailure, ExecutionTimeout
12 1
from tenacity import retry_if_exception_type, stop_after_attempt, wait_random
13
14 1
from kytos.core import log
15 1
from kytos.core.db import Mongo
16 1
from kytos.core.retry import before_sleep, for_all_methods, retries
17 1
from napps.kytos.maintenance.models import (
18
    MaintenanceWindow,
19
    MaintenanceWindows,
20
    MaintenanceID,
21
    Status,
22
)
23
24
25 1
@for_all_methods(
26
    retries,
27
    stop=stop_after_attempt(
28
        int(os.environ.get("MONGO_AUTO_RETRY_STOP_AFTER_ATTEMPT", 3))
29
    ),
30
    wait=wait_random(
31
        min=int(os.environ.get("MONGO_AUTO_RETRY_WAIT_RANDOM_MIN", 0.1)),
32
        max=int(os.environ.get("MONGO_AUTO_RETRY_WAIT_RANDOM_MAX", 1)),
33
    ),
34
    before_sleep=before_sleep,
35
    retry=retry_if_exception_type((ConnectionFailure, ExecutionTimeout)),
36
)
37 1
class MaintenanceController:
38
    """MaintenanceController."""
39
40 1
    def __init__(self, get_mongo=lambda: Mongo()) -> None:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable Mongo does not seem to be defined.
Loading history...
41
        """Constructor of MaintenanceController."""
42 1
        self.mongo = get_mongo()
43 1
        self.db_client = self.mongo.client
44 1
        self.db = self.db_client[self.mongo.db_name]
45 1
        self.windows = self.db['maintenance.windows'].with_options(
46
            codec_options=CodecOptions(
47
                tz_aware=True,
48
            )
49
        )
50
51 1
    def bootstrap_indexes(self) -> None:
52
        """Bootstrap all maintenance related indexes."""
53 1
        unique_index_tuples = [
54
            ("maintenance.windows", [("id", pymongo.ASCENDING)]),
55
        ]
56 1
        for collection, keys in unique_index_tuples:
57 1
            if self.mongo.bootstrap_index(collection, keys, unique=True):
58 1
                log.info(
59
                    f"Created DB unique index {keys}, collection: {collection})"
60
                )
61
62 1
    def insert_window(self, window: MaintenanceWindow):
63 1
        now = datetime.now(pytz.utc)
64 1
        self.windows.insert_one({
65
                    **window.model_dump(exclude={'inserted_at', 'updated_at'}),
66
                    'inserted_at': now,
67
                    'updated_at': now,
68
        })
69
70 1
    def update_window(self, window: MaintenanceWindow):
71 1
        self.windows.update_one(
72
            {'id': window.id},
73
            [{
74
                '$set': {
75
                    **window.model_dump(exclude={'inserted_at', 'updated_at'}),
76
                    'updated_at': '$$NOW',
77
                },
78
            }],
79
        )
80
81 1
    def get_window(self, mw_id: MaintenanceID) -> Optional[MaintenanceWindow]:
82 1
        window = self.windows.find_one(
83
            {'id': mw_id},
84
            {'_id': False},
85
        )
86 1
        if window is None:
87 1
            return None
88
        else:
89 1
            return MaintenanceWindow.model_construct(**window)
90
91 1
    def start_window(self, mw_id: MaintenanceID) -> MaintenanceWindow:
92
        window = self.windows.find_one_and_update(
93
            {'id': mw_id},
94
            [{
95
                '$set': {
96
                    'status': Status.RUNNING,
97
                    'last_modified': '$$NOW',
98
                },
99
            }],
100
            {'_id': False},
101
            return_document=pymongo.ReturnDocument.AFTER,
102
        )
103
        return MaintenanceWindow.model_construct(**window)
104
105 1
    def end_window(self, mw_id: MaintenanceID) -> MaintenanceWindow:
106
        window = self.windows.find_one_and_update(
107
            {'id': mw_id},
108
            [{
109
                '$set': {
110
                    'status': Status.FINISHED,
111
                    'last_modified': '$$NOW',
112
                },
113
            }],
114
            {'_id': False},
115
            return_document=pymongo.ReturnDocument.AFTER,
116
        )
117
        return MaintenanceWindow.model_construct(**window)
118
119 1
    def check_overlap(self, window: MaintenanceWindow, force: bool):
120
        """Check for overlap in the time periods of the MWs.
121
         If force=False, check for overlapping between MWs.
122
         If force=True, check for overlapping only between components.
123
        """
124 1
        query = {'$and': [
125
            {'status': {'$ne': Status.FINISHED}},
126
            {'$or': [
127
                {'$and': [
128
                    {'start': {'$lte': window.start}},
129
                    {'end': {'$gt': window.start}},
130
                ]},
131
                {'$and': [
132
                    {'start': {'$gte': window.start}},
133
                    {'start': {'$lt': window.end}},
134
                ]},
135
            ]},
136
        ]}
137 1
        if force:
138 1
            query['$and'].append({'$or': [
139
                {'switches': {"$in": window.switches}},
140
                {'interfaces': {"$in": window.interfaces}},
141
                {'links': {"$in": window.links}},
142
            ]})
143 1
        windows = self.windows.find(
144
            query,
145
            {'_id': False}
146
        )
147 1
        return MaintenanceWindows.model_construct(
148
            root = [MaintenanceWindow.model_construct(**window) for window in windows]
149
        )
150
151 1
    def get_windows(self) -> MaintenanceWindows:
152 1
        windows = self.windows.find(projection={'_id': False})
153 1
        return MaintenanceWindows.model_construct(
154
            root = [MaintenanceWindow.model_construct(**window) for window in windows]
155
        )
156
157 1
    def get_unfinished_windows(self) -> MaintenanceWindows:
158
        windows = self.windows.find(
159
            {'status': {'$ne': Status.FINISHED}},
160
            projection={'_id': False}
161
        )
162
        return MaintenanceWindows.model_construct(
163
            root = [MaintenanceWindow.model_construct(**window) for window in windows]
164
        )
165
166 1
    def remove_window(self, mw_id: MaintenanceID):
167
        self.windows.delete_one({'id': mw_id})
168
169 1
    def prepare_start(self):
170
        now = datetime.now(pytz.utc)
171
        self.windows.update_many(
172
            {'$and': [
173
                {'status': {'$eq': Status.PENDING}},
174
                {'start': {'$lte': now}},
175
            ]},
176
            {
177
                '$set': {
178
                    'status': Status.RUNNING,
179
                    'last_modified': now,
180
                },
181
            }
182
        )
183
        self.windows.update_many(
184
            {'$and': [
185
                {'status': {'$eq': Status.RUNNING}},
186
                {'end': {'$lte': now}},
187
            ]},
188
            {
189
                '$set': {
190
                    'status': Status.FINISHED,
191
                    'last_modified': now,
192
                },
193
            }
194
        )
195