Passed
Pull Request — master (#78)
by
unknown
05:53
created

build.main.Main.update_mw()   B

Complexity

Conditions 8

Size

Total Lines 30
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 8.1624

Importance

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