1
|
|
|
"""Manage Network Application files.""" |
2
|
|
|
import json |
3
|
|
|
import logging |
4
|
|
|
import shutil |
5
|
|
|
import urllib |
6
|
|
|
from pathlib import Path |
7
|
|
|
|
8
|
|
|
from kytos.core.napps import NApp |
9
|
|
|
|
10
|
|
|
log = logging.getLogger(__name__) |
11
|
|
|
|
12
|
|
|
|
13
|
|
|
class NAppsManager: |
14
|
|
|
"""Deal with NApps at filesystem level and ask Kytos to (un)load NApps.""" |
15
|
|
|
|
16
|
|
|
def __init__(self, controller): |
17
|
|
|
"""Need the controller for configuration paths and (un)loading NApps. |
18
|
|
|
|
19
|
|
|
Args: |
20
|
|
|
controller (kytos.Controller): Controller to (un)load NApps. |
21
|
|
|
""" |
22
|
|
|
self._config = controller.options |
23
|
|
|
|
24
|
|
|
self._controller = controller |
25
|
|
|
|
26
|
|
|
self._enabled = Path(self._config.napps) |
27
|
|
|
self._installed = self._enabled / '.installed' |
28
|
|
|
|
29
|
|
|
def enable(self, napp_uri): |
30
|
|
|
"""Enable a NApp if not already enabled. |
31
|
|
|
|
32
|
|
|
Raises: |
33
|
|
|
FileNotFoundError: If NApp is not installed. |
34
|
|
|
PermissionError: No filesystem permission to enable NApp. |
35
|
|
|
""" |
36
|
|
|
napp = NApp.create_from_uri(napp_uri) |
37
|
|
|
enabled = self._enabled / napp.username / napp.name |
38
|
|
|
installed = self._installed / napp.username / napp.name |
39
|
|
|
|
40
|
|
|
if not installed.is_dir(): |
41
|
|
|
log.error("Failed to enable NApp %s. NApp not installed.", |
42
|
|
|
napp.id) |
43
|
|
|
elif not enabled.exists(): |
44
|
|
|
self._create_module(enabled.parent) |
45
|
|
|
try: |
46
|
|
|
# Create symlink |
47
|
|
|
enabled.symlink_to(installed) |
48
|
|
|
if self._controller is not None: |
49
|
|
|
self._controller.load_napp(napp.username, napp.name) |
50
|
|
|
log.info("NApp enabled: %s", napp.id) |
51
|
|
|
except FileExistsError: |
52
|
|
|
pass # OK, NApp was already enabled |
53
|
|
|
except PermissionError: |
54
|
|
|
log.error("Failed to enable NApp %s. Permission denied.", |
55
|
|
|
napp.id) |
56
|
|
|
|
57
|
|
|
def disable(self, napp_uri): |
58
|
|
|
"""Disable a NApp if it is enabled.""" |
59
|
|
|
napp = NApp.create_from_uri(napp_uri) |
60
|
|
|
enabled = self._enabled / napp.username / napp.name |
61
|
|
|
try: |
62
|
|
|
enabled.unlink() |
63
|
|
|
log.info("NApp disabled: %s", napp.id) |
64
|
|
|
if self._controller is not None: |
65
|
|
|
self._controller.unload_napp(napp.username, napp.name) |
66
|
|
|
except FileNotFoundError: |
67
|
|
|
pass # OK, it was already disabled |
68
|
|
|
|
69
|
|
|
def install(self, napp_uri, enable=True): |
70
|
|
|
"""Install and enable a NApp from his repository. |
71
|
|
|
|
72
|
|
|
By default, install procedure will also enable the NApp. If you only |
73
|
|
|
want to install and keep NApp disabled, please use enable=False. |
74
|
|
|
""" |
75
|
|
|
napp = NApp.create_from_uri(napp_uri) |
76
|
|
|
|
77
|
|
|
if napp in self.list(): |
78
|
|
|
log.warning("Unable to install NApp %s. Already installed.", napp) |
79
|
|
|
return False |
80
|
|
|
|
81
|
|
|
if not napp.repository: |
82
|
|
|
options = self._controller.options |
83
|
|
|
napp.repository = json.loads(options.napps_repositories)[0] |
84
|
|
|
|
85
|
|
|
pkg_folder = None |
86
|
|
|
try: |
87
|
|
|
pkg_folder = napp.download() |
88
|
|
|
napp_folder = self._get_local_folder(napp, pkg_folder) |
89
|
|
|
dst = self._installed / napp.username / napp.name |
90
|
|
|
self._create_module(dst.parent) |
91
|
|
|
shutil.move(str(napp_folder), str(dst)) |
92
|
|
|
finally: |
93
|
|
|
if pkg_folder and pkg_folder.exists(): |
94
|
|
|
shutil.rmtree(str(pkg_folder)) |
95
|
|
|
|
96
|
|
|
log.info("New NApp installed: %s", napp) |
97
|
|
|
|
98
|
|
|
napp = NApp.create_from_json(dst/'kytos.json') |
99
|
|
|
log.info('Installing Napp Dependencies:') |
100
|
|
|
for napp in napp.napp_dependencies: |
101
|
|
|
self.install(napp, enable) |
102
|
|
|
|
103
|
|
|
if enable: |
104
|
|
|
return self.enable(napp_uri) |
105
|
|
|
|
106
|
|
|
return True |
107
|
|
|
|
108
|
|
|
def uninstall(self, napp_uri): |
109
|
|
|
"""Remove a NApp from filesystem, if existent.""" |
110
|
|
|
napp = NApp.create_from_uri(napp_uri) |
111
|
|
|
|
112
|
|
|
if self.is_enabled(napp_uri): |
113
|
|
|
log.warning("Unable to uninstall NApp %s. NApp currently in use.", |
114
|
|
|
napp) |
115
|
|
|
return False |
116
|
|
|
|
117
|
|
|
if self.is_installed(napp_uri): |
118
|
|
|
installed = self._installed / napp.username / napp.name |
119
|
|
|
if installed.is_symlink(): |
120
|
|
|
installed.unlink() |
121
|
|
|
else: |
122
|
|
|
shutil.rmtree(str(installed)) |
123
|
|
|
log.info("NApp uninstalled: %s", napp) |
124
|
|
|
else: |
125
|
|
|
log.warning("Unable to uninstall NApp %s. Already uninstalled.", |
126
|
|
|
napp) |
127
|
|
|
return True |
128
|
|
|
|
129
|
|
|
def is_enabled(self, napp_uri): |
130
|
|
|
"""Whether a NApp is enabled or not on this controller.""" |
131
|
|
|
napp = NApp.create_from_uri(napp_uri) |
132
|
|
|
return napp in self.list_enabled() |
133
|
|
|
|
134
|
|
|
def is_installed(self, napp_uri): |
135
|
|
|
"""Whether a NApp is installed or not on this controller.""" |
136
|
|
|
napp = NApp.create_from_uri(napp_uri) |
137
|
|
|
return napp in self.list() |
138
|
|
|
|
139
|
|
|
def list(self): |
140
|
|
|
"""List all NApps on this controller.""" |
141
|
|
|
disabled = self.list_disabled() |
142
|
|
|
enabled = self.list_enabled() |
143
|
|
|
return enabled + disabled |
144
|
|
|
|
145
|
|
|
def list_enabled(self): |
146
|
|
|
"""List all enabled NApps on this controller.""" |
147
|
|
|
enabled = self._list_all(self._enabled) |
148
|
|
|
for napp in enabled: |
149
|
|
|
napp.enabled = True |
150
|
|
|
return enabled |
151
|
|
|
|
152
|
|
|
def list_disabled(self): |
153
|
|
|
"""List all disabled NApps on this controller.""" |
154
|
|
|
installed = set(self._list_all(self._installed)) |
155
|
|
|
enabled = set(self.list_enabled()) |
156
|
|
|
return list(installed - enabled) |
157
|
|
|
|
158
|
|
|
def search(self, pattern, use_cache=False): |
159
|
|
|
"""Search for NApps in NApp repositories matching a pattern.""" |
160
|
|
|
# ISSUE #347, we need to loop here over all repositories |
161
|
|
|
repo = eval(self._config.napps_repositories)[0] # noqa |
162
|
|
|
|
163
|
|
|
if use_cache: |
164
|
|
|
# ISSUE #346, we should use cache here |
165
|
|
|
pass |
166
|
|
|
|
167
|
|
|
result = urllib.request.urlretrieve("{}/.database".format(repo))[0] |
168
|
|
|
with open(result, 'r') as fp: |
169
|
|
|
napps_json = json.load(fp) |
170
|
|
|
|
171
|
|
|
napps = [NApp.create_from_dict(napp_json) for napp_json in napps_json] |
172
|
|
|
return [napp for napp in napps if napp.match(pattern)] |
173
|
|
|
|
174
|
|
|
@staticmethod |
175
|
|
|
def _list_all(napps_dir): |
176
|
|
|
"""List all NApps found in ``napps_dir``.""" |
177
|
|
|
if not napps_dir.exists(): |
178
|
|
|
log.warning("NApps dir (%s) doesn't exist.", napps_dir) |
179
|
|
|
return [] |
180
|
|
|
|
181
|
|
|
jsons = napps_dir.glob('*/*/kytos.json') |
182
|
|
|
return [NApp.create_from_json(j) for j in jsons] |
183
|
|
|
|
184
|
|
|
@staticmethod |
185
|
|
|
def _create_module(folder): |
186
|
|
|
"""Create module folder with empty __init__.py if it doesn't exist. |
187
|
|
|
|
188
|
|
|
Args: |
189
|
|
|
folder (pathlib.Path): Module path. |
190
|
|
|
""" |
191
|
|
|
if not folder.exists(): |
192
|
|
|
folder.mkdir() |
193
|
|
|
(folder / '__init__.py').touch() |
194
|
|
|
|
195
|
|
|
@staticmethod |
196
|
|
|
def _get_local_folder(napp, root=None): |
197
|
|
|
"""Return local NApp root folder. |
198
|
|
|
|
199
|
|
|
Search for kytos.json in _./_ folder and _./user/napp_. |
200
|
|
|
|
201
|
|
|
Args: |
202
|
|
|
root (pathlib.Path): Where to begin searching. |
203
|
|
|
|
204
|
|
|
Raises: |
205
|
|
|
FileNotFoundError: If there is no such local NApp. |
206
|
|
|
|
207
|
|
|
Return: |
208
|
|
|
pathlib.Path: NApp root folder. |
209
|
|
|
""" |
210
|
|
|
if root is None: |
211
|
|
|
root = Path() |
212
|
|
|
for folders in ['.'], [napp.username, napp.name]: |
213
|
|
|
kytos_json = root / Path(*folders) / 'kytos.json' |
214
|
|
|
if kytos_json.exists(): |
215
|
|
|
with kytos_json.open() as f: |
216
|
|
|
meta = json.load(f) |
217
|
|
|
if meta['username'] == napp.username and \ |
218
|
|
|
meta['name'] == napp.name: |
219
|
|
|
return kytos_json.parent |
220
|
|
|
raise FileNotFoundError('kytos.json not found.') |
221
|
|
|
|