Completed
Push — master ( be1922...eb90f7 )
by Beraldo
12s
created

kytos.core.controller.Controller.load_napp()   B

Complexity

Conditions 5

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 25.5993

Importance

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