1
|
|
|
"""Manage Network Application files.""" |
2
|
1 |
|
import json |
3
|
1 |
|
import logging |
4
|
1 |
|
import os |
5
|
1 |
|
import pathlib |
6
|
1 |
|
import re |
7
|
1 |
|
import sys |
8
|
1 |
|
import tarfile |
9
|
1 |
|
import urllib |
10
|
1 |
|
from http import HTTPStatus |
11
|
|
|
|
12
|
|
|
# Disable pylint import checks that conflict with isort |
13
|
|
|
# pylint: disable=ungrouped-imports,wrong-import-order |
14
|
1 |
|
import pathspec |
15
|
1 |
|
from jinja2 import Environment, FileSystemLoader |
16
|
1 |
|
from ruamel.yaml import YAML |
17
|
|
|
|
18
|
1 |
|
from kytos.utils.client import NAppsClient |
19
|
1 |
|
from kytos.utils.config import KytosConfig |
20
|
1 |
|
from kytos.utils.exceptions import KytosException |
21
|
1 |
|
from kytos.utils.openapi import OpenAPI |
22
|
1 |
|
from kytos.utils.settings import SKEL_PATH |
23
|
|
|
|
24
|
1 |
|
LOG = logging.getLogger(__name__) |
25
|
|
|
|
26
|
|
|
|
27
|
|
|
# pylint: disable=too-many-instance-attributes,too-many-public-methods |
28
|
1 |
|
class NAppsManager: |
29
|
|
|
"""Deal with NApps at filesystem level and ask Kytos to (un)load NApps.""" |
30
|
|
|
|
31
|
1 |
|
_NAPP_ENABLE = "api/kytos/core/napps/{}/{}/enable" |
32
|
1 |
|
_NAPP_DISABLE = "api/kytos/core/napps/{}/{}/disable" |
33
|
1 |
|
_NAPP_INSTALL = "api/kytos/core/napps/{}/{}/install" |
34
|
1 |
|
_NAPP_UNINSTALL = "api/kytos/core/napps/{}/{}/uninstall" |
35
|
1 |
|
_NAPPS_INSTALLED = "api/kytos/core/napps_installed" |
36
|
1 |
|
_NAPPS_ENABLED = "api/kytos/core/napps_enabled" |
37
|
1 |
|
_NAPP_METADATA = "api/kytos/core/napps/{}/{}/metadata/{}" |
38
|
|
|
|
39
|
1 |
|
def __init__(self): |
40
|
|
|
"""Instance a new NAppsManager. |
41
|
|
|
|
42
|
|
|
This method do not need parameters. |
43
|
|
|
""" |
44
|
1 |
|
self._config = KytosConfig().config |
45
|
1 |
|
self._kytos_api = self._config.get('kytos', 'api') |
46
|
|
|
|
47
|
1 |
|
self.user = None |
48
|
1 |
|
self.napp = None |
49
|
1 |
|
self.version = None |
50
|
|
|
|
51
|
|
|
# Automatically get from kytosd API when needed |
52
|
1 |
|
self.__local_enabled = None |
53
|
1 |
|
self.__local_installed = None |
54
|
|
|
|
55
|
1 |
|
@property |
56
|
|
|
def _enabled(self): |
57
|
1 |
|
if self.__local_enabled is None: |
58
|
1 |
|
self.__require_kytos_config() |
59
|
1 |
|
return self.__local_enabled |
60
|
|
|
|
61
|
1 |
|
@property |
62
|
|
|
def _installed(self): |
63
|
1 |
|
if self.__local_installed is None: |
64
|
1 |
|
self.__require_kytos_config() |
65
|
1 |
|
return self.__local_installed |
66
|
|
|
|
67
|
1 |
|
def __require_kytos_config(self): |
68
|
|
|
"""Set path locations from kytosd API. |
69
|
|
|
|
70
|
|
|
It should not be called directly, but from properties that require a |
71
|
|
|
running kytosd instance. |
72
|
|
|
""" |
73
|
1 |
|
if self.__local_enabled is None: |
74
|
1 |
|
uri = self._kytos_api + 'api/kytos/core/config/' |
75
|
1 |
|
try: |
76
|
1 |
|
ops = json.loads(urllib.request.urlopen(uri).read()) |
77
|
1 |
|
except urllib.error.URLError as err: |
78
|
1 |
|
msg = f'Error connecting to Kytos daemon: {uri} {err.reason}' |
79
|
1 |
|
print(msg) |
80
|
1 |
|
sys.exit(1) |
81
|
1 |
|
self.__local_enabled = pathlib.Path(ops.get('napps')) |
82
|
1 |
|
self.__local_installed = pathlib.Path(ops.get('installed_napps')) |
83
|
|
|
|
84
|
1 |
|
def set_napp(self, user, napp, version=None): |
85
|
|
|
"""Set info about NApp. |
86
|
|
|
|
87
|
|
|
Args: |
88
|
|
|
user (str): NApps Server username. |
89
|
|
|
napp (str): NApp name. |
90
|
|
|
version (str): NApp version. |
91
|
|
|
|
92
|
|
|
""" |
93
|
1 |
|
self.user = user |
94
|
1 |
|
self.napp = napp |
95
|
1 |
|
self.version = version or 'latest' |
96
|
|
|
|
97
|
1 |
|
@property |
98
|
|
|
def napp_id(self): |
99
|
|
|
"""Return a Identifier of NApp.""" |
100
|
1 |
|
return '/'.join((self.user, self.napp)) |
101
|
|
|
|
102
|
1 |
|
@staticmethod |
103
|
|
|
def _get_napps(napps_dir): |
104
|
|
|
"""List of (username, napp_name) found in ``napps_dir``. |
105
|
|
|
|
106
|
|
|
Ex: [('kytos', 'of_core'), ('kytos', 'of_lldp')] |
107
|
|
|
""" |
108
|
1 |
|
jsons = napps_dir.glob('*/*/kytos.json') |
109
|
1 |
|
return sorted(j.parts[-3:-1] for j in jsons) |
110
|
|
|
|
111
|
1 |
|
def get_enabled_local(self): |
112
|
|
|
"""Sorted list of (username, napp_name) of enabled napps.""" |
113
|
1 |
|
return self._get_napps(self._enabled) |
114
|
|
|
|
115
|
1 |
|
def get_installed_local(self): |
116
|
|
|
"""Sorted list of (username, napp_name) of installed napps.""" |
117
|
1 |
|
return self._get_napps(self._installed) |
118
|
|
|
|
119
|
1 |
|
def get_enabled(self): |
120
|
|
|
"""Sorted list of (username, napp_name) of enabled napps.""" |
121
|
1 |
|
uri = self._kytos_api + self._NAPPS_ENABLED |
122
|
|
|
|
123
|
1 |
|
try: |
124
|
1 |
|
response = urllib.request.urlopen(uri) |
125
|
1 |
|
if response.getcode() != 200: |
126
|
1 |
|
msg = "Error calling Kytos to check enabled NApps." |
127
|
1 |
|
raise KytosException(msg) |
128
|
|
|
|
129
|
1 |
|
content = json.loads(response.read()) |
130
|
1 |
|
return sorted((c[0], c[1]) for c in content['napps']) |
131
|
1 |
|
except urllib.error.URLError as exception: |
132
|
1 |
|
LOG.error("Error checking enabled NApps. Is Kytos running?") |
133
|
1 |
|
raise KytosException(exception) |
134
|
|
|
|
135
|
1 |
|
def get_installed(self): |
136
|
|
|
"""Sorted list of (username, napp_name) of installed napps.""" |
137
|
1 |
|
uri = self._kytos_api + self._NAPPS_INSTALLED |
138
|
|
|
|
139
|
1 |
|
try: |
140
|
1 |
|
response = urllib.request.urlopen(uri) |
141
|
1 |
|
if response.getcode() != 200: |
142
|
1 |
|
msg = "Error calling Kytos to check installed NApps." |
143
|
1 |
|
raise KytosException(msg) |
144
|
|
|
|
145
|
1 |
|
content = json.loads(response.read()) |
146
|
1 |
|
return sorted((c[0], c[1]) for c in content['napps']) |
147
|
1 |
|
except urllib.error.URLError as exception: |
148
|
1 |
|
LOG.error("Error checking installed NApps. Is Kytos running?") |
149
|
1 |
|
raise KytosException(exception) |
150
|
|
|
|
151
|
1 |
|
def is_installed(self): |
152
|
|
|
"""Whether a NApp is installed.""" |
153
|
1 |
|
return (self.user, self.napp) in self.get_installed() |
154
|
|
|
|
155
|
1 |
|
def get_disabled(self): |
156
|
|
|
"""Sorted list of (username, napp_name) of disabled napps. |
157
|
|
|
|
158
|
|
|
The difference of installed and enabled. |
159
|
|
|
""" |
160
|
1 |
|
installed = set(self.get_installed()) |
161
|
1 |
|
enabled = set(self.get_enabled()) |
162
|
1 |
|
return sorted(installed - enabled) |
163
|
|
|
|
164
|
1 |
|
def dependencies(self, user=None, napp=None): |
165
|
|
|
"""Get napp_dependencies from install NApp. |
166
|
|
|
|
167
|
|
|
Args: |
168
|
|
|
user(string) A Username. |
169
|
|
|
napp(string): A NApp name. |
170
|
|
|
Returns: |
171
|
|
|
napps(list): List with tuples with Username and NApp name. |
172
|
|
|
e.g. [('kytos'/'of_core'), ('kytos/of_l2ls')] |
173
|
|
|
|
174
|
|
|
""" |
175
|
1 |
|
napps = self._get_napp_key('napp_dependencies', user, napp) |
176
|
1 |
|
return [tuple(napp.split('/')) for napp in napps] |
177
|
|
|
|
178
|
1 |
|
def get_description(self, user=None, napp=None): |
179
|
|
|
"""Return the description from kytos.json.""" |
180
|
1 |
|
return self._get_napp_key('description', user, napp) |
181
|
|
|
|
182
|
1 |
|
def get_version(self, user=None, napp=None): |
183
|
|
|
"""Return the version from kytos.json.""" |
184
|
1 |
|
return self._get_napp_key('version', user, napp) or 'latest' |
185
|
|
|
|
186
|
1 |
|
def _get_napp_key(self, key, user=None, napp=None): |
187
|
|
|
"""Return a value from kytos.json. |
188
|
|
|
|
189
|
|
|
Args: |
190
|
|
|
user (string): A Username. |
191
|
|
|
napp (string): A NApp name |
192
|
|
|
key (string): Key used to get the value within kytos.json. |
193
|
|
|
|
194
|
|
|
Returns: |
195
|
|
|
meta (object): Value stored in kytos.json. |
196
|
|
|
|
197
|
|
|
""" |
198
|
1 |
|
if user is None: |
199
|
1 |
|
user = self.user |
200
|
1 |
|
if napp is None: |
201
|
1 |
|
napp = self.napp |
202
|
|
|
|
203
|
1 |
|
uri = self._kytos_api + self._NAPP_METADATA |
204
|
1 |
|
uri = uri.format(user, napp, key) |
205
|
|
|
|
206
|
1 |
|
meta = json.loads(urllib.request.urlopen(uri).read()) |
207
|
1 |
|
return meta[key] |
208
|
|
|
|
209
|
1 |
|
def disable(self): |
210
|
|
|
"""Disable a NApp if it is enabled.""" |
211
|
1 |
|
uri = self._kytos_api + self._NAPP_DISABLE |
212
|
1 |
|
uri = uri.format(self.user, self.napp) |
213
|
|
|
|
214
|
1 |
|
try: |
215
|
1 |
|
json.loads(urllib.request.urlopen(uri).read()) |
216
|
1 |
|
except urllib.error.HTTPError as exception: |
217
|
1 |
|
if exception.code == HTTPStatus.BAD_REQUEST.value: |
218
|
1 |
|
LOG.error("NApp is not installed. Check the NApp list.") |
219
|
|
|
else: |
220
|
1 |
|
LOG.error("Error disabling the NApp") |
221
|
|
|
|
222
|
1 |
|
def enable(self): |
223
|
|
|
"""Enable a NApp if not already enabled.""" |
224
|
1 |
|
uri = self._kytos_api + self._NAPP_ENABLE |
225
|
1 |
|
uri = uri.format(self.user, self.napp) |
226
|
|
|
|
227
|
1 |
|
try: |
228
|
1 |
|
json.loads(urllib.request.urlopen(uri).read()) |
229
|
1 |
|
except urllib.error.HTTPError as exception: |
230
|
1 |
|
if exception.code == HTTPStatus.BAD_REQUEST.value: |
231
|
1 |
|
LOG.error("NApp is not installed. Check the NApp list.") |
232
|
|
|
else: |
233
|
1 |
|
LOG.error("Error enabling the NApp") |
234
|
|
|
|
235
|
1 |
|
def enabled_dir(self): |
236
|
|
|
"""Return the enabled dir from current napp.""" |
237
|
1 |
|
return self._enabled / self.user / self.napp |
238
|
|
|
|
239
|
1 |
|
def installed_dir(self): |
240
|
|
|
"""Return the installed dir from current napp.""" |
241
|
1 |
|
return self._installed / self.user / self.napp |
242
|
|
|
|
243
|
1 |
|
def is_enabled(self): |
244
|
|
|
"""Whether a NApp is enabled.""" |
245
|
1 |
|
return (self.user, self.napp) in self.get_enabled() |
246
|
|
|
|
247
|
1 |
|
def remote_uninstall(self): |
248
|
|
|
"""Delete code inside NApp directory, if existent.""" |
249
|
1 |
|
uri = self._kytos_api + self._NAPP_UNINSTALL |
250
|
1 |
|
uri = uri.format(self.user, self.napp) |
251
|
|
|
|
252
|
1 |
|
try: |
253
|
1 |
|
json.loads(urllib.request.urlopen(uri).read()) |
254
|
1 |
|
except urllib.error.HTTPError as exception: |
255
|
1 |
|
if exception.code == HTTPStatus.BAD_REQUEST.value: |
256
|
1 |
|
LOG.error("Check if the NApp is installed.") |
257
|
|
|
else: |
258
|
1 |
|
LOG.error("Error uninstalling the NApp") |
259
|
|
|
|
260
|
1 |
|
@staticmethod |
261
|
|
|
def valid_name(username): |
262
|
|
|
"""Check the validity of the given 'name'. |
263
|
|
|
|
264
|
|
|
The following checks are done: |
265
|
|
|
- name starts with a letter |
266
|
|
|
- name contains only letters, numbers or underscores |
267
|
|
|
""" |
268
|
1 |
|
return username and re.match(r'[a-zA-Z][a-zA-Z0-9_]{2,}$', username) |
269
|
|
|
|
270
|
1 |
|
@staticmethod |
271
|
|
|
def render_template(templates_path, template_filename, context): |
272
|
|
|
"""Render Jinja2 template for a NApp structure.""" |
273
|
1 |
|
template_env = Environment( |
274
|
|
|
autoescape=False, trim_blocks=False, |
275
|
|
|
loader=FileSystemLoader(str(templates_path))) |
276
|
1 |
|
return template_env.get_template(str(template_filename)) \ |
277
|
|
|
.render(context) |
278
|
|
|
|
279
|
1 |
|
@staticmethod |
280
|
|
|
def search(pattern): |
281
|
|
|
"""Search all server NApps matching pattern. |
282
|
|
|
|
283
|
|
|
Args: |
284
|
|
|
pattern (str): Python regular expression. |
285
|
|
|
|
286
|
|
|
""" |
287
|
1 |
|
def match(napp): |
288
|
|
|
"""Whether a NApp metadata matches the pattern.""" |
289
|
|
|
# WARNING: This will change for future versions, when 'author' will |
290
|
|
|
# be removed. |
291
|
1 |
|
username = napp.get('username', napp.get('author')) |
292
|
|
|
|
293
|
1 |
|
strings = ['{}/{}'.format(username, napp.get('name')), |
294
|
|
|
napp.get('description')] + napp.get('tags') |
295
|
1 |
|
return any(pattern.match(string) for string in strings) |
296
|
|
|
|
297
|
1 |
|
napps = NAppsClient().get_napps() |
298
|
1 |
|
return [napp for napp in napps if match(napp)] |
299
|
|
|
|
300
|
1 |
|
def remote_install(self): |
301
|
|
|
"""Ask kytos server to install NApp.""" |
302
|
1 |
|
uri = self._kytos_api + self._NAPP_INSTALL |
303
|
1 |
|
uri = uri.format(self.user, self.napp) |
304
|
|
|
|
305
|
1 |
|
json.loads(urllib.request.urlopen(uri).read()) |
306
|
|
|
|
307
|
1 |
|
@classmethod |
308
|
|
|
# pylint: disable=too-many-statements |
309
|
1 |
|
def create_napp(cls, meta_package=False): |
310
|
|
|
"""Bootstrap a basic NApp structure for you to develop your NApp. |
311
|
|
|
|
312
|
|
|
This will create, on the current folder, a clean structure of a NAPP, |
313
|
|
|
filling some contents on this structure. |
314
|
|
|
""" |
315
|
1 |
|
templates_path = SKEL_PATH / 'napp-structure/username/napp' |
316
|
|
|
|
317
|
1 |
|
ui_templates_path = os.path.join(templates_path, 'ui') |
318
|
|
|
|
319
|
1 |
|
username = None |
320
|
1 |
|
napp_name = None |
321
|
1 |
|
print('--------------------------------------------------------------') |
322
|
1 |
|
print('Welcome to the bootstrap process of your NApp.') |
323
|
1 |
|
print('--------------------------------------------------------------') |
324
|
1 |
|
print('In order to answer both the username and the NApp name,') |
325
|
1 |
|
print('You must follow these naming rules:') |
326
|
1 |
|
print(' - name starts with a letter') |
327
|
1 |
|
print(' - name contains only letters, numbers or underscores') |
328
|
1 |
|
print(' - at least three characters') |
329
|
1 |
|
print('--------------------------------------------------------------') |
330
|
1 |
|
print('') |
331
|
1 |
|
try: |
332
|
1 |
|
while not cls.valid_name(username): |
333
|
1 |
|
username = input('Please, insert your NApps Server username: ') |
334
|
|
|
|
335
|
1 |
|
while not cls.valid_name(napp_name): |
336
|
1 |
|
napp_name = input('Please, insert your NApp name: ') |
337
|
|
|
|
338
|
1 |
|
description = input('Please, insert a brief description for your ' |
339
|
|
|
'NApp [optional]: ') |
340
|
|
|
except KeyboardInterrupt: |
341
|
|
|
print("User cancelled NApp creation.") |
342
|
|
|
sys.exit(0) |
343
|
1 |
|
if not description: |
344
|
|
|
# pylint: disable=fixme |
345
|
1 |
|
description = '# TODO: <<<< Insert your NApp description here >>>>' |
346
|
|
|
# pylint: enable=fixme |
347
|
|
|
|
348
|
1 |
|
context = {'username': username, 'napp': napp_name, |
349
|
|
|
'description': description} |
350
|
|
|
|
351
|
|
|
#: Creating the directory structure (username/napp_name) |
352
|
1 |
|
os.makedirs(username, exist_ok=True) |
353
|
|
|
|
354
|
|
|
#: Creating ``__init__.py`` files |
355
|
1 |
|
with open(os.path.join(username, '__init__.py'), 'w') as init_file: |
356
|
1 |
|
init_file.write(f'"""NApps for the user {username}.""""') |
357
|
|
|
|
358
|
1 |
|
os.makedirs(os.path.join(username, napp_name)) |
359
|
|
|
|
360
|
|
|
#: Creating the other files based on the templates |
361
|
1 |
|
templates = os.listdir(templates_path) |
362
|
1 |
|
templates.remove('ui') |
363
|
1 |
|
templates.remove('openapi.yml.template') |
364
|
|
|
|
365
|
1 |
|
if meta_package: |
366
|
|
|
templates.remove('main.py.template') |
367
|
|
|
templates.remove('settings.py.template') |
368
|
|
|
|
369
|
1 |
|
for tmp in templates: |
370
|
1 |
|
fname = os.path.join(username, napp_name, |
371
|
|
|
tmp.rsplit('.template')[0]) |
372
|
1 |
|
with open(fname, 'w') as file: |
373
|
1 |
|
content = cls.render_template(templates_path, tmp, context) |
374
|
1 |
|
file.write(content) |
375
|
|
|
|
376
|
1 |
|
if not meta_package: |
377
|
1 |
|
NAppsManager.create_ui_structure(username, napp_name, |
378
|
|
|
ui_templates_path, context) |
379
|
|
|
|
380
|
1 |
|
print('\nCongratulations! Your NApp has been bootstrapped!\nNow you' |
381
|
|
|
f' can go to the directory "{username}/{napp_name}" and begin' |
382
|
|
|
' to code your NApp.') |
383
|
1 |
|
print('Have fun!') |
384
|
|
|
|
385
|
1 |
|
@classmethod |
386
|
|
|
def create_ui_structure(cls, username, napp_name, ui_templates_path, |
387
|
|
|
context): |
388
|
|
|
"""Create the ui directory structure.""" |
389
|
1 |
|
for section in ['k-info-panel', 'k-toolbar', 'k-action-menu']: |
390
|
1 |
|
os.makedirs(os.path.join(username, napp_name, 'ui', section)) |
391
|
|
|
|
392
|
1 |
|
templates = os.listdir(ui_templates_path) |
393
|
|
|
|
394
|
1 |
|
for tmp in templates: |
395
|
1 |
|
fname = os.path.join(username, napp_name, 'ui', |
396
|
|
|
tmp.rsplit('.template')[0]) |
397
|
|
|
|
398
|
1 |
|
with open(fname, 'w') as file: |
399
|
1 |
|
content = cls.render_template(ui_templates_path, tmp, |
400
|
|
|
context) |
401
|
1 |
|
file.write(content) |
402
|
|
|
|
403
|
1 |
|
@staticmethod |
404
|
|
|
def _check_module(folder): |
405
|
|
|
"""Create module folder with empty __init__.py if it doesn't exist. |
406
|
|
|
|
407
|
|
|
Args: |
408
|
|
|
folder (pathlib.pathlib.Path): Module path. |
409
|
|
|
|
410
|
|
|
""" |
411
|
1 |
|
if not folder.exists(): |
412
|
1 |
|
folder.mkdir(parents=True, exist_ok=True, mode=0o755) |
413
|
1 |
|
(folder / '__init__.py').touch() |
414
|
|
|
|
415
|
1 |
|
@staticmethod |
416
|
|
|
def build_napp_package(napp_name): |
417
|
|
|
"""Build the .napp file to be sent to the napps server. |
418
|
|
|
|
419
|
|
|
Args: |
420
|
|
|
napp_identifier (str): Identifier formatted as |
421
|
|
|
<username>/<napp_name> |
422
|
|
|
|
423
|
|
|
Return: |
424
|
|
|
file_payload (binary): The binary representation of the napp |
425
|
|
|
package that will be POSTed to the napp server. |
426
|
|
|
|
427
|
|
|
""" |
428
|
1 |
|
def get_matches(path): |
429
|
|
|
"""Return all NApp files matching any .gitignore pattern.""" |
430
|
1 |
|
ignored_files = [".git"] |
431
|
1 |
|
with open(".gitignore", 'r') as local_gitignore: |
432
|
1 |
|
ignored_files.extend(local_gitignore.readlines()) |
433
|
|
|
|
434
|
1 |
|
user_gitignore_path = pathlib.Path("%s/.gitignore" % |
435
|
|
|
pathlib.Path.home()) |
436
|
1 |
|
if user_gitignore_path.exists(): |
437
|
|
|
with open(user_gitignore_path, 'r') as user_gitignore: |
438
|
|
|
ignored_files.extend(user_gitignore.readlines()) |
439
|
|
|
|
440
|
|
|
# Define Wildmatch pattern (default gitignore pattern) |
441
|
1 |
|
pattern = pathspec.patterns.GitWildMatchPattern |
442
|
1 |
|
spec = pathspec.PathSpec.from_lines(pattern, ignored_files) |
443
|
|
|
# Get tree containing all matching files |
444
|
1 |
|
match_tree = spec.match_tree(path) |
445
|
|
|
# Create list with all absolute paths of match tree |
446
|
1 |
|
return ["%s/%s" % (path, match) for match in match_tree] |
447
|
|
|
|
448
|
1 |
|
files = [] |
449
|
1 |
|
path = os.getcwd() |
450
|
|
|
|
451
|
1 |
|
for dir_file in os.walk(path): |
452
|
1 |
|
dirname, _, arc = dir_file |
453
|
1 |
|
files.extend([os.path.join(dirname, f) for f in arc]) |
454
|
|
|
|
455
|
|
|
# Allow the user to run `kytos napps upload` from outside the |
456
|
|
|
# napp directory. |
457
|
|
|
# Filter the files with the napp_name in their path |
458
|
|
|
# Example: home/user/napps/kytos/, napp_name = kronos |
459
|
|
|
# This filter will get all files from: |
460
|
|
|
# home/user/napps/kytos/kronos/* |
461
|
1 |
|
files = list(filter(lambda x: napp_name in x, files)) |
462
|
|
|
|
463
|
1 |
|
matches = get_matches(path) |
464
|
|
|
|
465
|
1 |
|
for filename in files.copy(): |
466
|
1 |
|
if filename in matches: |
467
|
1 |
|
files.remove(filename) |
468
|
|
|
|
469
|
|
|
# Create the '.napp' package |
470
|
1 |
|
napp_file = tarfile.open(napp_name + '.napp', 'x:xz') |
471
|
1 |
|
for local_f in files: |
472
|
|
|
# Add relative paths instead of absolute paths |
473
|
1 |
|
napp_file.add(pathlib.PurePosixPath(local_f).relative_to(path)) |
474
|
1 |
|
napp_file.close() |
475
|
|
|
|
476
|
|
|
# Get the binary payload of the package |
477
|
1 |
|
file_payload = open(napp_name + '.napp', 'rb') |
478
|
|
|
|
479
|
|
|
# remove the created package from the filesystem |
480
|
1 |
|
os.remove(napp_name + '.napp') |
481
|
|
|
|
482
|
1 |
|
return file_payload |
483
|
|
|
|
484
|
1 |
|
@staticmethod |
485
|
|
|
def create_metadata(*args, **kwargs): # pylint: disable=unused-argument |
486
|
|
|
"""Generate the metadata to send the napp package.""" |
487
|
1 |
|
json_filename = kwargs.get('json_filename', 'kytos.json') |
488
|
1 |
|
readme_filename = kwargs.get('readme_filename', 'README.rst') |
489
|
1 |
|
ignore_json = kwargs.get('ignore_json', False) |
490
|
1 |
|
metadata = {} |
491
|
|
|
|
492
|
1 |
|
if not ignore_json: |
493
|
1 |
|
try: |
494
|
1 |
|
with open(json_filename) as json_file: |
495
|
1 |
|
metadata = json.load(json_file) |
496
|
|
|
except FileNotFoundError: |
497
|
|
|
print("ERROR: Could not access kytos.json file.") |
498
|
|
|
sys.exit(1) |
499
|
|
|
|
500
|
1 |
|
try: |
501
|
1 |
|
with open(readme_filename) as readme_file: |
502
|
1 |
|
metadata['readme'] = readme_file.read() |
503
|
|
|
except FileNotFoundError: |
504
|
|
|
metadata['readme'] = '' |
505
|
|
|
|
506
|
1 |
|
try: |
507
|
1 |
|
yaml = YAML(typ='safe') |
508
|
1 |
|
openapi_dict = yaml.load(pathlib.Path('openapi.yml').open()) |
509
|
1 |
|
openapi = json.dumps(openapi_dict) |
510
|
|
|
except FileNotFoundError: |
511
|
|
|
openapi = '' |
512
|
1 |
|
metadata['OpenAPI_Spec'] = openapi |
513
|
|
|
|
514
|
1 |
|
return metadata |
515
|
|
|
|
516
|
1 |
|
def upload(self, *args, **kwargs): |
517
|
|
|
"""Create package and upload it to NApps Server. |
518
|
|
|
|
519
|
|
|
Raises: |
520
|
|
|
FileNotFoundError: If kytos.json is not found. |
521
|
|
|
|
522
|
|
|
""" |
523
|
1 |
|
self.prepare() |
524
|
1 |
|
metadata = self.create_metadata(*args, **kwargs) |
525
|
1 |
|
package = self.build_napp_package(metadata.get('name')) |
526
|
|
|
|
527
|
1 |
|
NAppsClient().upload_napp(metadata, package) |
528
|
|
|
|
529
|
1 |
|
def delete(self): |
530
|
|
|
"""Delete a NApp. |
531
|
|
|
|
532
|
|
|
Raises: |
533
|
|
|
requests.HTTPError: When there's a server error. |
534
|
|
|
|
535
|
|
|
""" |
536
|
1 |
|
client = NAppsClient(self._config) |
537
|
1 |
|
client.delete(self.user, self.napp) |
538
|
|
|
|
539
|
1 |
|
@classmethod |
540
|
|
|
def prepare(cls): |
541
|
|
|
"""Prepare NApp to be uploaded by creating openAPI skeleton.""" |
542
|
1 |
|
if cls._ask_openapi(): |
543
|
1 |
|
napp_path = pathlib.Path() |
544
|
1 |
|
tpl_path = SKEL_PATH / 'napp-structure/username/napp' |
545
|
1 |
|
OpenAPI(napp_path, tpl_path).render_template() |
546
|
1 |
|
print('Please, update your openapi.yml file.') |
547
|
1 |
|
sys.exit() |
548
|
|
|
|
549
|
1 |
|
@staticmethod |
550
|
|
|
def _ask_openapi(): |
551
|
|
|
"""Return whether we should create a (new) skeleton.""" |
552
|
1 |
|
if pathlib.Path('openapi.yml').exists(): |
553
|
1 |
|
question = 'Override local openapi.yml with a new skeleton? (y/N) ' |
554
|
1 |
|
default = False |
555
|
|
|
else: |
556
|
1 |
|
question = 'Do you have REST endpoints and wish to create an API' \ |
557
|
|
|
' skeleton in openapi.yml? (Y/n) ' |
558
|
1 |
|
default = True |
559
|
|
|
|
560
|
1 |
|
while True: |
561
|
1 |
|
answer = input(question) |
562
|
1 |
|
if answer == '': |
563
|
1 |
|
return default |
564
|
1 |
|
if answer.lower() in ['y', 'yes']: |
565
|
1 |
|
return True |
566
|
1 |
|
if answer.lower() in ['n', 'no']: |
567
|
1 |
|
return False |
568
|
|
|
|
569
|
1 |
|
def reload(self, napps=None): |
570
|
|
|
"""Reload a NApp or all NApps. |
571
|
|
|
|
572
|
|
|
Args: |
573
|
|
|
napps (list): NApp list to be reloaded. |
574
|
|
|
Raises: |
575
|
|
|
requests.HTTPError: When there's a server error. |
576
|
|
|
|
577
|
|
|
""" |
578
|
1 |
|
client = NAppsClient(self._config) |
579
|
1 |
|
client.reload_napps(napps) |
580
|
|
|
|
581
|
|
|
|
582
|
|
|
# pylint: enable=too-many-instance-attributes,too-many-public-methods |
583
|
|
|
|