diff --git a/kytos/core/api_server.py b/kytos/core/api_server.py index 11a7ea2ce..baa8fc8d0 100644 --- a/kytos/core/api_server.py +++ b/kytos/core/api_server.py @@ -8,6 +8,7 @@ import zipfile from datetime import datetime from glob import glob +from http import HTTPStatus from urllib.error import HTTPError, URLError from urllib.request import urlopen, urlretrieve @@ -25,16 +26,21 @@ class APIServer: _CORE_PREFIX = "/api/kytos/core/" def __init__(self, app_name, listen='0.0.0.0', port=8181, - napps_dir=None): + napps_manager=None, napps_dir=None): """Start a Flask+SocketIO server. + Require controller to get NApps dir and NAppsManager + Args: app_name(string): String representing a App Name listen (string): host name used by api server instance port (int): Port number used by api server instance - napps_dir(string): napps path directory + controller(kytos.core.controller): A controller instance. """ dirname = os.path.dirname(os.path.abspath(__file__)) + self.nappsManager = napps_manager + self.napps_dir = napps_dir + self.flask_dir = os.path.join(dirname, '../web-ui') self.log = logging.getLogger('api_server') @@ -53,7 +59,6 @@ def __init__(self, app_name, listen='0.0.0.0', port=8181, # Update web-ui if necessary self.update_web_ui(force=False) - self.napps_dir = napps_dir def _enable_websocket_rooms(self): socket = self.server @@ -90,6 +95,9 @@ def start_api(self): self.register_core_endpoint('web/update/', self.update_web_ui, methods=['POST']) + + self.register_core_napp_services() + self._register_web_ui() def register_core_endpoint(self, rule, function, **options): @@ -112,7 +120,7 @@ def _register_web_ui(self): @staticmethod def status_api(): """Display kytos status using the route ``/kytos/status/``.""" - return '{"response": "running"}', 201 + return '{"response": "running"}', HTTPStatus.CREATED.value def stop_api_server(self): """Send a shutdown request to stop Api Server.""" @@ -131,18 +139,18 @@ def shutdown_api(self): allowed_host = ['127.0.0.1:'+str(self.port), 'localhost:'+str(self.port)] if request.host not in allowed_host: - return "", 403 + return "", HTTPStatus.FORBIDDEN.value self.server.stop() - return 'Server shutting down...', 200 + return 'Server shutting down...', HTTPStatus.OK.value def static_web_ui(self, username, napp_name, filename): """Serve static files from installed napps.""" path = f"{self.napps_dir}/{username}/{napp_name}/ui/{filename}" if os.path.exists(path): return send_file(path) - return "", 404 + return "", HTTPStatus.NOT_FOUND.value def get_ui_components(self, section_name): """Return all napps ui components from an specific section. @@ -332,3 +340,139 @@ def remove_napp_endpoints(self, napp): # pylint: enable=protected-access self.log.info('The Rest endpoints from %s were disabled.', prefix) + + def register_core_napp_services(self): + """ + Register /kytos/core/ services over NApps. + + It registers enable, disable, install, uninstall NApps that will + be used by kytos-utils. + """ + self.register_core_endpoint("napps///enable", + self._enable_napp) + self.register_core_endpoint("napps///disable", + self._disable_napp) + self.register_core_endpoint("napps///install", + self._install_napp) + self.register_core_endpoint("napps///uninstall", + self._uninstall_napp) + self.register_core_endpoint("napps_enabled", + self._list_enabled_napps) + self.register_core_endpoint("napps_installed", + self._list_installed_napps) + self.register_core_endpoint( + "napps///metadata/", + self._get_napp_metadata) + + def _enable_napp(self, username, napp_name): + """ + Enable an installed NApp. + + :param username: NApps user name + :param napp_name: NApp name + :return: JSON content and return code + """ + # Check if the NApp is installed + if not self.nappsManager.is_installed(username, napp_name): + return '{"response": "not installed"}', \ + HTTPStatus.BAD_REQUEST.value + + # Check if the NApp is already been enabled + if not self.nappsManager.is_enabled(username, napp_name): + self.nappsManager.enable(username, napp_name) + + # Check if NApp is enabled + if not self.nappsManager.is_enabled(username, napp_name): + # If it is not enabled an admin user must check the log file + return '{"response": "error"}', \ + HTTPStatus.INTERNAL_SERVER_ERROR.value + + return '{"response": "enabled"}', HTTPStatus.OK.value + + def _disable_napp(self, username, napp_name): + """ + Disable an installed NApp. + + :param username: NApps user name + :param napp_name: NApp name + :return: JSON content and return code + """ + # Check if the NApp is installed + if not self.nappsManager.is_installed(username, napp_name): + return '{"response": "not installed"}', \ + HTTPStatus.BAD_REQUEST.value + + # Check if the NApp is enabled + if self.nappsManager.is_enabled(username, napp_name): + self.nappsManager.disable(username, napp_name) + + # Check if NApp is still enabled + if self.nappsManager.is_enabled(username, napp_name): + # If it is still enabled an admin user must check the log file + return '{"response": "error"}', \ + HTTPStatus.INTERNAL_SERVER_ERROR.value + + return '{"response": "disabled"}', \ + HTTPStatus.OK.value + + def _install_napp(self, username, napp_name): + # Check if the NApp is installed + if self.nappsManager.is_installed(username, napp_name): + return '{"response": "installed"}', HTTPStatus.OK.value + + napp = "{}/{}".format(username, napp_name) + + # Try to install the napp + if not self.nappsManager.install(napp, enable=False): + # If it is not installed an admin user must check the log file + return '{"response": "error"}', \ + HTTPStatus.INTERNAL_SERVER_ERROR.value + + return '{"response": "installed"}', HTTPStatus.OK.value + + def _uninstall_napp(self, username, napp_name): + # Check if the NApp is installed + if self.nappsManager.is_installed(username, napp_name): + # Try to unload/uninstall the napp + if not self.nappsManager.uninstall(username, napp_name): + # If it is not uninstalled admin user must check the log file + return '{"response": "error"}', \ + HTTPStatus.INTERNAL_SERVER_ERROR.value + + return '{"response": "uninstalled"}', HTTPStatus.OK.value + + def _list_enabled_napps(self): + """Sorted list of (username, napp_name) of enabled napps.""" + serialized_dict = json.dumps( + self.nappsManager.get_enabled_napps(), + default=lambda a: [a.username, a.name]) + + return '{"napps": %s}' % serialized_dict, HTTPStatus.OK.value + + def _list_installed_napps(self): + """Sorted list of (username, napp_name) of installed napps.""" + serialized_dict = json.dumps( + self.nappsManager.get_installed_napps(), + default=lambda a: [a.username, a.name]) + + return '{"napps": %s}' % serialized_dict, HTTPStatus.OK.value + + def _get_napp_metadata(self, username, napp_name, key): + """Get NApp metadata value. + + For safety reasons, only some keys can be retrieved: + napp_dependencies, description, version. + + """ + _VALID_KEYS = ['napp_dependencies', 'description', 'version'] + + if not self.nappsManager.is_installed(username, napp_name): + return "Napp is not installed.", HTTPStatus.BAD_REQUEST.value + + if key not in _VALID_KEYS: + return "Invalid key.", HTTPStatus.BAD_REQUEST.value + + data = self.nappsManager.get_napp_metadata(username, napp_name, key) + serialized_dict = json.dumps({key: data}) + + return '%s' % serialized_dict, HTTPStatus.OK.value diff --git a/kytos/core/controller.py b/kytos/core/controller.py index 9eb03851e..06d732a0b 100644 --- a/kytos/core/controller.py +++ b/kytos/core/controller.py @@ -110,18 +110,18 @@ def __init__(self, options=None, loop=None): #: logging.Logger: Logger instance used by Kytos. self.log = None - #: API Server used to expose rest endpoints. - self.api_server = APIServer(__name__, self.options.listen, - self.options.api_port, - napps_dir=self.options.napps) - - self._register_endpoints() #: Observer that handle NApps when they are enabled or disabled. self.napp_dir_listener = NAppDirListener(self) self.napps_manager = NAppsManager(self) + #: API Server used to expose rest endpoints. + self.api_server = APIServer(__name__, self.options.listen, + self.options.api_port, + controller=self) + + self._register_endpoints() #: Adding the napps 'enabled' directory into the PATH #: Now you can access the enabled napps with: #: from napps.. import ?.... diff --git a/kytos/core/napps/manager.py b/kytos/core/napps/manager.py index b6adf3419..e4274650b 100644 --- a/kytos/core/napps/manager.py +++ b/kytos/core/napps/manager.py @@ -211,6 +211,28 @@ def get_installed_napps(self): """Return all NApps installed on this controller FS.""" return self.get_napps_from_path(self._installed_path) + def get_napp_metadata(self, username, napp_name, key): + """Return a value from kytos.json. + + Args: + username (string): A Username. + napp_name (string): A NApp name + key (string): Key used to get the value within kytos.json. + + Returns: + meta (object): Value stored in kytos.json. + + """ + napp_id = "{}/{}".format(username, napp_name) + kytos_json = self._installed_path / napp_id / 'kytos.json' + try: + with kytos_json.open() as file_descriptor: + meta = json.load(file_descriptor) + return meta[key] + except (FileNotFoundError, json.JSONDecodeError, KeyError): + LOG.warning("Load napp metada failed: %s/kytos.json", napp_id) + return '' + @staticmethod def get_napps_from_path(path: Path): """List all NApps found in ``napps_dir``.""" @@ -233,7 +255,7 @@ def _create_module(path: Path): (path / '__init__.py').touch() @staticmethod - def _find_napp(napp, root: Path=None) -> Path: + def _find_napp(napp, root: Path = None) -> Path: """Return local NApp root folder. Search for kytos.json in _./_ folder and _./user/napp_.