Passed
Push — master ( a726d1...e98202 )
by Italo Valcy
01:48 queued 14s
created

build.main.Main.create_mw()   B

Complexity

Conditions 6

Size

Total Lines 20
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 6.6563

Importance

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