Passed
Pull Request — master (#64)
by
unknown
02:38
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 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
        try:
99 1
            if data.get('id') == '':
100
                del data['id']
101 1
            maintenance = MW.parse_obj(data)
102 1
            force = data.get('force', False)
103 1
        except ValidationError as err:
104 1
            raise BadRequest(f'{err.errors()[0]["msg"]}') from err
105 1
        try:
106 1
            self.scheduler.add(maintenance, force=force)
107
        except OverlapError as err:
108
            raise BadRequest(f'{err}') from err
109 1
        return jsonify({'mw_id': maintenance.id}), 201
110
111 1
    @rest('/v1/<mw_id>', methods=['PATCH'])
112 1
    def update_mw(self, mw_id: MaintenanceID):
113
        """Update a maintenance window."""
114 1
        data = request.get_json()
115 1
        if not data:
116 1
            raise UnsupportedMediaType('The request does not have a json')
117 1
        old_maintenance = self.scheduler.get_maintenance(mw_id)
118 1
        if old_maintenance is None:
119 1
            raise NotFound(f'Maintenance with id {mw_id} not found')
120 1
        if old_maintenance.status == Status.RUNNING:
121
            raise BadRequest('Updating a running maintenance is not allowed')
122 1
        if 'status' in data:
123 1
            raise BadRequest('Updating a maintenance status is not allowed')
124 1
        try:
125 1
            new_maintenance = MW.parse_obj({**old_maintenance.dict(), **data})
126 1
        except ValidationError as err:
127 1
            raise BadRequest(f'{err.errors()[0]["msg"]}') from err
128 1
        if new_maintenance.id != old_maintenance.id:
129
            raise BadRequest('Updated id must match old id')
130 1
        self.scheduler.update(new_maintenance)
131 1
        return jsonify({'response': f'Maintenance {mw_id} updated'}), 200
132
133 1
    @rest('/v1/<mw_id>', methods=['DELETE'])
134 1
    def remove_mw(self, mw_id: MaintenanceID):
135
        """Delete a maintenance window."""
136 1
        maintenance = self.scheduler.get_maintenance(mw_id)
137 1
        if maintenance is None:
138 1
            raise NotFound(f'Maintenance with id {mw_id} not found')
139 1
        if maintenance.status == Status.RUNNING:
140 1
            raise BadRequest('Deleting a running maintenance is not allowed')
141 1
        self.scheduler.remove(mw_id)
142 1
        return jsonify({'response': f'Maintenance with id {mw_id} '
143
                                    f'successfully removed'}), 200
144
145 1
    @rest('/v1/<mw_id>/end', methods=['PATCH'])
146 1
    def end_mw(self, mw_id: MaintenanceID):
147
        """Finish a maintenance window right now."""
148 1
        maintenance = self.scheduler.get_maintenance(mw_id)
149 1
        if maintenance is None:
150 1
            raise NotFound(f'Maintenance with id {mw_id} not found')
151 1
        if maintenance.status == Status.PENDING:
152 1
            raise BadRequest(
153
                f'Maintenance window {mw_id} has not yet started'
154
            )
155 1
        if maintenance.status == Status.FINISHED:
156 1
            raise BadRequest(
157
                f'Maintenance window {mw_id} has already finished'
158
            )
159 1
        self.scheduler.end_maintenance_early(mw_id)
160 1
        return jsonify({'response': f'Maintenance window {mw_id} '
161
                                    f'finished'}), 200
162
163 1
    @rest('/v1/<mw_id>/extend', methods=['PATCH'])
164 1
    def extend_mw(self, mw_id):
165
        """Extend a running maintenance window."""
166 1
        data = request.get_json()
167 1
        if not data:
168 1
            raise UnsupportedMediaType('The request does not have a json')
169 1
        maintenance = self.scheduler.get_maintenance(mw_id)
170 1
        if maintenance is None:
171 1
            raise NotFound(f'Maintenance with id {mw_id} not found')
172 1
        if 'minutes' not in data:
173 1
            raise BadRequest('Minutes of extension must be sent')
174 1
        if maintenance.status == Status.PENDING:
175 1
            raise BadRequest(
176
                f'Maintenance window {mw_id} has not yet started'
177
            )
178 1
        if maintenance.status == Status.FINISHED:
179 1
            raise BadRequest(
180
                f'Maintenance window {mw_id} has already finished'
181
            )
182 1
        try:
183 1
            maintenance_end = maintenance.end + \
184
                timedelta(minutes=data['minutes'])
185 1
            new_maintenance = maintenance.copy(
186
                update={'end': maintenance_end}
187
            )
188 1
        except TypeError as exc:
189 1
            raise BadRequest('Minutes of extension must be integer') from exc
190
191 1
        self.scheduler.update(new_maintenance)
192
        return jsonify({'response': f'Maintenance {mw_id} extended'}), 200
193