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

build.main   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 265
Duplicated Lines 0 %

Test Coverage

Coverage 88.8%

Importance

Changes 0
Metric Value
eloc 180
dl 0
loc 265
ccs 111
cts 125
cp 0.888
rs 8.8798
c 0
b 0
f 0
wmc 44

11 Methods

Rating   Name   Duplication   Size   Complexity  
A Main.get_all_mw() 0 8 1
A Main.get_mw() 0 12 2
A Main.shutdown() 0 6 1
A Main.setup() 0 11 1
A Main.execute() 0 2 1
B Main.update_mw() 0 30 8
C Main.create_mw() 0 34 9
B Main.validate_item_existence() 0 41 7
A Main.end_mw() 0 19 4
B Main.extend_mw() 0 41 7
A Main.remove_mw() 0 15 3

How to fix   Complexity   

Complexity

Complex classes like build.main often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
            if not force:
97 1
                self.validate_item_existence(maintenance)
98 1
            self.scheduler.add(maintenance, force=force)
99 1
        except ValidationError as err:
100 1
            msg = error_msg(err.errors())
101 1
            raise HTTPException(400, detail=msg) from err
102
        except DuplicateKeyError as err:
103
            raise HTTPException(
104
                409,
105
                detail=f'Window with id: {maintenance.id} already exists'
106
            ) from err
107
        except OverlapError as err:
108
            raise HTTPException(400, detail=f'{err}') from err
109
        except ValueError as err:
110
            raise HTTPException(400, detail=f'{err}') from err
111 1
        return JSONResponse({'mw_id': maintenance.id}, status_code=201)
112
113 1
    @rest('/v1/{mw_id}', methods=['PATCH'])
114 1
    def update_mw(self, request: Request) -> JSONResponse:
115
        """Update a maintenance window."""
116 1
        data = get_json_or_400(request, self.controller.loop)
117 1
        if not isinstance(data, dict) or not data:
118
            raise HTTPException(400, detail=f"Invalid json body value: {data}")
119
120 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
121 1
        old_maintenance = self.scheduler.get_maintenance(mw_id)
122 1
        if old_maintenance is None:
123 1
            raise HTTPException(
124
                404, detail=f'Maintenance with id {mw_id} not found'
125
            )
126 1
        if old_maintenance.status == Status.RUNNING:
127
            raise HTTPException(
128
                400, detail='Updating a running maintenance is not allowed'
129
            )
130 1
        if 'status' in data:
131 1
            raise HTTPException(
132
                400, detail='Updating a maintenance status is not allowed'
133
            )
134 1
        try:
135 1
            new_maintenance = MW.parse_obj({**old_maintenance.dict(), **data})
136 1
        except ValidationError as err:
137 1
            msg = error_msg(err.errors())
138 1
            raise HTTPException(400, detail=msg) from err
139 1
        if new_maintenance.id != old_maintenance.id:
140
            raise HTTPException(400, detail='Updated id must match old id')
141 1
        self.scheduler.update(new_maintenance)
142 1
        return JSONResponse({'response': f'Maintenance {mw_id} updated'})
143
144 1
    @rest('/v1/{mw_id}', methods=['DELETE'])
145 1
    def remove_mw(self, request: Request) -> JSONResponse:
146
        """Delete a maintenance window."""
147 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
148 1
        maintenance = self.scheduler.get_maintenance(mw_id)
149 1
        if maintenance is None:
150 1
            raise HTTPException(
151
                404, detail=f'Maintenance with id {mw_id} not found'
152
            )
153 1
        if maintenance.status == Status.RUNNING:
154 1
            raise HTTPException(
155
                400, detail='Deleting a running maintenance is not allowed'
156
            )
157 1
        self.scheduler.remove(mw_id)
158 1
        return JSONResponse({'response': f'Maintenance with id {mw_id} '
159
                                         f'successfully removed'})
160
161 1
    @rest('/v1/{mw_id}/end', methods=['PATCH'])
162 1
    def end_mw(self, request: Request) -> JSONResponse:
163
        """Finish a maintenance window right now."""
164 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
165 1
        maintenance = self.scheduler.get_maintenance(mw_id)
166 1
        if maintenance is None:
167 1
            raise HTTPException(
168
                404, detail=f'Maintenance with id {mw_id} not found'
169
            )
170 1
        if maintenance.status == Status.PENDING:
171 1
            raise HTTPException(
172
                400, detail=f'Maintenance window {mw_id} has not yet started'
173
            )
174 1
        if maintenance.status == Status.FINISHED:
175 1
            raise HTTPException(
176
                400, detail=f'Maintenance window {mw_id} has already finished'
177
            )
178 1
        self.scheduler.end_maintenance_early(mw_id)
179 1
        return JSONResponse({'response': f'Maintenance window {mw_id} '
180
                                         f'finished'})
181
182 1
    @rest('/v1/{mw_id}/extend', methods=['PATCH'])
183 1
    def extend_mw(self, request: Request) -> JSONResponse:
184
        """Extend a running maintenance window."""
185 1
        mw_id: MaintenanceID = request.path_params["mw_id"]
186 1
        data = get_json_or_400(request, self.controller.loop)
187 1
        if not isinstance(data, dict):
188
            raise HTTPException(400, detail=f"Invalid json body value: {data}")
189
190 1
        maintenance = self.scheduler.get_maintenance(mw_id)
191 1
        if maintenance is None:
192 1
            raise HTTPException(
193
                404, detail=f'Maintenance with id {mw_id} not found'
194
            )
195 1
        if 'minutes' not in data:
196 1
            raise HTTPException(
197
                400,
198
                detail='Minutes of extension must be sent'
199
            )
200 1
        if maintenance.status == Status.PENDING:
201 1
            raise HTTPException(
202
                400,
203
                detail=f'Maintenance window {mw_id} has not yet started'
204
            )
205 1
        if maintenance.status == Status.FINISHED:
206 1
            raise HTTPException(
207
                400,
208
                detail=f'Maintenance window {mw_id} has already finished'
209
            )
210 1
        try:
211 1
            maintenance_end = maintenance.end + \
212
                timedelta(minutes=data['minutes'])
213 1
            new_maintenance = maintenance.copy(
214
                update={'end': maintenance_end}
215
            )
216 1
        except TypeError as exc:
217 1
            raise HTTPException(
218
                400,
219
                detail='Minutes of extension must be integer') from exc
220
221 1
        self.scheduler.update(new_maintenance)
222 1
        return JSONResponse({'response': f'Maintenance {mw_id} extended'})
223
224 1
    def validate_item_existence(self, window: MW):
225
        """Validate that all items in a maintenance window exist."""
226 1
        non_existant_switches = list(
227
            filter(
228
                lambda switch_id:
229
                    self.controller.switches.get(switch_id)
230
                    is None,
231
                window.switches
232
            )
233
        )
234 1
        non_existant_interfaces = list(
235
            filter(
236
                lambda interface_id:
237
                    self.controller.get_interface_by_id(interface_id)
238
                    is None,
239
                window.interfaces
240
            )
241
        )
242 1
        non_existant_links = list(
243
            filter(
244
                lambda interface_id:
245
                    self.controller.napps[('kytos', 'topology')]
246
                    .links.get(interface_id)
247
                    is None,
248
                window.interfaces
249
            )
250
        )
251
252 1
        if (
253
            non_existant_switches
254
            or non_existant_interfaces
255
            or non_existant_links
256
        ):
257
            items = {
258
                'switches': non_existant_switches,
259
                'interfaces': non_existant_interfaces,
260
                'links': non_existant_links,
261
            }
262
            raise HTTPException(
263
                400,
264
                f"Window contains non-existant items: {items}")
265