diff --git a/kytos/core/api_server.py b/kytos/core/api_server.py index 11a7ea2ce..4dbf7b245 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 @@ -24,17 +25,23 @@ class APIServer: _NAPP_PREFIX = "/api/{napp.username}/{napp.name}/" _CORE_PREFIX = "/api/kytos/core/" + # pylint: disable=too-many-arguments 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.napps_manager = napps_manager + self.napps_dir = napps_dir + self.flask_dir = os.path.join(dirname, '../web-ui') self.log = logging.getLogger('api_server') @@ -53,7 +60,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 +96,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 +121,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 +140,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 +341,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.napps_manager.is_installed(username, napp_name): + return '{"response": "not installed"}', \ + HTTPStatus.BAD_REQUEST.value + + # Check if the NApp is already been enabled + if not self.napps_manager.is_enabled(username, napp_name): + self.napps_manager.enable(username, napp_name) + + # Check if NApp is enabled + if not self.napps_manager.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.napps_manager.is_installed(username, napp_name): + return '{"response": "not installed"}', \ + HTTPStatus.BAD_REQUEST.value + + # Check if the NApp is enabled + if self.napps_manager.is_enabled(username, napp_name): + self.napps_manager.disable(username, napp_name) + + # Check if NApp is still enabled + if self.napps_manager.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.napps_manager.is_installed(username, napp_name): + return '{"response": "installed"}', HTTPStatus.OK.value + + napp = "{}/{}".format(username, napp_name) + + # Try to install the napp + if not self.napps_manager.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.napps_manager.is_installed(username, napp_name): + # Try to unload/uninstall the napp + if not self.napps_manager.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.napps_manager.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.napps_manager.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.napps_manager.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.napps_manager.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/connection.py b/kytos/core/connection.py index a5d1ea54a..5f6a27627 100644 --- a/kytos/core/connection.py +++ b/kytos/core/connection.py @@ -2,8 +2,8 @@ import logging from enum import Enum from errno import EBADF, ENOTCONN -from socket import error as SocketError from socket import SHUT_RDWR +from socket import error as SocketError __all__ = ('Connection', 'ConnectionProtocol', 'ConnectionState') diff --git a/kytos/core/controller.py b/kytos/core/controller.py index 9eb03851e..ddad94ca1 100644 --- a/kytos/core/controller.py +++ b/kytos/core/controller.py @@ -22,8 +22,8 @@ import sys import threading from concurrent.futures import ThreadPoolExecutor -from importlib import reload as reload_module from importlib import import_module +from importlib import reload as reload_module from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path @@ -110,18 +110,17 @@ 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, + self.napps_manager, self.options.napps) + + self._register_endpoints() #: Adding the napps 'enabled' directory into the PATH #: Now you can access the enabled napps with: #: from napps.. import ?.... @@ -572,7 +571,7 @@ def set_switch_options(self, dpid): self.log.error("Invalid vlan_pool settings: %s", err) if vlan_pool.get(dpid): - self.log.info(f"Loading vlan_pool configuration for dpid {dpid}") + self.log.info("Loading vlan_pool configuration for dpid %s", dpid) for intf_num, port_list in vlan_pool[dpid].items(): if not switch.interfaces.get((intf_num)): vlan_ids = set() diff --git a/kytos/core/interface.py b/kytos/core/interface.py index 411260405..55055d9f7 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -107,6 +107,7 @@ def id(self): # pylint: disable=invalid-name Returns: string: Interface id. + """ return "{}:{}".format(self.switch.dpid, self.port_number) diff --git a/kytos/core/napps/manager.py b/kytos/core/napps/manager.py index b6adf3419..a0ccee725 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("NApp metadata load 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_. diff --git a/tests/test_core/test_interface.py b/tests/test_core/test_interface.py index 4047a3658..04eb61271 100644 --- a/tests/test_core/test_interface.py +++ b/tests/test_core/test_interface.py @@ -5,7 +5,7 @@ from pyof.v0x04.common.port import PortFeatures -from kytos.core.interface import Interface, TAG, TAGType +from kytos.core.interface import TAG, Interface, TAGType from kytos.core.switch import Switch logging.basicConfig(level=logging.CRITICAL) diff --git a/tests/test_core/test_switch.py b/tests/test_core/test_switch.py index 808902282..e44e565c0 100644 --- a/tests/test_core/test_switch.py +++ b/tests/test_core/test_switch.py @@ -28,7 +28,7 @@ def tearDown(self): def test_switch_vlan_pool_default(self): """Test default vlan_pool value.""" - self.assertEqual(self.options.vlan_pool, {}) + self.assertEqual(self.options.vlan_pool, '{}') def test_switch_vlan_pool_options(self): """Test switch with the example from kytos.conf."""