Passed
Pull Request — master (#53)
by Vinicius
03:08
created

build.models.MaintenanceWindow.end_mw()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 3
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
nop 1
crap 1
1
"""Models used by the maintenance NApp.
2
3
This module define models for the maintenance window itself and the
4
scheduler.
5
"""
6 1
import datetime
7 1
from enum import IntEnum
8 1
from uuid import uuid4
9
10 1
import pytz
11 1
from apscheduler.jobstores.base import JobLookupError
12 1
from apscheduler.schedulers.background import BackgroundScheduler
13
14 1
from kytos.core import KytosEvent, log
15 1
from kytos.core.interface import TAG, UNI
16 1
from kytos.core.link import Link
17
18 1
TIME_FMT = "%Y-%m-%dT%H:%M:%S%z"
19
20
21 1
class Status(IntEnum):
22
    """Maintenance windows status."""
23
24 1
    PENDING = 0
25 1
    RUNNING = 1
26 1
    FINISHED = 2
27
28
29 1
class MaintenanceWindow:
30
    """Class to store a maintenance window."""
31
32 1
    def __init__(self, start, end, controller, **kwargs):
33
        """Create an instance of MaintenanceWindow.
34
35
        Args:
36
            start(datetime): when the maintenance will begin
37
            end(datetime): when the maintenance will finish
38
            items: list of items that will be maintained;
39
                each item can be either a switch, a link or a client interface
40
        """
41
        # pylint: disable=invalid-name
42 1
        self.controller = controller
43 1
        items = kwargs.get('items')
44 1
        if items is None:
45
            items = []
46 1
        mw_id = kwargs.get('mw_id')
47 1
        self.id = mw_id if mw_id else uuid4().hex
48 1
        self.description = kwargs.get('description')
49 1
        self.start = start
50 1
        self.end = end
51 1
        self._switches = []
52 1
        self._links = []
53 1
        self._unis = []
54 1
        self.items = items
55 1
        self.status = kwargs.get('status', Status.PENDING)
56
57 1
    @property
58 1
    def items(self):
59
        """Items getter."""
60 1
        return self._switches + self._links + self._unis
61
62 1
    @items.setter
63 1
    def items(self, items):
64
        """Items setter."""
65 1
        self._switches = []
66 1
        self._unis = []
67 1
        self._links = []
68 1
        for i in items:
69 1
            if isinstance(i, UNI):
70
                self._unis.append(i)
71 1
            elif isinstance(i, Link):
72
                self._links.append(i)
73
            else:
74 1
                self._switches.append(i)
75
76 1
    def as_dict(self):
77
        """Return this maintenance window as a dictionary."""
78 1
        mw_dict = {}
79 1
        mw_dict['id'] = self.id
80 1
        mw_dict['description'] = self.description if self.description else ''
81 1
        mw_dict['status'] = self.status
82 1
        mw_dict['start'] = self.start.strftime(TIME_FMT)
83 1
        mw_dict['end'] = self.end.strftime(TIME_FMT)
84 1
        mw_dict['items'] = []
85 1
        for i in self.items:
86 1
            try:
87 1
                mw_dict['items'].append(i.as_dict())
88 1
            except (AttributeError, TypeError):
89 1
                mw_dict['items'].append(i)
90 1
        return mw_dict
91
92 1
    @classmethod
93 1
    def from_dict(cls, mw_dict, controller):
94
        """Create a maintenance window from a dictionary of attributes."""
95
        mw_id = mw_dict.get('id')
96
97
        start = cls.str_to_datetime(mw_dict['start'])
98
        end = cls.str_to_datetime(mw_dict['end'])
99
        try:
100
            items = mw_dict['items']
101
        except KeyError:
102
            raise ValueError('At least one item must be provided')
103
        if not items:
104
            raise ValueError('At least one item must be provided')
105
        description = mw_dict.get('description')
106
        status = mw_dict.get('status', Status.PENDING)
107
        return cls(start, end, controller, items=items, mw_id=mw_id,
108
                   description=description, status=status)
109
110 1
    def update(self, mw_dict):
111
        """Update a maintenance window with the data from a dictionary."""
112 1
        try:
113 1
            start = self.str_to_datetime(mw_dict['start'])
114 1
        except KeyError:
115 1
            start = self.start
116 1
        try:
117 1
            end = self.str_to_datetime(mw_dict['end'])
118 1
        except KeyError:
119 1
            end = self.end
120 1
        now = datetime.datetime.now(pytz.utc)
121 1
        if start < now:
122
            raise ValueError('Start in the past not allowed.')
123 1
        if end < start:
124
            raise ValueError('End before start not allowed.')
125 1
        if 'items' in mw_dict:
126 1
            if not mw_dict['items']:
127 1
                raise ValueError('At least one item must be provided')
128 1
            self.items = mw_dict['items']
129 1
        self.start = start
130 1
        self.end = end
131 1
        if 'description' in mw_dict:
132
            self.description = mw_dict['description']
133
134 1
    @staticmethod
135 1
    def intf_from_dict(intf_id, controller):
136
        """Get the Interface instance with intf_id."""
137
        intf = controller.get_interface_by_id(intf_id)
138
        return intf
139
140 1
    @staticmethod
141 1
    def uni_from_dict(uni_dict, controller):
142
        """Create UNI instance from a dictionary."""
143
        intf = MaintenanceWindow.intf_from_dict(uni_dict['interface_id'],
144
                                                controller)
145
        tag = TAG.from_dict(uni_dict['tag'])
146
        if intf and tag:
147
            return UNI(intf, tag)
148
        return None
149
150 1
    @staticmethod
151 1
    def link_from_dict(link_dict, controller):
152
        """Create a link instance from a dictionary."""
153
        endpoint_a = controller.get_interface_by_id(
154
            link_dict['endpoint_a']['id'])
155
        endpoint_b = controller.get_interface_by_id(
156
            link_dict['endpoint_b']['id'])
157
158
        link = Link(endpoint_a, endpoint_b)
159
        if 'metadata' in link_dict:
160
            link.extend_metadata(link_dict['metadata'])
161
        s_vlan = link.get_metadata('s_vlan')
162
        if s_vlan:
163
            tag = TAG.from_dict(s_vlan)
164
            link.update_metadata('s_vlan', tag)
165
        return link
166
167 1
    @staticmethod
168 1
    def str_to_datetime(str_date):
169
        """Convert a string representing a date and time to datetime."""
170 1
        date = datetime.datetime.strptime(str_date, TIME_FMT)
171 1
        return date.astimezone(pytz.utc)
172
173 1
    def maintenance_event(self, operation):
174
        """Create events to start/end a maintenance."""
175 1
        if self._switches:
176 1
            switches = []
177 1
            for dpid in self._switches:
178 1
                switch = self.controller.switches.get(dpid, None)
179 1
                if switch:
180 1
                    switches.append(switch)
181 1
            event = KytosEvent(name=f'kytos/maintenance.{operation}_switch',
182
                               content={'switches': switches})
183 1
            self.controller.buffers.app.put(event)
184 1
        if self._unis:
185 1
            event = KytosEvent(name=f'kytos/maintenance.{operation}_uni',
186
                               content={'unis': self._unis})
187 1
            self.controller.buffers.app.put(event)
188 1
        if self._links:
189 1
            event = KytosEvent(name=f'kytos/maintenance.{operation}_link',
190
                               content={'links': self._links})
191 1
            self.controller.buffers.app.put(event)
192
193 1
    def start_mw(self):
194
        """Actions taken when a maintenance window starts."""
195 1
        self.status = Status.RUNNING
196 1
        self.maintenance_event('start')
197
198 1
    def end_mw(self):
199
        """Actions taken when a maintenance window finishes."""
200 1
        self.status = Status.FINISHED
201 1
        self.maintenance_event('end')
202
203
204 1
class Scheduler:
205
    """Scheduler for a maintenance window."""
206
207 1
    def __init__(self):
208
        """Initialize a new scheduler."""
209 1
        self.scheduler = BackgroundScheduler(timezone=pytz.utc)
210 1
        self.scheduler.start()
211
212 1
    def add(self, maintenance):
213
        """Add jobs to start and end a maintenance window."""
214 1
        self.scheduler.add_job(maintenance.start_mw, 'date',
215
                               id=f'{maintenance.id}-start',
216
                               run_date=maintenance.start)
217 1
        self.scheduler.add_job(maintenance.end_mw, 'date',
218
                               id=f'{maintenance.id}-end',
219
                               run_date=maintenance.end)
220
221 1
    def remove(self, maintenance):
222
        """Remove jobs that start and end a maintenance window."""
223 1
        try:
224 1
            self.scheduler.remove_job(f'{maintenance.id}-start')
225 1
        except JobLookupError:
226 1
            log.info(f'Job to start {maintenance.id} already removed.')
227 1
        try:
228 1
            self.scheduler.remove_job(f'{maintenance.id}-end')
229 1
        except JobLookupError:
230
            log.info(f'Job to end {maintenance.id} already removed.')
231