Test Failed
Pull Request — master (#167)
by Italo Valcy
05:44
created

kytos.core.napps.manager   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 301
Duplicated Lines 0 %

Test Coverage

Coverage 89.71%

Importance

Changes 0
Metric Value
eloc 184
dl 0
loc 301
ccs 157
cts 175
cp 0.8971
rs 4.08
c 0
b 0
f 0
wmc 59

20 Methods

Rating   Name   Duplication   Size   Complexity  
B NAppsManager.disable() 0 24 6
A NAppsManager.get_all_napps() 0 3 1
B NAppsManager.install() 0 36 7
A NAppsManager.__init__() 0 19 2
A NAppsManager.get_napp_fullname_from_uri() 0 8 1
A NAppsManager.get_napp_metadata() 0 21 3
A NAppsManager.enable_all() 0 4 2
A NAppsManager.disable_all() 0 4 2
A NAppsManager.is_enabled() 0 6 1
B NAppsManager.uninstall() 0 30 7
A NAppsManager.get_installed_napps() 0 3 1
A NAppsManager.is_installed() 0 5 1
B NAppsManager.enable() 0 32 8
B NAppsManager._find_napp() 0 27 7
A NAppsManager.get_napps_from_path() 0 12 3
A NAppsManager.get_enabled_napps() 0 7 2
A NAppsManager.get_disabled_napps() 0 5 1
A NewNAppManager._find_napps() 0 2 1
A NAppsManager._create_module() 0 10 2
A NewNAppManager.__init__() 0 4 1

How to fix   Complexity   

Complexity

Complex classes like kytos.core.napps.manager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Manage Network Application files."""
2 2
import json
3 2
import logging
4 2
import re
5 2
import shutil
6 2
from pathlib import Path
7
8 2
from kytos.core.napps import NApp
9
10 2
LOG = logging.getLogger(__name__)
11
12
13 2
class NAppsManager:
14
    """Deal with NApps at filesystem level and ask Kytos to (un)load NApps."""
15
16 2
    def __init__(self, controller=None, base_path=None):
17
        """Need the controller for configuration paths and (un)loading NApps.
18
19
        Args:
20
            controller (kytos.Controller): Controller to (un)load NApps.
21
            base_path (pathlib.Path): base path for enabled NApps.
22
                This will be supported while kytos-utils still imports
23
                kytos.core directly, and may be removed when it calls Kytos'
24
                Web API.
25
        """
26 2
        self._controller = controller
27
28 2
        if base_path:
29
            self._enabled_path = base_path
30
        else:
31 2
            self._config = controller.options
32 2
            self._enabled_path = Path(self._config.napps)
33
34 2
        self._installed_path = self._enabled_path / '.installed'
35
36 2
    def install(self, napp_uri, enable=True):
37
        """Install and enable a NApp from its repository.
38
39
        By default, install procedure will also enable the NApp. If you only
40
        want to install and keep NApp disabled, please use enable=False.
41
        """
42 2
        napp = NApp.create_from_uri(napp_uri)
43
44 2
        if napp in self.get_all_napps():
45 2
            LOG.warning("Unable to install NApp %s. Already installed.", napp)
46 2
            return False
47
48 2
        if not napp.repository:
49
            napp.repository = self._controller.options.napps_repositories[0]
50
51 2
        pkg_folder = None
52 2
        try:
53 2
            pkg_folder = napp.download()
54 2
            napp_folder = self._find_napp(napp, pkg_folder)
55 2
            dst = self._installed_path / napp.username / napp.name
56 2
            self._create_module(dst.parent)
57 2
            shutil.move(str(napp_folder), str(dst))
58
        finally:
59 2
            if pkg_folder and pkg_folder.exists():
60 2
                shutil.rmtree(str(pkg_folder))
61
62 2
        LOG.info("New NApp installed: %s", napp)
63
64 2
        napp = NApp.create_from_json(dst/'kytos.json')
65 2
        for uri in napp.napp_dependencies:
66
            self.install(uri, enable)
67
68 2
        if enable:
69 2
            return self.enable(napp.username, napp.name)
70
71 2
        return True
72
73 2
    def uninstall(self, username, napp_name):
74
        """Remove a NApp from filesystem, if existent."""
75 2
        napp_id = "{}/{}".format(username, napp_name)
76
77 2
        if self.is_enabled(username, napp_name):
78 2
            LOG.warning("Unable to uninstall NApp %s. NApp currently in use.",
79
                        napp_id)
80 2
            return False
81
82 2
        new_manager = NewNAppManager(self._installed_path)
83 2
        napp = new_manager.napps[napp_id]
84 2
        deps = napp.napp_dependencies
85
86 2
        if deps and napp.meta:
87 2
            LOG.info('Uninstalling Meta-NApp %s dependencies: %s', napp, deps)
88 2
            for uri in deps:
89 2
                username, napp_name = self.get_napp_fullname_from_uri(uri)
90 2
                self.uninstall(username, napp_name)
91
92 2
        if self.is_installed(username, napp_name):
93 2
            installed = self._installed_path / napp_id
94 2
            if installed.is_symlink():
95 2
                installed.unlink()
96
            else:
97
                shutil.rmtree(str(installed))
98 2
            LOG.info("NApp uninstalled: %s", napp_id)
99
        else:
100 2
            LOG.warning("Unable to uninstall NApp %s. Already uninstalled.",
101
                        napp_id)
102 2
        return True
103
104 2
    def enable(self, username, napp_name):
105
        """Enable a NApp if not already enabled."""
106 2
        napp_id = "{}/{}".format(username, napp_name)
107
108 2
        enabled = self._enabled_path / napp_id
109 2
        installed = self._installed_path / napp_id
110
111 2
        new_manager = NewNAppManager(self._installed_path)
112 2
        napp = new_manager.napps[napp_id]
113 2
        deps = napp.napp_dependencies
114
115 2
        if deps and napp.meta:
116 2
            LOG.info('Enabling Meta-NApp %s dependencies: %s', napp, deps)
117 2
            for uri in deps:
118 2
                username, napp_name = self.get_napp_fullname_from_uri(uri)
119 2
                self.enable(username, napp_name)
120
121 2
        if not installed.is_dir():
122
            LOG.error("Failed to enable NApp %s. NApp not installed.", napp_id)
123 2
        elif not enabled.exists():
124 2
            self._create_module(enabled.parent)
125 2
            try:
126
                # Create symlink
127 2
                enabled.symlink_to(installed)
128 2
                LOG.info("NApp enabled: %s", napp_id)
129
            except FileExistsError:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable FileExistsError does not seem to be defined.
Loading history...
130
                pass  # OK, NApp was already enabled
131
            except PermissionError:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable PermissionError does not seem to be defined.
Loading history...
132
                LOG.error("Failed to enable NApp %s. Permission denied.",
133
                          napp_id)
134
135 2
        return True
136
137 2
    def disable(self, username, napp_name):
138
        """Disable a NApp if it is enabled."""
139 2
        napp_id = "{}/{}".format(username, napp_name)
140 2
        enabled = self._enabled_path / napp_id
141
142 2
        new_manager = NewNAppManager(self._installed_path)
143 2
        napp = new_manager.napps[napp_id]
144 2
        deps = napp.napp_dependencies
145
146 2
        if deps and napp.meta:
147 2
            LOG.info('Disabling Meta-NApp %s dependencies: %s', napp, deps)
148 2
            for uri in deps:
149 2
                username, napp_name = self.get_napp_fullname_from_uri(uri)
150 2
                self.disable(username, napp_name)
151
152 2
        try:
153 2
            enabled.unlink()
154 2
            LOG.info("NApp disabled: %s", napp_id)
155 2
            if self._controller is not None:
156 2
                self._controller.unload_napp(username, napp_name)
157
        except FileNotFoundError:
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable FileNotFoundError does not seem to be defined.
Loading history...
158
            pass  # OK, it was already disabled
159
160 2
        return True
161
162 2
    def enable_all(self):
163
        """Enable all napps already installed and disabled."""
164 2
        for napp in self.get_disabled_napps():
165 2
            self.enable(napp.username, napp.name)
166
167 2
    def disable_all(self):
168
        """Disable all napps already installed and enabled."""
169 2
        for napp in self.get_enabled_napps():
170 2
            self.disable(napp.username, napp.name)
171
172 2
    def is_enabled(self, username, napp_name):
173
        """Whether a NApp is enabled or not on this controller FS."""
174 2
        napp_id = "{}/{}".format(username, napp_name)
175
176 2
        napp = NApp.create_from_uri(napp_id)
177 2
        return napp in self.get_enabled_napps()
178
179 2
    def is_installed(self, username, napp_name):
180
        """Whether a NApp is installed or not on this controller."""
181 2
        napp_id = "{}/{}".format(username, napp_name)
182 2
        napp = NApp.create_from_uri(napp_id)
183 2
        return napp in self.get_all_napps()
184
185 2
    @staticmethod
186
    def get_napp_fullname_from_uri(uri):
187
        """Parse URI and get (username, napp_name) tuple."""
188 2
        regex = r'^(((https?://|file://)(.+))/)?(.+?)/(.+?)/?(:(.+))?$'
189 2
        match = re.match(regex, uri)
190 2
        username = match.groups()[4]
191 2
        napp_name = match.groups()[5]
192 2
        return username, napp_name
193
194 2
    def get_all_napps(self):
195
        """List all NApps on this controller FS."""
196 2
        return self.get_installed_napps()
197
198 2
    def get_enabled_napps(self):
199
        """Return all enabled NApps on this controller FS."""
200 2
        enabled = self.get_napps_from_path(self._enabled_path)
201 2
        for napp in enabled:
202
            # We should also check if the NApp is enabled on controller
203
            napp.enabled = True
204 2
        return enabled
205
206 2
    def get_disabled_napps(self):
207
        """Return all disabled NApps on this controller FS."""
208 2
        installed = set(self.get_installed_napps())
209
        enabled = set(self.get_enabled_napps())
210
        return list(installed - enabled)
211
212 2
    def get_installed_napps(self):
213
        """Return all NApps installed on this controller FS."""
214 2
        return self.get_napps_from_path(self._installed_path)
215
216 2
    def get_napp_metadata(self, username, napp_name, key):
217
        """Return a value from kytos.json.
218
219
        Args:
220
            username (string): A Username.
221
            napp_name (string): A NApp name
222
            key (string): Key used to get the value within kytos.json.
223
224
        Returns:
225
            meta (object): Value stored in kytos.json.
226
227
        """
228 2
        napp_id = "{}/{}".format(username, napp_name)
229 2
        kytos_json = self._installed_path / napp_id / 'kytos.json'
230 2
        try:
231 2
            with kytos_json.open() as file_descriptor:
232 2
                meta = json.load(file_descriptor)
233 2
                return meta[key]
234 2
        except (FileNotFoundError, json.JSONDecodeError, KeyError):
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable FileNotFoundError does not seem to be defined.
Loading history...
235 2
            LOG.warning("NApp metadata load failed: %s/kytos.json", napp_id)
236 2
            return ''
237
238 2
    @staticmethod
239 2
    def get_napps_from_path(path: Path):
240
        """List all NApps found in ``napps_dir``."""
241 2
        if not path.exists():
242 2
            LOG.warning("NApps dir (%s) doesn't exist.", path)
243 2
            return []
244
245 2
        jsons = sorted(
246
            path.glob('*/*/kytos.json'),
247
            key=lambda f: f.stat().st_mtime
248
        )
249 2
        return [NApp.create_from_json(j) for j in jsons]
250
251 2
    @staticmethod
252 2
    def _create_module(path: Path):
253
        """Create module with empty __init__.py in `path` if it doesn't exist.
254
255
        Args:
256
            path: Module path.
257
        """
258 2
        if not path.exists():
259 2
            path.mkdir(parents=True, exist_ok=True, mode=0o755)
260 2
        (path / '__init__.py').touch()
261
262 2
    @staticmethod
263 2
    def _find_napp(napp, root: Path = None) -> Path:
264
        """Return local NApp root folder.
265
266
        Search for kytos.json in _./_ folder and _./user/napp_.
267
268
        Args:
269
            root: Where to begin searching.
270
271
        Raises:
272
            FileNotFoundError: If there is no such local NApp.
273
274
        Returns:
275
            NApp root folder.
276
277
        """
278 2
        if root is None:
279 2
            root = Path()
280 2
        for folders in ['.'], [napp.username, napp.name]:
281 2
            kytos_json = root / Path(*folders) / 'kytos.json'
282 2
            if kytos_json.exists():
283 2
                with kytos_json.open() as file_descriptor:
284 2
                    meta = json.load(file_descriptor)
285 2
                    if meta['username'] == napp.username and \
286
                            meta['name'] == napp.name:
287 2
                        return kytos_json.parent
288
        raise FileNotFoundError('kytos.json not found.')
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable FileNotFoundError does not seem to be defined.
Loading history...
289
290
291 2
class NewNAppManager:
292
    """A more simple NApp Manager, for just one NApp at a time."""
293
294 2
    def __init__(self, base_path: Path):
295
        """Create a manager from a NApp base path."""
296
        self.base_path = base_path
297
        self.napps = {napp.id: napp for napp in self._find_napps()}
298
299 2
    def _find_napps(self):
300
        return NAppsManager.get_napps_from_path(self.base_path)
301