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

build.main   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Test Coverage

Coverage 91.3%

Importance

Changes 0
Metric Value
eloc 145
dl 0
loc 216
ccs 105
cts 115
cp 0.913
rs 9.52
c 0
b 0
f 0
wmc 36

10 Methods

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