Test Failed
Pull Request — master (#64)
by
unknown
03:02
created

build.main.Main.create_mw()   A

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

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