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

kytos.core.napps.base.KytosNApp.napp_id()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1.2963

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 1
dl 0
loc 4
ccs 1
cts 3
cp 0.3333
crap 1.2963
rs 10
c 0
b 0
f 0
1
"""Kytos Napps Module."""
2 1
import json
3 1
import os
4 1
import re
5 1
import sys
6 1
import tarfile
7 1
import urllib
8 1
from abc import ABCMeta, abstractmethod
9 1
from pathlib import Path
10 1
from random import randint
11 1
from threading import Event, Thread
12
13 1
from kytos.core.logs import NAppLog
14
15 1
__all__ = ('KytosNApp',)
16
17 1
LOG = NAppLog()
18
19
20 1
class NApp:
21
    """Class to represent a NApp."""
22
23 1
    # pylint: disable=too-many-arguments
24
    def __init__(self, username=None, name=None, version=None,
25 1
                 repository=None, meta=False):
26 1
        self.username = username
27 1
        self.name = name
28 1
        self.version = version if version else 'latest'
29 1
        self.repository = repository
30 1
        self.meta = meta
31 1
        self.description = None
32 1
        self.tags = []
33
        self.enabled = False
34 1
        self.napp_dependencies = []
35 1
36
    def __str__(self):
37 1
        return "{}/{}".format(self.username, self.name)
38
39
    def __repr__(self):
40 1
        return f"NApp({self.username}/{self.name})"
41
42
    def __hash__(self):
43 1
        return hash(self.id)
44
45
    def __eq__(self, other):
46
        """Compare username/name strings."""
47 1
        return isinstance(other, self.__class__) and self.id == other.id
48
49
    @property
50 1
    def id(self):  # pylint: disable=invalid-name
51
        """username/name string."""
52 1
        return str(self)
53
54
    @property
55
    def uri(self):
56
        """Return a unique identifier of this NApp."""
57
        version = self.version if self.version else 'latest'
58
59
        if not self._has_valid_repository():
60
            return ""
61
62
        # Use the next line after Diraol fix redirect using ":" for version
63
        # return "{}/{}:{}".format(self.repository, self.id, version)
64
65 1
        return "{}/{}-{}".format(self.repository, self.id, version)
66
67
    @property
68
    def package_url(self):
69
        """Return a fully qualified URL for a NApp package."""
70
        if not self.uri:
71
            return ""
72 1
        return "{}.napp".format(self.uri)
73
74
    @classmethod
75
    def create_from_uri(cls, uri):
76
        """Return a new NApp instance from an unique identifier."""
77
        regex = r'^(((https?://|file://)(.+))/)?(.+?)/(.+?)/?(:(.+))?$'
78
        match = re.match(regex, uri)
79
80 1
        if not match:
81
            return None
82
83
        return cls(username=match.groups()[4],
84
                   name=match.groups()[5],
85
                   version=match.groups()[7],
86
                   repository=match.groups()[1])
87
88
    @classmethod
89
    def create_from_json(cls, filename):
90 1
        """Return a new NApp instance from a metadata file."""
91
        with open(filename, encoding='utf-8') as data_file:
92
            data = json.loads(data_file.read())
93
94
        return cls.create_from_dict(data)
95
96
    @classmethod
97
    def create_from_dict(cls, data):
98
        """Return a new NApp instance from metadata."""
99
        napp = cls()
100
101
        for attribute, value in data.items():
102
            setattr(napp, attribute, value)
103
104 1
        return napp
105
106
    def as_json(self):
107
        """Dump all NApp attributes on a json format."""
108
        return json.dumps(self.__dict__)
109
110
    def match(self, pattern):
111
        """Whether a pattern is present on NApp id, description and tags."""
112
        try:
113
            pattern = '.*{}.*'.format(pattern)
114 1
            pattern = re.compile(pattern, re.IGNORECASE)
115
            strings = [self.id, self.description] + self.tags
116
            return any(pattern.match(string) for string in strings)
117
        except TypeError:
118
            return False
119
120
    def download(self):
121
        """Download NApp package from his repository.
122
123
        Raises:
124
            urllib.error.HTTPError: If download is not successful.
125
126
        Returns:
127
            str: Downloaded temp filename.
128
129
        """
130
        if not self.package_url:
131
            return None
132
133 1
        package_filename = urllib.request.urlretrieve(self.package_url)[0]
134
        extracted = self._extract(package_filename)
135
        Path(package_filename).unlink()
136
        self._update_repo_file(extracted)
137 1
        return extracted
138
139
    @staticmethod
140
    def _extract(filename):
141
        """Extract NApp package to a temporary folder.
142
143
        Return:
144
            pathlib.Path: Temp dir with package contents.
145
        """
146
        random_string = '{:0d}'.format(randint(0, 10**6))
147
        tmp = '/tmp/kytos-napp-' + Path(filename).stem + '-' + random_string
148
        os.mkdir(tmp)
149
        with tarfile.open(filename, 'r:xz') as tar:
150
            tar.extractall(tmp)
151 1
        return Path(tmp)
152
153
    def _has_valid_repository(self):
154
        """Whether this NApp has a valid repository or not."""
155 1
        return all([self.username, self.name, self.repository])
156
157
    def _update_repo_file(self, destination=None):
158
        """Create or update the file '.repo' inside NApp package."""
159
        with open("{}/.repo".format(destination), 'w') as repo_file:
160
            repo_file.write(self.repository + '\n')
161 1
162
163
class KytosNApp(Thread, metaclass=ABCMeta):
164 1
    """Base class for any KytosNApp to be developed."""
165
166
    def __init__(self, controller, **kwargs):
167
        """Contructor of KytosNapps.
168
169
        Go through all of the instance methods and selects those that have
170
        the events attribute, then creates a dict containing the event_name
171
        and the list of methods that are responsible for handling such event.
172
173
        At the end, the setup method is called as a complement of the init
174
        process.
175
        """
176
        Thread.__init__(self, daemon=False)
177
        self.controller = controller
178
        self.username = None  # loaded from json
179
        self.name = None      # loaded from json
180
        self.meta = False     # loaded from json
181
        self._load_json()
182
183
        # Force a listener with a private method.
184
        self._listeners = {
185
            'kytos/core.shutdown': [self._shutdown_handler],
186
            'kytos/core.shutdown.' + self.napp_id: [self._shutdown_handler]}
187
188
        self.__event = Event()
189
        #: int: Seconds to sleep before next call to :meth:`execute`. If
190
        #: negative, run :meth:`execute` only once.
191
        self.__interval = -1
192
        self.setup()
193
194
        #: Add non-private methods that listen to events.
195
        handler_methods = [getattr(self, method_name) for method_name in
196
                           dir(self) if method_name[0] != '_' and
197
                           callable(getattr(self, method_name)) and
198
                           hasattr(getattr(self, method_name), 'events')]
199
200
        # Building the listeners dictionary
201
        for method in handler_methods:
202
            for event_name in method.events:
203 1
                if event_name not in self._listeners:
204
                    self._listeners[event_name] = []
205
                self._listeners[event_name].append(method)
206
207 1
    @property
208
    def napp_id(self):
209
        """username/name string."""
210
        return "{}/{}".format(self.username, self.name)
211
212
    def listeners(self):
213
        """Return all listeners registered."""
214
        return list(self._listeners.keys())
215
216
    def _load_json(self):
217 1
        """Update object attributes based on kytos.json."""
218
        current_file = sys.modules[self.__class__.__module__].__file__
219
        json_path = os.path.join(os.path.dirname(current_file), 'kytos.json')
220
        with open(json_path, encoding='utf-8') as data_file:
221
            data = json.loads(data_file.read())
222
223
        for attribute, value in data.items():
224
            setattr(self, attribute, value)
225
226
    def execute_as_loop(self, interval):
227
        """Run :meth:`execute` within a loop. Call this method during setup.
228
229
        By calling this method, the application does not need to worry about
230 1
        loop details such as sleeping and stopping the loop when
231
        :meth:`shutdown` is called. Just call this method during :meth:`setup`
232
        and implement :meth:`execute` as a single execution.
233
234
        Args:
235
            interval (int): Seconds between each call to :meth:`execute`.
236
        """
237
        self.__interval = interval
238
239
    def run(self):
240
        """Call the execute method, looping as needed.
241
242 1
        It should not be overriden.
243
        """
244
        LOG.info("Running NApp: %s", self)
245
        self.execute()
246
        while self.__interval > 0 and not self.__event.is_set():
247
            self.__event.wait(self.__interval)
248
            self.execute()
249
250
    # all listeners receive event
251
    def _shutdown_handler(self, event):  # pylint: disable=unused-argument
252
        """Listen shutdown event from kytos.
253
254
        This method listens the kytos/core.shutdown event and call the shutdown
255 1
        method from napp subclass implementation.
256
257
        Paramters
258
            event (:class:`KytosEvent`): event to be listened.
259
        """
260
        if not self.__event.is_set():
261
            self.__event.set()
262
            self.shutdown()
263
264 1
    @abstractmethod
265
    def setup(self):
266
        """Replace the 'init' method for the KytosApp subclass.
267
268
        The setup method is automatically called on the NApp __init__().
269
        Users aren't supposed to call this method directly.
270
        """
271
        pass
272
273 1
    @abstractmethod
274
    def execute(self):
275
        """Execute in a loop until 'kytos/core.shutdown' is received.
276
277
        The execute method is called by KytosNApp class.
278
        Users shouldn't call this method directly.
279
        """
280
        pass
281
282
    @abstractmethod
283
    def shutdown(self):
284
        """Run before the app is unloaded and the controller, stopped.
285
286
        The user implementation of this method should do the necessary routine
287
        for the user App and also it is a good moment to break the loop of the
288
        execute method if it is in a loop.
289
290
        This methods is not going to be called directly, it is going to be
291
        called by the _shutdown_handler method when a KytosShutdownEvent is
292
        sent.
293
        """
294
        pass
295