Test Failed
Pull Request — master (#966)
by Gleyberson
02:44
created

kytos.core.controller.Controller.__init__()   A

Complexity

Conditions 2

Size

Total Lines 69
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 2.0004

Importance

Changes 0
Metric Value
cc 2
eloc 24
nop 3
dl 0
loc 69
ccs 20
cts 21
cp 0.9524
crap 2.0004
rs 9.304
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
"""Kytos SDN Platform main class.
2
3
This module contains the main class of Kytos, which is
4
:class:`~.core.Controller`.
5
6
Basic usage:
7
8
.. code-block:: python3
9
10
    from kytos.config import KytosConfig
11
    from kytos.core import Controller
12
    config = KytosConfig()
13
    controller = Controller(config.options)
14
    controller.start()
15
"""
16 1
import asyncio
17 1
import atexit
18 1
import json
19 1
import logging
20 1
import os
21 1
import re
22 1
import sys
23 1
import threading
24 1
from concurrent.futures import ThreadPoolExecutor
25 1
from importlib import import_module
26 1
from importlib import reload as reload_module
27 1
from importlib.util import module_from_spec, spec_from_file_location
28 1
from pathlib import Path
29
30 1
from kytos.core.api_server import APIServer
31
# from kytos.core.tcp_server import KytosRequestHandler, KytosServer
32 1
from kytos.core.atcp_server import KytosServer, KytosServerProtocol
33 1
from kytos.core.auth import Auth
34 1
from kytos.core.buffers import KytosBuffers
35 1
from kytos.core.config import KytosConfig
36 1
from kytos.core.connection import ConnectionState
37 1
from kytos.core.events import KytosEvent
38 1
from kytos.core.helpers import now
39 1
from kytos.core.interface import Interface
40 1
from kytos.core.logs import LogManager
41 1
from kytos.core.napps.base import NApp
42 1
from kytos.core.napps.manager import NAppsManager
43 1
from kytos.core.napps.napp_dir_listener import NAppDirListener
44 1
from kytos.core.switch import Switch
45
46 1
__all__ = ('Controller',)
47
48
49 1
class Controller:
50
    """Main class of Kytos.
51
52
    The main responsibilities of this class are:
53
        - start a thread with :class:`~.core.tcp_server.KytosServer`;
54
        - manage KytosNApps (install, load and unload);
55
        - keep the buffers (instance of :class:`~.core.buffers.KytosBuffers`);
56
        - manage which event should be sent to NApps methods;
57
        - manage the buffers handlers, considering one thread per handler.
58
    """
59
60
    # Created issue #568 for the disabled checks.
61
    # pylint: disable=too-many-instance-attributes,too-many-public-methods
62 1
    def __init__(self, options=None, loop=None):
63
        """Init method of Controller class takes the parameters below.
64
65
        Args:
66
            options (:attr:`ParseArgs.args`): :attr:`options` attribute from an
67
                instance of :class:`~kytos.core.config.KytosConfig` class.
68
        """
69 1
        if options is None:
70
            options = KytosConfig().options['daemon']
71
72 1
        self._loop = loop or asyncio.get_event_loop()
73 1
        self._pool = ThreadPoolExecutor(max_workers=1)
74
75
        #: dict: keep the main threads of the controller (buffers and handler)
76 1
        self._threads = {}
77
        #: KytosBuffers: KytosBuffer object with Controller buffers
78 1
        self.buffers = KytosBuffers(loop=self._loop)
79
        #: dict: keep track of the socket connections labeled by ``(ip, port)``
80
        #:
81
        #: This dict stores all connections between the controller and the
82
        #: switches. The key for this dict is a tuple (ip, port). The content
83
        #: is another dict with the connection information.
84 1
        self.connections = {}
85
        #: dict: mapping of events and event listeners.
86
        #:
87
        #: The key of the dict is a KytosEvent (or a string that represent a
88
        #: regex to match agains KytosEvents) and the value is a list of
89
        #: methods that will receive the referenced event
90 1
        self.events_listeners = {'kytos/core.connection.new':
91
                                 [self.new_connection]}
92
93
        #: dict: Current loaded apps - ``'napp_name'``: ``napp`` (instance)
94
        #:
95
        #: The key is the napp name (string), while the value is the napp
96
        #: instance itself.
97 1
        self.napps = {}
98
        #: Object generated by ParseArgs on config.py file
99 1
        self.options = options
100
        #: KytosServer: Instance of KytosServer that will be listening to TCP
101
        #: connections.
102 1
        self.server = None
103
        #: dict: Current existing switches.
104
        #:
105
        #: The key is the switch dpid, while the value is a Switch object.
106 1
        self.switches = {}  # dpid: Switch()
107
108
        #: datetime.datetime: Time when the controller finished starting.
109 1
        self.started_at = None
110
111
        #: logging.Logger: Logger instance used by Kytos.
112 1
        self.log = None
113
114
        #: Observer that handle NApps when they are enabled or disabled.
115 1
        self.napp_dir_listener = NAppDirListener(self)
116
117 1
        self.napps_manager = NAppsManager(self)
118
119
        #: API Server used to expose rest endpoints.
120 1
        self.api_server = APIServer(__name__, self.options.listen,
121
                                    self.options.api_port,
122
                                    self.napps_manager, self.options.napps)
123
124 1
        self.auth = Auth(self)
125
126 1
        self._register_endpoints()
127
        #: Adding the napps 'enabled' directory into the PATH
128
        #: Now you can access the enabled napps with:
129
        #: from napps.<username>.<napp_name> import ?....
130 1
        sys.path.append(os.path.join(self.options.napps, os.pardir))
131
132 1
    def enable_logs(self):
133
        """Register kytos log and enable the logs."""
134
        LogManager.load_config_file(self.options.logging, self.options.debug)
135
        LogManager.enable_websocket(self.api_server.server)
136
        self.log = logging.getLogger(__name__)
137
138 1
    def start(self, restart=False):
139
        """Create pidfile and call start_controller method."""
140
        self.enable_logs()
141
        if not restart:
142
            self.create_pidfile()
143
        self.start_controller()
144
145 1
    def create_pidfile(self):
146
        """Create a pidfile."""
147
        pid = os.getpid()
148
149
        # Creates directory if it doesn't exist
150
        # System can erase /var/run's content
151
        pid_folder = Path(self.options.pidfile).parent
152
        self.log.info(pid_folder)
153
154
        # Pylint incorrectly infers Path objects
155
        # https://github.com/PyCQA/pylint/issues/224
156
        # pylint: disable=no-member
157
        if not pid_folder.exists():
158
            pid_folder.mkdir()
159
            pid_folder.chmod(0o1777)
160
        # pylint: enable=no-member
161
162
        # Make sure the file is deleted when controller stops
163
        atexit.register(Path(self.options.pidfile).unlink)
164
165
        # Checks if a pidfile exists. Creates a new file.
166
        try:
167
            pidfile = open(self.options.pidfile, mode='x')
168
        except OSError:
169
            # This happens if there is a pidfile already.
170
            # We shall check if the process that created the pidfile is still
171
            # running.
172
            try:
173
                existing_file = open(self.options.pidfile, mode='r')
174
                old_pid = int(existing_file.read())
175
                os.kill(old_pid, 0)
176
                # If kill() doesn't return an error, there's a process running
177
                # with the same PID. We assume it is Kytos and quit.
178
                # Otherwise, overwrite the file and proceed.
179
                error_msg = ("PID file {} exists. Delete it if Kytos is not "
180
                             "running. Aborting.")
181
                sys.exit(error_msg.format(self.options.pidfile))
182
            except OSError:
183
                try:
184
                    pidfile = open(self.options.pidfile, mode='w')
185
                except OSError as exception:
186
                    error_msg = "Failed to create pidfile {}: {}."
187
                    sys.exit(error_msg.format(self.options.pidfile, exception))
188
189
        # Identifies the process that created the pidfile.
190
        pidfile.write(str(pid))
191
        pidfile.close()
192
193 1
    def start_controller(self):
194
        """Start the controller.
195
196
        Starts the KytosServer (TCP Server) coroutine.
197
        Starts a thread for each buffer handler.
198
        Load the installed apps.
199
        """
200
        self.log.info("Starting Kytos - Kytos Controller")
201
        self.server = KytosServer((self.options.listen,
202
                                   int(self.options.port)),
203
                                  KytosServerProtocol,
204
                                  self,
205
                                  self.options.protocol_name)
206
207
        self.log.info("Starting TCP server: %s", self.server)
208
        self.server.serve_forever()
209
210
        def _stop_loop(_):
211
            loop = asyncio.get_event_loop()
212
            # print(_.result())
213
            threads = threading.enumerate()
214
            self.log.debug("%s threads before loop.stop: %s",
215
                           len(threads), threads)
216
            loop.stop()
217
218
        async def _run_api_server_thread(executor):
219
            log = logging.getLogger('kytos.core.controller.api_server_thread')
220
            log.debug('starting')
221
            # log.debug('creating tasks')
222
            loop = asyncio.get_event_loop()
223
            blocking_tasks = [
224
                loop.run_in_executor(executor, self.api_server.run)
225
            ]
226
            # log.debug('waiting for tasks')
227
            completed, pending = await asyncio.wait(blocking_tasks)
228
            # results = [t.result() for t in completed]
229
            # log.debug('results: {!r}'.format(results))
230
            log.debug('completed: %d, pending: %d',
231
                      len(completed), len(pending))
232
233
        task = self._loop.create_task(self.raw_event_handler())
234
        task = self._loop.create_task(self.msg_in_event_handler())
235
        task = self._loop.create_task(self.msg_out_event_handler())
236
        task = self._loop.create_task(self.app_event_handler())
237
        task = self._loop.create_task(_run_api_server_thread(self._pool))
238
        task.add_done_callback(_stop_loop)
239
240
        self.log.info("ThreadPool started: %s", self._pool)
241
242
        # ASYNC TODO: ensure all threads started correctly
243
        # This is critical, if any of them failed starting we should exit.
244
        # sys.exit(error_msg.format(thread, exception))
245
246
        self.log.info("Loading Kytos NApps...")
247
        self.napp_dir_listener.start()
248
        self.pre_install_napps(self.options.napps_pre_installed)
249
        self.load_napps()
250
251
        self.started_at = now()
252
253 1
    def _register_endpoints(self):
254
        """Register all rest endpoint served by kytos.
255
256
        -   Register APIServer endpoints
257
        -   Register WebUI endpoints
258
        -   Register ``/api/kytos/core/config`` endpoint
259
        """
260 1
        self.api_server.start_api()
261
        # Register controller endpoints as /api/kytos/core/...
262 1
        self.api_server.register_core_endpoint('config/',
263
                                               self.configuration_endpoint)
264
265 1
        self.api_server.register_core_endpoint(
266
            'reload/<username>/<napp_name>/',
267
            self.rest_reload_napp)
268 1
        self.api_server.register_core_endpoint('reload/all',
269
                                               self.rest_reload_all_napps)
270 1
        self.auth.register_core_auth_services()
271
272 1
    def register_rest_endpoint(self, url, function, methods):
273
        """Deprecate in favor of @rest decorator."""
274 1
        self.api_server.register_rest_endpoint(url, function, methods)
275
276 1
    def configuration_endpoint(self):
277
        """Return the configuration options used by Kytos.
278
279
        Returns:
280
            string: Json with current configurations used by kytos.
281
282
        """
283 1
        return json.dumps(self.options.__dict__)
284
285 1
    def restart(self, graceful=True):
286
        """Restart Kytos SDN Controller.
287
288
        Args:
289
            graceful(bool): Represents the way that Kytos will restart.
290
        """
291
        if self.started_at is not None:
292
            self.stop(graceful)
293
            self.__init__(self.options)
294
295
        self.start(restart=True)
296
297 1
    def stop(self, graceful=True):
298
        """Shutdown all services used by kytos.
299
300
        This method should:
301
            - stop all Websockets
302
            - stop the API Server
303
            - stop the Controller
304
        """
305
        if self.started_at:
306
            self.stop_controller(graceful)
307
308 1
    def stop_controller(self, graceful=True):
309
        """Stop the controller.
310
311
        This method should:
312
            - announce on the network that the controller will shutdown;
313
            - stop receiving incoming packages;
314
            - call the 'shutdown' method of each KytosNApp that is running;
315
            - finish reading the events on all buffers;
316
            - stop each running handler;
317
            - stop all running threads;
318
            - stop the KytosServer;
319
        """
320
        self.log.info("Stopping Kytos")
321
322
        self.buffers.send_stop_signal()
323
        self.api_server.stop_api_server()
324
        self.napp_dir_listener.stop()
325
326
        self.log.info("Stopping threadpool: %s", self._pool)
327
328
        threads = threading.enumerate()
329
        self.log.debug("%s threads before threadpool shutdown: %s",
330
                       len(threads), threads)
331
332
        self._pool.shutdown(wait=graceful)
333
334
        # self.server.socket.shutdown()
335
        # self.server.socket.close()
336
337
        # for thread in self._threads.values():
338
        #     self.log.info("Stopping thread: %s", thread.name)
339
        #     thread.join()
340
341
        # for thread in self._threads.values():
342
        #     while thread.is_alive():
343
        #         self.log.info("Thread is alive: %s", thread.name)
344
        #         pass
345
346
        self.started_at = None
347
        self.unload_napps()
348
        self.buffers = KytosBuffers()
349
350
        # ASYNC TODO: close connections
351
        # self.server.server_close()
352
353
        # Shutdown the TCP server and the main asyncio loop
354
        self.server.shutdown()
355
356 1
    def status(self):
357
        """Return status of Kytos Server.
358
359
        If the controller kytos is running this method will be returned
360
        "Running since 'Started_At'", otherwise "Stopped".
361
362
        Returns:
363
            string: String with kytos status.
364
365
        """
366
        if self.started_at:
367
            return "Running since %s" % self.started_at
368
        return "Stopped"
369
370 1
    def uptime(self):
371
        """Return the uptime of kytos server.
372
373
        This method should return:
374
            - 0 if Kytos Server is stopped.
375
            - (kytos.start_at - datetime.now) if Kytos Server is running.
376
377
        Returns:
378
           datetime.timedelta: The uptime interval.
379
380
        """
381
        return now() - self.started_at if self.started_at else 0
382
383 1
    def notify_listeners(self, event):
384
        """Send the event to the specified listeners.
385
386
        Loops over self.events_listeners matching (by regexp) the attribute
387
        name of the event with the keys of events_listeners. If a match occurs,
388
        then send the event to each registered listener.
389
390
        Args:
391
            event (~kytos.core.KytosEvent): An instance of a KytosEvent.
392
        """
393
        self.log.debug("looking for listeners for %s", event)
394
        for event_regex, listeners in dict(self.events_listeners).items():
395
            # self.log.debug("listeners found for %s: %r => %s", event,
396
            #                event_regex, [l.__qualname__ for l in listeners])
397
            # Do not match if the event has more characters
398
            # e.g. "shutdown" won't match "shutdown.kytos/of_core"
399
            if event_regex[-1] != '$' or event_regex[-2] == '\\':
400
                event_regex += '$'
401
            if re.match(event_regex, event.name):
402
                # self.log.debug('Calling listeners for %s', event)
403
                for listener in listeners:
404
                    listener(event)
405
406 1
    async def raw_event_handler(self):
407
        """Handle raw events.
408
409
        Listen to the raw_buffer and send all its events to the
410
        corresponding listeners.
411
        """
412
        self.log.info("Raw Event Handler started")
413
        while True:
414
            event = await self.buffers.raw.aget()
415
            self.notify_listeners(event)
416
            self.log.debug("Raw Event handler called")
417
418
            if event.name == "kytos/core.shutdown":
419
                self.log.debug("Raw Event handler stopped")
420
                break
421
422 1
    async def msg_in_event_handler(self):
423
        """Handle msg_in events.
424
425
        Listen to the msg_in buffer and send all its events to the
426
        corresponding listeners.
427
        """
428
        self.log.info("Message In Event Handler started")
429
        while True:
430
            event = await self.buffers.msg_in.aget()
431
            self.notify_listeners(event)
432
            self.log.debug("Message In Event handler called")
433
434
            if event.name == "kytos/core.shutdown":
435
                self.log.debug("Message In Event handler stopped")
436
                break
437
438 1
    async def msg_out_event_handler(self):
439
        """Handle msg_out events.
440
441
        Listen to the msg_out buffer and send all its events to the
442
        corresponding listeners.
443
        """
444
        self.log.info("Message Out Event Handler started")
445
        while True:
446
            triggered_event = await self.buffers.msg_out.aget()
447
448
            if triggered_event.name == "kytos/core.shutdown":
449
                self.log.debug("Message Out Event handler stopped")
450
                break
451
452
            message = triggered_event.content['message']
453
            destination = triggered_event.destination
454
            if (destination and
455
                    not destination.state == ConnectionState.FINISHED):
456
                packet = message.pack()
457
                destination.send(packet)
458
                self.log.debug('Connection %s: OUT OFP, '
459
                               'version: %s, type: %s, xid: %s - %s',
460
                               destination.id,
461
                               message.header.version,
462
                               message.header.message_type,
463
                               message.header.xid,
464
                               packet.hex())
465
                self.notify_listeners(triggered_event)
466
                self.log.debug("Message Out Event handler called")
467
            else:
468
                self.log.info("connection closed. Cannot send message")
469
470 1
    async def app_event_handler(self):
471
        """Handle app events.
472
473
        Listen to the app buffer and send all its events to the
474
        corresponding listeners.
475
        """
476
        self.log.info("App Event Handler started")
477
        while True:
478
            event = await self.buffers.app.aget()
479
            self.notify_listeners(event)
480
            self.log.debug("App Event handler called")
481
482
            if event.name == "kytos/core.shutdown":
483
                self.log.debug("App Event handler stopped")
484
                break
485
486 1
    def get_interface_by_id(self, interface_id):
487
        """Find a Interface  with interface_id.
488
489
        Args:
490
            interface_id(str): Interface Identifier.
491
492
        Returns:
493
            Interface: Instance of Interface with the id given.
494
495
        """
496
        if interface_id is None:
497
            return None
498
499
        switch_id = ":".join(interface_id.split(":")[:-1])
500
        interface_number = int(interface_id.split(":")[-1])
501
502
        switch = self.switches.get(switch_id)
503
504
        if not switch:
505
            return None
506
507
        return switch.interfaces.get(interface_number, None)
508
509 1
    def get_switch_by_dpid(self, dpid):
510
        """Return a specific switch by dpid.
511
512
        Args:
513
            dpid (|DPID|): dpid object used to identify a switch.
514
515
        Returns:
516
            :class:`~kytos.core.switch.Switch`: Switch with dpid specified.
517
518
        """
519 1
        return self.switches.get(dpid)
520
521 1
    def get_switch_or_create(self, dpid, connection):
522
        """Return switch or create it if necessary.
523
524
        Args:
525
            dpid (|DPID|): dpid object used to identify a switch.
526
            connection (:class:`~kytos.core.connection.Connection`):
527
                connection used by switch. If a switch has a connection that
528
                will be updated.
529
530
        Returns:
531
            :class:`~kytos.core.switch.Switch`: new or existent switch.
532
533
        """
534 1
        self.create_or_update_connection(connection)
535 1
        switch = self.get_switch_by_dpid(dpid)
536 1
        event_name = 'kytos/core.switch.'
537
538 1
        if switch is None:
539
            switch = Switch(dpid=dpid)
540
            self.add_new_switch(switch)
541
            event_name += 'new'
542
        else:
543 1
            event_name += 'reconnected'
544
545 1
        self.set_switch_options(dpid=dpid)
546 1
        event = KytosEvent(name=event_name, content={'switch': switch})
547
548 1
        old_connection = switch.connection
549 1
        switch.update_connection(connection)
550
551 1
        if old_connection is not connection:
552
            self.remove_connection(old_connection)
553
554 1
        self.buffers.app.put(event)
555
556 1
        return switch
557
558 1
    def set_switch_options(self, dpid):
559
        """Update the switch settings based on kytos.conf options.
560
561
        Args:
562
            dpid (str): dpid used to identify a switch.
563
564
        """
565 1
        switch = self.switches.get(dpid)
566 1
        if not switch:
567
            return
568
569 1
        vlan_pool = {}
570 1
        try:
571 1
            vlan_pool = json.loads(self.options.vlan_pool)
572 1
            if not vlan_pool:
573
                return
574
        except (TypeError, json.JSONDecodeError) as err:
575
            self.log.error("Invalid vlan_pool settings: %s", err)
576
577 1
        if vlan_pool.get(dpid):
578 1
            self.log.info("Loading vlan_pool configuration for dpid %s", dpid)
579 1
            for intf_num, port_list in vlan_pool[dpid].items():
580 1
                if not switch.interfaces.get((intf_num)):
581 1
                    vlan_ids = set()
582 1
                    for vlan_range in port_list:
583 1
                        (vlan_begin, vlan_end) = (vlan_range[0:2])
584 1
                        for vlan_id in range(vlan_begin, vlan_end):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable range does not seem to be defined.
Loading history...
585 1
                            vlan_ids.add(vlan_id)
586 1
                    intf_num = int(intf_num)
587 1
                    intf = Interface(name=intf_num, port_number=intf_num,
588
                                     switch=switch)
589 1
                    intf.set_available_tags(vlan_ids)
590 1
                    switch.update_interface(intf)
591
592 1
    def create_or_update_connection(self, connection):
593
        """Update a connection.
594
595
        Args:
596
            connection (:class:`~kytos.core.connection.Connection`):
597
                Instance of connection that will be updated.
598
        """
599 1
        self.connections[connection.id] = connection
600
601 1
    def get_connection_by_id(self, conn_id):
602
        """Return a existent connection by id.
603
604
        Args:
605
            id (int): id from a connection.
606
607
        Returns:
608
            :class:`~kytos.core.connection.Connection`: Instance of connection
609
                or None Type.
610
611
        """
612
        return self.connections.get(conn_id)
613
614 1
    def remove_connection(self, connection):
615
        """Close a existent connection and remove it.
616
617
        Args:
618
            connection (:class:`~kytos.core.connection.Connection`):
619
                Instance of connection that will be removed.
620
        """
621
        if connection is None:
622
            return False
623
624
        try:
625
            connection.close()
626
            del self.connections[connection.id]
627
        except KeyError:
628
            return False
629
        return True
630
631 1
    def remove_switch(self, switch):
632
        """Remove an existent switch.
633
634
        Args:
635
            switch (:class:`~kytos.core.switch.Switch`):
636
                Instance of switch that will be removed.
637
        """
638
        try:
639
            del self.switches[switch.dpid]
640
        except KeyError:
641
            return False
642
        return True
643
644 1
    def new_connection(self, event):
645
        """Handle a kytos/core.connection.new event.
646
647
        This method will read new connection event and store the connection
648
        (socket) into the connections attribute on the controller.
649
650
        It also clear all references to the connection since it is a new
651
        connection on the same ip:port.
652
653
        Args:
654
            event (~kytos.core.KytosEvent):
655
                The received event (``kytos/core.connection.new``) with the
656
                needed info.
657
        """
658
        self.log.info("Handling %s...", event)
659
660
        connection = event.source
661
        self.log.debug("Event source: %s", event.source)
662
663
        # Remove old connection (aka cleanup) if it exists
664
        if self.get_connection_by_id(connection.id):
665
            self.remove_connection(connection.id)
666
667
        # Update connections with the new connection
668
        self.create_or_update_connection(connection)
669
670 1
    def add_new_switch(self, switch):
671
        """Add a new switch on the controller.
672
673
        Args:
674
            switch (Switch): A Switch object
675
        """
676
        self.switches[switch.dpid] = switch
677
678 1
    def _import_napp(self, username, napp_name):
679
        """Import a NApp module.
680
681
        Raises:
682
            FileNotFoundError: if NApp's main.py is not found.
683
            ModuleNotFoundError: if any NApp requirement is not installed.
684
685
        """
686
        mod_name = '.'.join(['napps', username, napp_name, 'main'])
687
        path = os.path.join(self.options.napps, username, napp_name,
688
                            'main.py')
689
        napp_spec = spec_from_file_location(mod_name, path)
690
        napp_module = module_from_spec(napp_spec)
691
        sys.modules[napp_spec.name] = napp_module
692
        napp_spec.loader.exec_module(napp_module)
693
        return napp_module
694
695 1
    def load_napp(self, username, napp_name):
696
        """Load a single NApp.
697
698
        Args:
699
            username (str): NApp username (makes up NApp's path).
700
            napp_name (str): Name of the NApp to be loaded.
701
        """
702
        if (username, napp_name) in self.napps:
703
            message = 'NApp %s/%s was already loaded'
704
            self.log.warning(message, username, napp_name)
705
            return
706
707
        try:
708
            napp_module = self._import_napp(username, napp_name)
709
        except ModuleNotFoundError as err:
710
            self.log.error("Error loading NApp '%s/%s': %s",
711
                           username, napp_name, err)
712
            return
713
        except FileNotFoundError as err:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable FileNotFoundError does not seem to be defined.
Loading history...
714
            msg = "NApp module not found, assuming it's a meta napp: %s"
715
            self.log.warning(msg, err.filename)
716
            return
717
718
        napp = napp_module.Main(controller=self)
719
720
        self.napps[(username, napp_name)] = napp
721
722
        napp.start()
723
        self.api_server.register_napp_endpoints(napp)
724
725
        # pylint: disable=protected-access
726
        for event, listeners in napp._listeners.items():
727
            self.events_listeners.setdefault(event, []).extend(listeners)
728
        # pylint: enable=protected-access
729
730 1
    def pre_install_napps(self, napps, enable=True):
731
        """Pre install and enable NApps.
732
733
        Before installing, it'll check if it's installed yet.
734
735
        Args:
736
            napps ([str]): List of NApps to be pre-installed and enabled.
737
        """
738
        all_napps = self.napps_manager.get_installed_napps()
739
        installed = [str(napp) for napp in all_napps]
740
        napps_diff = [napp for napp in napps if napp not in installed]
741
        for napp in napps_diff:
742
            self.napps_manager.install(napp, enable=enable)
743
744 1
    def load_napps(self):
745
        """Load all NApps enabled on the NApps dir."""
746
        for napp in self.napps_manager.get_enabled_napps():
747
            try:
748
                self.log.info("Loading NApp %s", napp.id)
749
                self.load_napp(napp.username, napp.name)
750
            except FileNotFoundError as exception:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable FileNotFoundError does not seem to be defined.
Loading history...
751
                self.log.error("Could not load NApp %s: %s",
752
                               napp.id, exception)
753
754 1
    def unload_napp(self, username, napp_name):
755
        """Unload a specific NApp.
756
757
        Args:
758
            username (str): NApp username.
759
            napp_name (str): Name of the NApp to be unloaded.
760
        """
761 1
        napp = self.napps.pop((username, napp_name), None)
762
763 1
        if napp is None:
764
            self.log.warning('NApp %s/%s was not loaded', username, napp_name)
765
        else:
766 1
            self.log.info("Shutting down NApp %s/%s...", username, napp_name)
767 1
            napp_id = NApp(username, napp_name).id
768 1
            event = KytosEvent(name='kytos/core.shutdown.' + napp_id)
769 1
            napp_shutdown_fn = self.events_listeners[event.name][0]
770
            # Call listener before removing it from events_listeners
771 1
            napp_shutdown_fn(event)
772
773
            # Remove rest endpoints from that napp
774 1
            self.api_server.remove_napp_endpoints(napp)
775
776
            # Removing listeners from that napp
777
            # pylint: disable=protected-access
778 1
            for event_type, napp_listeners in napp._listeners.items():
779
                event_listeners = self.events_listeners[event_type]
780
                for listener in napp_listeners:
781
                    event_listeners.remove(listener)
782
                if not event_listeners:
783
                    del self.events_listeners[event_type]
784
            # pylint: enable=protected-access
785
786 1
    def unload_napps(self):
787
        """Unload all loaded NApps that are not core NApps."""
788
        # list() is used here to avoid the error:
789
        # 'RuntimeError: dictionary changed size during iteration'
790
        # This is caused by looping over an dictionary while removing
791
        # items from it.
792
        for (username, napp_name) in list(self.napps.keys()):  # noqa
793
            self.unload_napp(username, napp_name)
794
795 1
    def reload_napp_module(self, username, napp_name, napp_file):
796
        """Reload a NApp Module."""
797
        mod_name = '.'.join(['napps', username, napp_name, napp_file])
798
        try:
799
            napp_module = import_module(mod_name)
800
        except ModuleNotFoundError as err:
801
            self.log.error("Module '%s' not found", mod_name)
802
            raise
803
        try:
804
            napp_module = reload_module(napp_module)
805
        except ImportError as err:
806
            self.log.error("Error reloading NApp '%s/%s': %s",
807
                           username, napp_name, err)
808
            raise
809
810 1
    def reload_napp(self, username, napp_name):
811
        """Reload a NApp."""
812
        self.unload_napp(username, napp_name)
813
        try:
814
            self.reload_napp_module(username, napp_name, 'settings')
815
            self.reload_napp_module(username, napp_name, 'main')
816
        except (ModuleNotFoundError, ImportError):
817
            return 400
818
        self.log.info("NApp '%s/%s' successfully reloaded",
819
                      username, napp_name)
820
        self.load_napp(username, napp_name)
821
        return 200
822
823 1
    def rest_reload_napp(self, username, napp_name):
824
        """Request reload a NApp."""
825
        res = self.reload_napp(username, napp_name)
826
        return 'reloaded', res
827
828 1
    def rest_reload_all_napps(self):
829
        """Request reload all NApps."""
830
        for napp in self.napps:
831
            self.reload_napp(*napp)
832
        return 'reloaded', 200
833