Code

< 40 %
40-60 %
> 60 %
1
"""Main module of kytos/maintenance Kytos Network Application.
2
3
This NApp creates maintenance windows, allowing the maintenance of network
4
devices (switch, link, and interface) without receiving alerts.
5
"""
6
7 1
from datetime import timedelta
8
9 1
from napps.kytos.maintenance.managers import MaintenanceDeployer as Deployer
10 1
from napps.kytos.maintenance.managers import MaintenanceScheduler as Scheduler
11 1
from napps.kytos.maintenance.models import MaintenanceID
12 1
from napps.kytos.maintenance.models import MaintenanceWindow as MW
13 1
from napps.kytos.maintenance.models import OverlapError, Status
14 1
from pydantic import ValidationError
15 1
from pymongo.errors import DuplicateKeyError
16
17 1
from kytos.core import KytosNApp, rest
18 1
from kytos.core.rest_api import (
19
    HTTPException,
20
    JSONResponse,
21
    Request,
22
    Response,
23
    error_msg,
24
    get_json_or_400,
25
)
26
27
28 1
class Main(KytosNApp):
29
    """Main class of kytos/maintenance NApp.
30
31
    This class is the entry point for this napp.
32
    """
33
34 1
    def setup(self):
35
        """Replace the '__init__' method for the KytosNApp subclass.
36
37
        The setup method is automatically called by the controller when your
38
        application is loaded.
39
40
        So, if you have any setup routine, insert it here.
41
        """
42 1
        self.maintenance_deployer = Deployer.new_deployer(self.controller)
43 1
        self.scheduler = Scheduler.new_scheduler(self.maintenance_deployer)
44 1
        self.scheduler.start()
45
46 1
    def execute(self):
47
        """Run after the setup method execution.
48
49
        You can also use this method in loop mode if you add to the above setup
50
        method a line like the following example:
51
52
            self.execute_as_loop(30)  # 30-second interval.
53
        """
54
55 1
    def shutdown(self):
56
        """Run when your napp is unloaded.
57
58
        If you have some cleanup procedure, insert it here.
59
        """
60
        self.scheduler.shutdown()
61
62 1
    @rest("/v1", methods=["GET"])
63 1
    def get_all_mw(self, _request: Request) -> Response:
64
        """Return all maintenance windows."""
65 1
        maintenances = self.scheduler.list_maintenances()
66 1
        return Response(
67
            f"{maintenances.json()}\n",
68
            status_code=200,
69
            media_type="application/json",
70
        )
71
72 1
    @rest("/v1/{mw_id}", methods=["GET"])
73 1
    def get_mw(self, request: Request) -> Response:
74
        """Return one maintenance window."""
75 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
76 1
        window = self.scheduler.get_maintenance(mw_id)
77 1
        if window:
78 1
            return Response(
79
                f"{window.json()}\n",
80
                status_code=200,
81
                media_type="application/json",
82
            )
83 1
        raise HTTPException(404, f"Maintenance with id {mw_id} not found")
84
85 1
    @rest("/v1", methods=["POST"])
86 1
    def create_mw(self, request: Response) -> JSONResponse:
87
        """Create a new maintenance window."""
88 1
        data = get_json_or_400(request, self.controller.loop)
89 1
        if not isinstance(data, dict) or not data:
90
            raise HTTPException(400, detail=f"Invalid json body value: {data}")
91
92 1
        if "status" in data:
93 1
            raise HTTPException(
94
                400, detail="Setting a maintenance status is not allowed"
95
            )
96
        # if 'id' in data:
97
        #     raise HTTPException(
98
        #         400, detail='Setting a maintenance id is not allowed'
99
        #     )
100 1
        try:
101 1
            maintenance = MW.model_validate(data)
102 1
            force = data.get("force", False)
103 1
            ignore_no_exists = data.get("ignore_no_exists")
104 1
            if not ignore_no_exists:
105 1
                self.validate_item_existence(maintenance)
106 1
            self.scheduler.add(maintenance, force=force)
107 1
        except ValidationError as err:
108 1
            msg = error_msg(err.errors())
109 1
            raise HTTPException(400, detail=msg) from err
110
        except DuplicateKeyError as err:
111
            raise HTTPException(
112
                409, detail=f"Window with id: {maintenance.id} already exists"
113
            ) from err
114
        except OverlapError as err:
115
            raise HTTPException(400, detail=f"{err}") from err
116
        except ValueError as err:
117
            raise HTTPException(400, detail=f"{err}") from err
118 1
        return JSONResponse({"mw_id": maintenance.id}, status_code=201)
119
120 1
    @rest("/v1/{mw_id}", methods=["PATCH"])
121 1
    def update_mw(self, request: Request) -> JSONResponse:
122
        """Update a maintenance window."""
123 1
        data = get_json_or_400(request, self.controller.loop)
124 1
        if not isinstance(data, dict) or not data:
125
            raise HTTPException(400, detail=f"Invalid json body value: {data}")
126
127 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
128 1
        old_maintenance = self.scheduler.get_maintenance(mw_id)
129 1
        if old_maintenance is None:
130 1
            raise HTTPException(404, detail=f"Maintenance with id {mw_id} not found")
131 1
        if old_maintenance.status == Status.RUNNING:
132
            raise HTTPException(
133
                400, detail="Updating a running maintenance is not allowed"
134
            )
135 1
        if "status" in data:
136 1
            raise HTTPException(
137
                400, detail="Updating a maintenance status is not allowed"
138
            )
139 1
        try:
140 1
            new_maintenance = MW.model_validate(
141
                {**old_maintenance.model_dump(), **data}
142
            )
143 1
        except ValidationError as err:
144 1
            msg = error_msg(err.errors())
145 1
            raise HTTPException(400, detail=msg) from err
146 1
        if new_maintenance.id != old_maintenance.id:
147
            raise HTTPException(400, detail="Updated id must match old id")
148 1
        self.scheduler.update(new_maintenance)
149 1
        return JSONResponse({"response": f"Maintenance {mw_id} updated"})
150
151 1
    @rest("/v1/{mw_id}", methods=["DELETE"])
152 1
    def remove_mw(self, request: Request) -> JSONResponse:
153
        """Delete a maintenance window."""
154 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
155 1
        maintenance = self.scheduler.get_maintenance(mw_id)
156 1
        if maintenance is None:
157 1
            raise HTTPException(404, detail=f"Maintenance with id {mw_id} not found")
158 1
        if maintenance.status == Status.RUNNING:
159 1
            raise HTTPException(
160
                400, detail="Deleting a running maintenance is not allowed"
161
            )
162 1
        self.scheduler.remove(mw_id)
163 1
        return JSONResponse(
164
            {"response": f"Maintenance with id {mw_id} successfully removed"}
165
        )
166
167 1
    @rest("/v1/{mw_id}/end", methods=["PATCH"])
168 1
    def end_mw(self, request: Request) -> JSONResponse:
169
        """Finish a maintenance window right now."""
170 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
171 1
        maintenance = self.scheduler.get_maintenance(mw_id)
172 1
        if maintenance is None:
173 1
            raise HTTPException(404, detail=f"Maintenance with id {mw_id} not found")
174 1
        if maintenance.status == Status.PENDING:
175 1
            raise HTTPException(
176
                400, detail=f"Maintenance window {mw_id} has not yet started"
177
            )
178 1
        if maintenance.status == Status.FINISHED:
179 1
            raise HTTPException(
180
                400, detail=f"Maintenance window {mw_id} has already finished"
181
            )
182 1
        self.scheduler.end_maintenance_early(mw_id)
183 1
        return JSONResponse({"response": f"Maintenance window {mw_id} " f"finished"})
184
185 1
    @rest("/v1/{mw_id}/extend", methods=["PATCH"])
186 1
    def extend_mw(self, request: Request) -> JSONResponse:
187
        """Extend a running maintenance window."""
188 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
189 1
        data = get_json_or_400(request, self.controller.loop)
190 1
        if not isinstance(data, dict):
191
            raise HTTPException(400, detail=f"Invalid json body value: {data}")
192
193 1
        maintenance = self.scheduler.get_maintenance(mw_id)
194 1
        if maintenance is None:
195 1
            raise HTTPException(404, detail=f"Maintenance with id {mw_id} not found")
196 1
        if "minutes" not in data:
197 1
            raise HTTPException(400, detail="Minutes of extension must be sent")
198 1
        if maintenance.status == Status.PENDING:
199 1
            raise HTTPException(
200
                400, detail=f"Maintenance window {mw_id} has not yet started"
201
            )
202 1
        if maintenance.status == Status.FINISHED:
203 1
            raise HTTPException(
204
                400, detail=f"Maintenance window {mw_id} has already finished"
205
            )
206 1
        try:
207 1
            maintenance_end = maintenance.end + timedelta(minutes=data["minutes"])
208 1
            new_maintenance = maintenance.copy(update={"end": maintenance_end})
209 1
        except TypeError as exc:
210 1
            raise HTTPException(
211
                400, detail="Minutes of extension must be integer"
212
            ) from exc
213
214 1
        self.scheduler.update(new_maintenance)
215 1
        return JSONResponse({"response": f"Maintenance {mw_id} extended"})
216
217 1
    def validate_item_existence(self, window: MW):
218
        """Validate that all items in a maintenance window exist."""
219 1
        non_existant_switches = list(
220
            filter(
221
                lambda switch_id: self.controller.switches.get(switch_id) is None,
222
                window.switches,
223
            )
224
        )
225 1
        non_existant_interfaces = list(
226
            filter(
227
                lambda interface_id: self.controller.get_interface_by_id(interface_id)
228
                is None,
229
                window.interfaces,
230
            )
231
        )
232 1
        non_existant_links = list(
233
            filter(
234
                lambda link_id: self.controller.links.get(link_id) is None,
235
                window.links,
236
            )
237
        )
238
239 1
        if non_existant_switches or non_existant_interfaces or non_existant_links:
240
            items = {
241
                "switches": non_existant_switches,
242
                "interfaces": non_existant_interfaces,
243
                "links": non_existant_links,
244
            }
245
            raise HTTPException(400, f"Window contains non-existant items: {items}")
246