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
![]() |
|||
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): |
|
120 | # If two time periods are overlapping, |
||
121 | # then the start of one time period must occur in the other time period |
||
122 | windows = self.windows.find( |
||
123 | { |
||
124 | '$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 | }, |
||
138 | {'_id': False} |
||
139 | ) |
||
140 | return MaintenanceWindows.model_construct( |
||
141 | root = [MaintenanceWindow.model_construct(**window) for window in windows] |
||
142 | ) |
||
143 | |||
144 | 1 | def get_windows(self) -> MaintenanceWindows: |
|
145 | 1 | windows = self.windows.find(projection={'_id': False}) |
|
146 | 1 | return MaintenanceWindows.model_construct( |
|
147 | root = [MaintenanceWindow.model_construct(**window) for window in windows] |
||
148 | ) |
||
149 | |||
150 | 1 | def get_unfinished_windows(self) -> MaintenanceWindows: |
|
151 | windows = self.windows.find( |
||
152 | {'status': {'$ne': Status.FINISHED}}, |
||
153 | projection={'_id': False} |
||
154 | ) |
||
155 | return MaintenanceWindows.model_construct( |
||
156 | root = [MaintenanceWindow.model_construct(**window) for window in windows] |
||
157 | ) |
||
158 | |||
159 | 1 | def remove_window(self, mw_id: MaintenanceID): |
|
160 | self.windows.delete_one({'id': mw_id}) |
||
161 | |||
162 | 1 | def prepare_start(self): |
|
163 | now = datetime.now(pytz.utc) |
||
164 | self.windows.update_many( |
||
165 | {'$and': [ |
||
166 | {'status': {'$eq': Status.PENDING}}, |
||
167 | {'start': {'$lte': now}}, |
||
168 | ]}, |
||
169 | { |
||
170 | '$set': { |
||
171 | 'status': Status.RUNNING, |
||
172 | 'last_modified': now, |
||
173 | }, |
||
174 | } |
||
175 | ) |
||
176 | self.windows.update_many( |
||
177 | {'$and': [ |
||
178 | {'status': {'$eq': Status.RUNNING}}, |
||
179 | {'end': {'$lte': now}}, |
||
180 | ]}, |
||
181 | { |
||
182 | '$set': { |
||
183 | 'status': Status.FINISHED, |
||
184 | 'last_modified': now, |
||
185 | }, |
||
186 | } |
||
187 | ) |
||
188 |