Passed
Pull Request — master (#78)
by
unknown
02:34
created

build.main.Main.execute()   A

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

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