def test_save_token(self): """Test save_token method.""" self.kytos_config.save_token('user', 'token') config = KytosConfig(self.config_file).config has_token = config.has_option('auth', 'token') self.assertTrue(has_token)
def test_clear_token(self): """Test clear_token method.""" self.kytos_config.clear_token() config = KytosConfig(self.config_file).config has_token = config.has_option('auth', 'token') self.assertFalse(has_token)
def __init__(self, controller=None): """If controller is not informed, the necessary paths must be. If ``controller`` is available, NApps will be (un)loaded at runtime and you don't need to inform the paths. Otherwise, you should inform the required paths for the methods called. Args: controller (kytos.Controller): Controller to (un)load NApps. install_path (str): Folder where NApps should be installed. If None, use the controller's configuration. enabled_path (str): Folder where enabled NApps are stored. If None, use the controller's configuration. """ self._controller = controller self._config = KytosConfig().config self._kytos_api = self._config.get('kytos', 'api') self.user = None self.napp = None self.version = None # Automatically get from kytosd API when needed self.__enabled = None self.__installed = None
def __init__(self, func): """Init method. Save the function on the func attribute and bootstrap a new config. """ self.func = func self.config = KytosConfig().config self.cls = None self.obj = None
def setUp(self): """Execute steps before each tests.""" with tempfile.TemporaryDirectory() as tmp_dir: config_file = '{}.kytosrc'.format(tmp_dir) kytos_config = KytosConfig(config_file) kytos_config.save_token('user', 'token') self.napps_client = NAppsClient() self.napps_client._config = kytos_config.config self.napps_client._config.set('kytos', 'api', 'endpoint') self.napps_client._config.set('napps', 'api', 'endpoint')
class TestKytosConfig(unittest.TestCase): """Test the class KytosConfig.""" def setUp(self): """Execute steps before each tests.""" with tempfile.TemporaryDirectory() as tmp_dir: self.config_file = '{}.kytosrc'.format(tmp_dir) self.kytos_config = KytosConfig(self.config_file) def test_clear_token(self): """Test clear_token method.""" self.kytos_config.clear_token() config = KytosConfig(self.config_file).config has_token = config.has_option('auth', 'token') self.assertFalse(has_token) def test_save_token(self): """Test save_token method.""" self.kytos_config.save_token('user', 'token') config = KytosConfig(self.config_file).config has_token = config.has_option('auth', 'token') self.assertTrue(has_token) @patch('builtins.open') @patch('kytos.utils.config.urlopen') @patch('kytos.utils.config.logging.RootLogger.warning') def test_check_versions__success(self, *args): """Test check_versions method to success case.""" (mock_warning, mock_urlopen, mock_open) = args urlopen = MagicMock() urlopen.read.return_value = '{"__version__": "123"}' mock_urlopen.return_value = urlopen read_file = MagicMock() read_file.read.return_value = "__version__ = '123'" mock_open.return_value = read_file self.kytos_config.check_versions() mock_warning.assert_not_called() @patch('builtins.open') @patch('kytos.utils.config.urlopen') @patch('kytos.utils.config.logging.RootLogger.warning') def test_check_versions__error(self, *args): """Test check_versions method to error case.""" (mock_warning, mock_urlopen, mock_open) = args urlopen = MagicMock() urlopen.read.return_value = '{"__version__": "123"}' mock_urlopen.return_value = urlopen read_file = MagicMock() read_file.read.return_value = "__version__ = '456'" mock_open.return_value = read_file self.kytos_config.check_versions() mock_warning.assert_called_once()
def __init__(self): """Instance a new NAppsManager. This method do not need parameters. """ self._config = KytosConfig().config self._kytos_api = self._config.get('kytos', 'api') self.user = None self.napp = None self.version = None # Automatically get from kytosd API when needed self.__local_enabled = None self.__local_installed = None
def test_update(self, mock_post): """Test update method.""" args = {'<version>': 'ABC'} self.web_api.update(args) kytos_api = KytosConfig().config.get('kytos', 'api') url = f"{kytos_api}api/kytos/core/web/update/ABC" mock_post.assert_called_with(url)
def authenticate(self): """Check the user authentication.""" endpoint = os.path.join(self.config.get('napps', 'api'), 'auth', '') username = self.config.get('auth', 'user') password = getpass("Enter the password for {}: ".format(username)) response = requests.get(endpoint, auth=(username, password)) if response.status_code != 201: LOG.error(response.content) LOG.error('ERROR: %s: %s', response.status_code, response.reason) sys.exit(1) else: data = response.json() KytosConfig().save_token(username, data.get('hash')) return data.get('hash')
def update(cls): """Call the method to update the Web UI.""" kytos_api = KytosConfig().config.get('kytos', 'api') url = f"{kytos_api}api/kytos/core/web/update/" try: result = requests.post(url) except (HTTPError, URLError, requests.exceptions.ConnectionError): LOG.error(f"Can't connect to the server: {kytos_api}") return if result.status_code != 200: LOG.info(f"Error while update web ui: {result.content}") else: LOG.info("Web UI updated.")
def setUp(self): """Execute steps before each tests.""" self.kytos_auth = kytos_auth(MagicMock()) self.kytos_auth.__get__(MagicMock(), MagicMock()) config = KytosConfig('/tmp/.kytosrc').config config.set('auth', 'user', 'username') config.set('auth', 'token', 'hash') self.kytos_auth.config = config
def upload_napp(self, metadata, package): """Upload the napp from the current directory to the napps server.""" endpoint = os.path.join(self._config.get('napps', 'api'), 'napps', '') metadata['token'] = self._config.get('auth', 'token') request = self.make_request(endpoint, json=metadata, package=package, method="POST") if request.status_code != 201: KytosConfig().clear_token() LOG.error("%s: %s", request.status_code, request.reason) sys.exit(1) # WARNING: this will change in future versions, when 'author' will get # removed. username = metadata.get('username', metadata.get('author')) name = metadata.get('name') print("SUCCESS: NApp {}/{} uploaded.".format(username, name))
def update(cls, args): """Call the method to update the Web UI.""" kytos_api = KytosConfig().config.get('kytos', 'api') url = f"{kytos_api}api/kytos/core/web/update" version = args["<version>"] if version: url += f"/{version}" try: result = requests.post(url) except (HTTPError, URLError, requests.exceptions.ConnectionError): LOG.error("Can't connect to server: %s", kytos_api) return if result.status_code != 200: LOG.info("Error while updating web ui: %s", result.content) else: LOG.info("Web UI updated.")
def authenticate(self): """Check the user authentication.""" endpoint = os.path.join(self.config.get('napps', 'api'), 'auth', '') username = self.config.get('auth', 'user') password = getpass('Enter the password for {}: '.format(username)) response = requests.get(endpoint, auth=(username, password)) # Check if it is unauthorized if response.status_code == 401: print(f'Error with status code: {response.status_code}.\n' 'Possible causes: incorrect credentials, the token was ' 'not set or was expired.') if response.status_code != 201: LOG.error(response.content) LOG.error('ERROR: %s: %s', response.status_code, response.reason) print('Press Ctrl+C or CTRL+Z to stop the process.') user = input('Enter the username: '******'auth', 'user', user) self.authenticate() else: data = response.json() KytosConfig().save_token(username, data.get('hash')) return data.get('hash')
class NAppsManager: """Deal with NApps at filesystem level and ask Kytos to (un)load NApps.""" def __init__(self, controller=None): """If controller is not informed, the necessary paths must be. If ``controller`` is available, NApps will be (un)loaded at runtime and you don't need to inform the paths. Otherwise, you should inform the required paths for the methods called. Args: controller (kytos.Controller): Controller to (un)load NApps. install_path (str): Folder where NApps should be installed. If None, use the controller's configuration. enabled_path (str): Folder where enabled NApps are stored. If None, use the controller's configuration. """ self._controller = controller self._config = KytosConfig().config self._kytos_api = self._config.get('kytos', 'api') self.user = None self.napp = None self.version = None # Automatically get from kytosd API when needed self.__enabled = None self.__installed = None @property def _enabled(self): if self.__enabled is None: self.__require_kytos_config() return self.__enabled @property def _installed(self): if self.__installed is None: self.__require_kytos_config() return self.__installed def __require_kytos_config(self): """Set path locations from kytosd API. It should not be called directly, but from properties that require a running kytosd instance. """ if self.__enabled is None: uri = self._kytos_api + 'api/kytos/core/config/' try: options = json.loads(urllib.request.urlopen(uri).read()) except urllib.error.URLError: print('Kytos is not running.') sys.exit() self.__enabled = Path(options.get('napps')) self.__installed = Path(options.get('installed_napps')) def set_napp(self, user, napp, version=None): """Set info about NApp. Args: user (str): NApps Server username. napp (str): NApp name. version (str): NApp version. """ self.user = user self.napp = napp self.version = version or 'latest' @property def napp_id(self): """Return a Identifier of NApp.""" return '/'.join((self.user, self.napp)) @staticmethod def _get_napps(napps_dir): """List of (username, napp_name) found in ``napps_dir``.""" jsons = napps_dir.glob('*/*/kytos.json') return sorted(j.parts[-3:-1] for j in jsons) def get_enabled(self): """Sorted list of (username, napp_name) of enabled napps.""" return self._get_napps(self._enabled) def get_installed(self): """Sorted list of (username, napp_name) of installed napps.""" return self._get_napps(self._installed) def is_installed(self): """Whether a NApp is installed.""" return (self.user, self.napp) in self.get_installed() def get_disabled(self): """Sorted list of (username, napp_name) of disabled napps. The difference of installed and enabled. """ installed = set(self.get_installed()) enabled = set(self.get_enabled()) return sorted(installed - enabled) def dependencies(self, user=None, napp=None): """Get napp_dependencies from install NApp. Args: user(string) A Username. napp(string): A NApp name. Returns: napps(list): List with tuples with Username and NApp name. e.g. [('kytos'/'of_core'), ('kytos/of_l2ls')] """ napps = self._get_napp_key('napp_dependencies', user, napp) return [tuple(napp.split('/')) for napp in napps] def get_description(self, user=None, napp=None): """Return the description from kytos.json.""" return self._get_napp_key('description', user, napp) def get_version(self, user=None, napp=None): """Return the version from kytos.json.""" return self._get_napp_key('version', user, napp) or 'latest' def _get_napp_key(self, key, user=None, napp=None): """Return a value from kytos.json. Args: user (string): A Username. napp (string): A NApp name key (string): Key used to get the value within kytos.json. Returns: meta (object): Value stored in kytos.json. """ if user is None: user = self.user if napp is None: napp = self.napp kytos_json = self._installed / user / napp / 'kytos.json' try: with kytos_json.open() as file_descriptor: meta = json.load(file_descriptor) return meta[key] except (FileNotFoundError, json.JSONDecodeError, KeyError): return '' def disable(self): """Disable a NApp if it is enabled.""" enabled = self.enabled_dir() try: enabled.unlink() if self._controller is not None: self._controller.unload_napp(self.user, self.napp) except FileNotFoundError: pass # OK, it was already disabled def enabled_dir(self): """Return the enabled dir from current napp.""" return self._enabled / self.user / self.napp def installed_dir(self): """Return the installed dir from current napp.""" return self._installed / self.user / self.napp def enable(self): """Enable a NApp if not already enabled. Raises: FileNotFoundError: If NApp is not installed. PermissionError: No filesystem permission to enable NApp. """ enabled = self.enabled_dir() installed = self.installed_dir() if not installed.is_dir(): raise FileNotFoundError('Install NApp {} first.'.format( self.napp_id)) elif not enabled.exists(): self._check_module(enabled.parent) try: # Create symlink enabled.symlink_to(installed) if self._controller is not None: self._controller.load_napp(self.user, self.napp) except FileExistsError: pass # OK, NApp was already enabled except PermissionError: raise PermissionError('Permission error on enabling NApp. Try ' 'with sudo.') def is_enabled(self): """Whether a NApp is enabled.""" return (self.user, self.napp) in self.get_enabled() def uninstall(self): """Delete code inside NApp directory, if existent.""" if self.is_installed(): installed = self.installed_dir() if installed.is_symlink(): installed.unlink() else: shutil.rmtree(str(installed)) @staticmethod def valid_name(username): """Check the validity of the given 'name'. The following checks are done: - name starts with a letter - name contains only letters, numbers or underscores """ return username and re.match(r'[a-zA-Z][a-zA-Z0-9_]{2,}$', username) @staticmethod def render_template(templates_path, template_filename, context): """Render Jinja2 template for a NApp structure.""" template_env = Environment(autoescape=False, trim_blocks=False, loader=FileSystemLoader( str(templates_path))) return template_env.get_template(str(template_filename)) \ .render(context) @staticmethod def search(pattern): """Search all server NApps matching pattern. Args: pattern (str): Python regular expression. """ def match(napp): """Whether a NApp metadata matches the pattern.""" # WARNING: This will change for future versions, when 'author' will # be removed. username = napp.get('username', napp.get('author')) strings = [ '{}/{}'.format(username, napp.get('name')), napp.get('description') ] + napp.get('tags') return any(pattern.match(string) for string in strings) napps = NAppsClient().get_napps() return [napp for napp in napps if match(napp)] def install_local(self): """Make a symlink in install folder to a local NApp. Raises: FileNotFoundError: If NApp is not found. """ folder = self._get_local_folder() installed = self.installed_dir() self._check_module(installed.parent) installed.symlink_to(folder.resolve()) def _get_local_folder(self, root=None): """Return local NApp root folder. Search for kytos.json in _./_ folder and _./user/napp_. Args: root (pathlib.Path): Where to begin searching. Return: pathlib.Path: NApp root folder. Raises: FileNotFoundError: If there is no such local NApp. """ if root is None: root = Path() for folders in ['.'], [self.user, self.napp]: kytos_json = root / Path(*folders) / 'kytos.json' if kytos_json.exists(): with kytos_json.open() as file_descriptor: meta = json.load(file_descriptor) # WARNING: This will change in future versions, when # 'author' will be removed. username = meta.get('username', meta.get('author')) if username == self.user and meta.get('name') == self.napp: return kytos_json.parent raise FileNotFoundError('kytos.json not found.') def install_remote(self): """Download, extract and install NApp.""" package, pkg_folder = None, None try: package = self._download() pkg_folder = self._extract(package) napp_folder = self._get_local_folder(pkg_folder) dst = self._installed / self.user / self.napp self._check_module(dst.parent) shutil.move(str(napp_folder), str(dst)) finally: # Delete temporary files if package: Path(package).unlink() if pkg_folder and pkg_folder.exists(): shutil.rmtree(str(pkg_folder)) def _download(self): """Download NApp package from server. Return: str: Downloaded temp filename. Raises: urllib.error.HTTPError: If download is not successful. """ repo = self._config.get('napps', 'repo') napp_id = '{}/{}-{}.napp'.format(self.user, self.napp, self.version) uri = os.path.join(repo, napp_id) return urllib.request.urlretrieve(uri)[0] @staticmethod def _extract(filename): """Extract package to a temporary folder. Return: pathlib.Path: Temp dir with package contents. """ random_string = '{:0d}'.format(randint(0, 10**6)) tmp = '/tmp/kytos-napp-' + Path(filename).stem + '-' + random_string os.mkdir(tmp) with tarfile.open(filename, 'r:xz') as tar: tar.extractall(tmp) return Path(tmp) @classmethod def create_napp(cls): """Bootstrap a basic NApp strucutre for you to develop your NApp. This will create, on the current folder, a clean structure of a NAPP, filling some contents on this structure. """ base = os.environ.get('VIRTUAL_ENV', '/') templates_path = os.path.join(base, 'etc', 'skel', 'kytos', 'napp-structure', 'username', 'napp') username = None napp_name = None description = None print('--------------------------------------------------------------') print('Welcome to the bootstrap process of your NApp.') print('--------------------------------------------------------------') print('In order to answer both the username and the napp name,') print('You must follow this naming rules:') print(' - name starts with a letter') print(' - name contains only letters, numbers or underscores') print(' - at least three characters') print('--------------------------------------------------------------') print('') msg = 'Please, insert your NApps Server username: '******'Please, insert your NApp name: ') msg = 'Please, insert a brief description for your NApp [optional]: ' description = input(msg) if not description: # pylint: disable=fixme description = '# TODO: <<<< Insert here your NApp description >>>>' # pylint: enable=fixme context = { 'username': username, 'napp': napp_name, 'description': description } #: Creating the directory structure (username/napp_name) os.makedirs(username, exist_ok=True) #: Creating ``__init__.py`` files with open(os.path.join(username, '__init__.py'), 'w'): pass os.makedirs(os.path.join(username, napp_name)) with open(os.path.join(username, napp_name, '__init__.py'), 'w'): pass #: Creating the other files based on the templates templates = os.listdir(templates_path) templates.remove('__init__.py') templates.remove('openapi.yml.template') for tmp in templates: fname = os.path.join(username, napp_name, tmp.rsplit('.template')[0]) with open(fname, 'w') as file: content = cls.render_template(templates_path, tmp, context) file.write(content) msg = '\nCongratulations! Your NApp have been bootstrapped!\nNow you ' msg += 'can go to the directory {}/{} and begin to code your NApp.' print(msg.format(username, napp_name)) print('Have fun!') @staticmethod def _check_module(folder): """Create module folder with empty __init__.py if it doesn't exist. Args: folder (pathlib.Path): Module path. """ if not folder.exists(): folder.mkdir(parents=True, exist_ok=True, mode=0o755) (folder / '__init__.py').touch() @staticmethod def build_napp_package(napp_name): """Build the .napp file to be sent to the napps server. Args: napp_identifier (str): Identifier formatted as <username>/<napp_name> Return: file_payload (binary): The binary representation of the napp package that will be POSTed to the napp server. """ ignored_extensions = ['.swp', '.pyc', '.napp'] ignored_dirs = ['__pycache__'] files = os.listdir() for filename in files: if os.path.isfile(filename) and '.' in filename and \ filename.rsplit('.', 1)[1] in ignored_extensions: files.remove(filename) elif os.path.isdir(filename) and filename in ignored_dirs: files.remove(filename) # Create the '.napp' package napp_file = tarfile.open(napp_name + '.napp', 'x:xz') for local_f in files: napp_file.add(local_f) napp_file.close() # Get the binary payload of the package file_payload = open(napp_name + '.napp', 'rb') # remove the created package from the filesystem os.remove(napp_name + '.napp') return file_payload @staticmethod def create_metadata(*args, **kwargs): # pylint: disable=unused-argument """Generate the metadata to send the napp package.""" json_filename = kwargs.get('json_filename', 'kytos.json') readme_filename = kwargs.get('readme_filename', 'README.rst') ignore_json = kwargs.get('ignore_json', False) metadata = {} if not ignore_json: try: with open(json_filename) as json_file: metadata = json.load(json_file) except FileNotFoundError: print("ERROR: Could not access kytos.json file.") sys.exit(1) try: with open(readme_filename) as readme_file: metadata['readme'] = readme_file.read() except FileNotFoundError: metadata['readme'] = '' try: yaml = YAML(typ='safe') openapi_dict = yaml.load(Path('openapi.yml').open()) openapi = json.dumps(openapi_dict) except FileNotFoundError: openapi = '' metadata['OpenAPI_Spec'] = openapi return metadata def upload(self, *args, **kwargs): """Create package and upload it to NApps Server. Raises: FileNotFoundError: If kytos.json is not found. """ self.prepare() metadata = self.create_metadata(*args, **kwargs) package = self.build_napp_package(metadata.get('name')) NAppsClient().upload_napp(metadata, package) def delete(self): """Delete a NApp. Raises: requests.HTTPError: When there's a server error. """ client = NAppsClient(self._config) client.delete(self.user, self.napp) @classmethod def prepare(cls): """Prepare NApp to be uploaded by creating openAPI skeleton.""" if cls._ask_openapi(): napp_path = Path() prefix = Path(sys.prefix) tpl_path = prefix / 'etc/skel/kytos/napp-structure/username/napp' OpenAPI(napp_path, tpl_path).render_template() print('Please, update your openapi.yml file.') sys.exit() @staticmethod def _ask_openapi(): """Return whether we should create a (new) skeleton.""" if Path('openapi.yml').exists(): question = 'Override local openapi.yml with a new skeleton? (y/N) ' default = False else: question = 'Do you have REST endpoints and wish to create an API' \ ' skeleton in openapi.yml? (Y/n) ' default = True while True: answer = input(question) if answer == '': return default if answer.lower() in ['y', 'yes']: return True if answer.lower() in ['n', 'no']: return False
def __init__(self, config=None): """Set Kytos config.""" if config is None: config = KytosConfig().config self._config = config
class NAppsManager: """Deal with NApps at filesystem level and ask Kytos to (un)load NApps.""" _NAPP_ENABLE = "api/kytos/core/napps/{}/{}/enable" _NAPP_DISABLE = "api/kytos/core/napps/{}/{}/disable" _NAPP_INSTALL = "api/kytos/core/napps/{}/{}/install" _NAPP_UNINSTALL = "api/kytos/core/napps/{}/{}/uninstall" _NAPPS_INSTALLED = "api/kytos/core/napps_installed" _NAPPS_ENABLED = "api/kytos/core/napps_enabled" _NAPP_METADATA = "api/kytos/core/napps/{}/{}/metadata/{}" def __init__(self): """Instance a new NAppsManager. This method do not need parameters. """ self._config = KytosConfig().config self._kytos_api = self._config.get('kytos', 'api') self.user = None self.napp = None self.version = None # Automatically get from kytosd API when needed self.__local_enabled = None self.__local_installed = None @property def _enabled(self): if self.__local_enabled is None: self.__require_kytos_config() return self.__local_enabled @property def _installed(self): if self.__local_installed is None: self.__require_kytos_config() return self.__local_installed def __require_kytos_config(self): """Set path locations from kytosd API. It should not be called directly, but from properties that require a running kytosd instance. """ if self.__local_enabled is None: uri = self._kytos_api + 'api/kytos/core/config/' try: ops = json.loads(urllib.request.urlopen(uri).read()) except urllib.error.URLError as err: msg = f'Error connecting to Kytos daemon: {uri} {err.reason}' print(msg) sys.exit(1) self.__local_enabled = pathlib.Path(ops.get('napps')) self.__local_installed = pathlib.Path(ops.get('installed_napps')) def set_napp(self, user, napp, version=None): """Set info about NApp. Args: user (str): NApps Server username. napp (str): NApp name. version (str): NApp version. """ self.user = user self.napp = napp self.version = version or 'latest' @property def napp_id(self): """Return a Identifier of NApp.""" return '/'.join((self.user, self.napp)) @staticmethod def _get_napps(napps_dir): """List of (username, napp_name) found in ``napps_dir``. Ex: [('kytos', 'of_core'), ('kytos', 'of_lldp')] """ jsons = napps_dir.glob('*/*/kytos.json') return sorted(j.parts[-3:-1] for j in jsons) def get_enabled_local(self): """Sorted list of (username, napp_name) of enabled napps.""" return self._get_napps(self._enabled) def get_installed_local(self): """Sorted list of (username, napp_name) of installed napps.""" return self._get_napps(self._installed) def get_enabled(self): """Sorted list of (username, napp_name) of enabled napps.""" uri = self._kytos_api + self._NAPPS_ENABLED try: response = urllib.request.urlopen(uri) if response.getcode() != 200: msg = "Error calling Kytos to check enabled NApps." raise KytosException(msg) content = json.loads(response.read()) return sorted((c[0], c[1]) for c in content['napps']) except urllib.error.URLError as exception: LOG.error("Error checking enabled NApps. Is Kytos running?") raise KytosException(exception) def get_installed(self): """Sorted list of (username, napp_name) of installed napps.""" uri = self._kytos_api + self._NAPPS_INSTALLED try: response = urllib.request.urlopen(uri) if response.getcode() != 200: msg = "Error calling Kytos to check installed NApps." raise KytosException(msg) content = json.loads(response.read()) return sorted((c[0], c[1]) for c in content['napps']) except urllib.error.URLError as exception: LOG.error("Error checking installed NApps. Is Kytos running?") raise KytosException(exception) def is_installed(self): """Whether a NApp is installed.""" return (self.user, self.napp) in self.get_installed() def get_disabled(self): """Sorted list of (username, napp_name) of disabled napps. The difference of installed and enabled. """ installed = set(self.get_installed()) enabled = set(self.get_enabled()) return sorted(installed - enabled) def dependencies(self, user=None, napp=None): """Get napp_dependencies from install NApp. Args: user(string) A Username. napp(string): A NApp name. Returns: napps(list): List with tuples with Username and NApp name. e.g. [('kytos'/'of_core'), ('kytos/of_l2ls')] """ napps = self._get_napp_key('napp_dependencies', user, napp) return [tuple(napp.split('/')) for napp in napps] def get_description(self, user=None, napp=None): """Return the description from kytos.json.""" return self._get_napp_key('description', user, napp) def get_version(self, user=None, napp=None): """Return the version from kytos.json.""" return self._get_napp_key('version', user, napp) or 'latest' def _get_napp_key(self, key, user=None, napp=None): """Return a value from kytos.json. Args: user (string): A Username. napp (string): A NApp name key (string): Key used to get the value within kytos.json. Returns: meta (object): Value stored in kytos.json. """ if user is None: user = self.user if napp is None: napp = self.napp uri = self._kytos_api + self._NAPP_METADATA uri = uri.format(user, napp, key) meta = json.loads(urllib.request.urlopen(uri).read()) return meta[key] def disable(self): """Disable a NApp if it is enabled.""" uri = self._kytos_api + self._NAPP_DISABLE uri = uri.format(self.user, self.napp) try: json.loads(urllib.request.urlopen(uri).read()) except urllib.error.HTTPError as exception: if exception.code == HTTPStatus.BAD_REQUEST.value: LOG.error("NApp is not installed. Check the NApp list.") else: LOG.error("Error disabling the NApp") def enable(self): """Enable a NApp if not already enabled.""" uri = self._kytos_api + self._NAPP_ENABLE uri = uri.format(self.user, self.napp) try: json.loads(urllib.request.urlopen(uri).read()) except urllib.error.HTTPError as exception: if exception.code == HTTPStatus.BAD_REQUEST.value: LOG.error("NApp is not installed. Check the NApp list.") else: LOG.error("Error enabling the NApp") def enabled_dir(self): """Return the enabled dir from current napp.""" return self._enabled / self.user / self.napp def installed_dir(self): """Return the installed dir from current napp.""" return self._installed / self.user / self.napp def is_enabled(self): """Whether a NApp is enabled.""" return (self.user, self.napp) in self.get_enabled() def remote_uninstall(self): """Delete code inside NApp directory, if existent.""" uri = self._kytos_api + self._NAPP_UNINSTALL uri = uri.format(self.user, self.napp) try: json.loads(urllib.request.urlopen(uri).read()) except urllib.error.HTTPError as exception: if exception.code == HTTPStatus.BAD_REQUEST.value: LOG.error("Check if the NApp is installed.") else: LOG.error("Error uninstalling the NApp") @staticmethod def valid_name(username): """Check the validity of the given 'name'. The following checks are done: - name starts with a letter - name contains only letters, numbers or underscores """ return username and re.match(r'[a-zA-Z][a-zA-Z0-9_]{2,}$', username) @staticmethod def render_template(templates_path, template_filename, context): """Render Jinja2 template for a NApp structure.""" template_env = Environment(autoescape=False, trim_blocks=False, loader=FileSystemLoader( str(templates_path))) return template_env.get_template(str(template_filename)) \ .render(context) @staticmethod def search(pattern): """Search all server NApps matching pattern. Args: pattern (str): Python regular expression. """ def match(napp): """Whether a NApp metadata matches the pattern.""" # WARNING: This will change for future versions, when 'author' will # be removed. username = napp.get('username', napp.get('author')) strings = [ '{}/{}'.format(username, napp.get('name')), napp.get('description') ] + napp.get('tags') return any(pattern.match(string) for string in strings) napps = NAppsClient().get_napps() return [napp for napp in napps if match(napp)] def remote_install(self): """Ask kytos server to install NApp.""" uri = self._kytos_api + self._NAPP_INSTALL uri = uri.format(self.user, self.napp) json.loads(urllib.request.urlopen(uri).read()) @classmethod def create_napp(cls, meta_package=False): """Bootstrap a basic NApp structure for you to develop your NApp. This will create, on the current folder, a clean structure of a NAPP, filling some contents on this structure. """ templates_path = SKEL_PATH / 'napp-structure/username/napp' ui_templates_path = os.path.join(templates_path, 'ui') username = None napp_name = None print('--------------------------------------------------------------') print('Welcome to the bootstrap process of your NApp.') print('--------------------------------------------------------------') print('In order to answer both the username and the NApp name,') print('You must follow these naming rules:') print(' - name starts with a letter') print(' - name contains only letters, numbers or underscores') print(' - at least three characters') print('--------------------------------------------------------------') print('') while not cls.valid_name(username): username = input('Please, insert your NApps Server username: '******'Please, insert your NApp name: ') description = input('Please, insert a brief description for your ' 'NApp [optional]: ') if not description: # pylint: disable=fixme description = '# TODO: <<<< Insert your NApp description here >>>>' # pylint: enable=fixme context = { 'username': username, 'napp': napp_name, 'description': description } #: Creating the directory structure (username/napp_name) os.makedirs(username, exist_ok=True) #: Creating ``__init__.py`` files with open(os.path.join(username, '__init__.py'), 'w') as init_file: init_file.write(f'"""NApps for the user {username}.""""') os.makedirs(os.path.join(username, napp_name)) #: Creating the other files based on the templates templates = os.listdir(templates_path) templates.remove('ui') templates.remove('openapi.yml.template') if meta_package: templates.remove('main.py.template') templates.remove('settings.py.template') for tmp in templates: fname = os.path.join(username, napp_name, tmp.rsplit('.template')[0]) with open(fname, 'w') as file: content = cls.render_template(templates_path, tmp, context) file.write(content) if not meta_package: NAppsManager.create_ui_structure(username, napp_name, ui_templates_path, context) print() print(f'Congratulations! Your NApp has been bootstrapped!\n' f'Now you can go to the "{username}/{napp_name}" directory ' f'and begin to code your NApp.') print('Have fun!') @classmethod def create_ui_structure(cls, username, napp_name, ui_templates_path, context): """Create the ui directory structure.""" for section in ['k-info-panel', 'k-toolbar', 'k-action-menu']: os.makedirs(os.path.join(username, napp_name, 'ui', section)) templates = os.listdir(ui_templates_path) for tmp in templates: fname = os.path.join(username, napp_name, 'ui', tmp.rsplit('.template')[0]) with open(fname, 'w') as file: content = cls.render_template(ui_templates_path, tmp, context) file.write(content) @staticmethod def _check_module(folder): """Create module folder with empty __init__.py if it doesn't exist. Args: folder (pathlib.pathlib.Path): Module path. """ if not folder.exists(): folder.mkdir(parents=True, exist_ok=True, mode=0o755) (folder / '__init__.py').touch() @staticmethod def build_napp_package(napp_name): """Build the .napp file to be sent to the napps server. Args: napp_identifier (str): Identifier formatted as <username>/<napp_name> Return: file_payload (binary): The binary representation of the napp package that will be POSTed to the napp server. """ def get_matches(path): """Return all NApp files matching any .gitignore pattern.""" ignored_files = [".git"] with open(".gitignore", 'r') as local_gitignore: ignored_files.extend(local_gitignore.readlines()) user_gitignore_path = pathlib.Path("%s/.gitignore" % pathlib.Path.home()) if user_gitignore_path.exists(): with open(user_gitignore_path, 'r') as user_gitignore: ignored_files.extend(user_gitignore.readlines()) # Define Wildmatch pattern (default gitignore pattern) pattern = pathspec.patterns.GitWildMatchPattern spec = pathspec.PathSpec.from_lines(pattern, ignored_files) # Get tree containing all matching files match_tree = spec.match_tree(path) # Create list with all absolute paths of match tree return ["%s/%s" % (path, match) for match in match_tree] files = [] path = os.getcwd() for dir_file in os.walk(path): dirname, _, arc = dir_file files.extend([os.path.join(dirname, f) for f in arc]) # Allow the user to run `kytos napps upload` from outside the # napp directory. # Filter the files with the napp_name in their path # Example: home/user/napps/kytos/, napp_name = kronos # This filter will get all files from: # home/user/napps/kytos/kronos/* files = list(filter(lambda x: napp_name in x, files)) matches = get_matches(path) for filename in files.copy(): if filename in matches: files.remove(filename) # Create the '.napp' package napp_file = tarfile.open(napp_name + '.napp', 'x:xz') for local_f in files: # Add relative paths instead of absolute paths napp_file.add(pathlib.PurePosixPath(local_f).relative_to(path)) napp_file.close() # Get the binary payload of the package file_payload = open(napp_name + '.napp', 'rb') # remove the created package from the filesystem os.remove(napp_name + '.napp') return file_payload @staticmethod def create_metadata(*args, **kwargs): # pylint: disable=unused-argument """Generate the metadata to send the napp package.""" json_filename = kwargs.get('json_filename', 'kytos.json') readme_filename = kwargs.get('readme_filename', 'README.rst') ignore_json = kwargs.get('ignore_json', False) metadata = {} if not ignore_json: try: with open(json_filename) as json_file: metadata = json.load(json_file) except FileNotFoundError: print("ERROR: Could not access kytos.json file.") sys.exit(1) try: with open(readme_filename) as readme_file: metadata['readme'] = readme_file.read() except FileNotFoundError: metadata['readme'] = '' try: yaml = YAML(typ='safe') openapi_dict = yaml.load(pathlib.Path('openapi.yml').open()) openapi = json.dumps(openapi_dict) except FileNotFoundError: openapi = '' metadata['OpenAPI_Spec'] = openapi return metadata def upload(self, *args, **kwargs): """Create package and upload it to NApps Server. Raises: FileNotFoundError: If kytos.json is not found. """ self.prepare() metadata = self.create_metadata(*args, **kwargs) package = self.build_napp_package(metadata.get('name')) NAppsClient().upload_napp(metadata, package) def delete(self): """Delete a NApp. Raises: requests.HTTPError: When there's a server error. """ client = NAppsClient(self._config) client.delete(self.user, self.napp) @classmethod def prepare(cls): """Prepare NApp to be uploaded by creating openAPI skeleton.""" if cls._ask_openapi(): napp_path = pathlib.Path() tpl_path = SKEL_PATH / 'napp-structure/username/napp' OpenAPI(napp_path, tpl_path).render_template() print('Please, update your openapi.yml file.') sys.exit() @staticmethod def _ask_openapi(): """Return whether we should create a (new) skeleton.""" if pathlib.Path('openapi.yml').exists(): question = 'Override local openapi.yml with a new skeleton? (y/N) ' default = False else: question = 'Do you have REST endpoints and wish to create an API' \ ' skeleton in openapi.yml? (Y/n) ' default = True while True: answer = input(question) if answer == '': return default if answer.lower() in ['y', 'yes']: return True if answer.lower() in ['n', 'no']: return False def reload(self, napps=None): """Reload a NApp or all NApps. Args: napps (list): NApp list to be reloaded. Raises: requests.HTTPError: When there's a server error. """ client = NAppsClient(self._config) client.reload_napps(napps)
def call(subcommand, args): """Call a subcommand passing the args.""" KytosConfig.check_versions() args['<napp>'] = parse_napps(args['<napp>']) func = getattr(NAppsAPI, subcommand) func(args)
def call(subcommand, args): # pylint: disable=unused-argument """Call a subcommand passing the args.""" KytosConfig.check_versions() func = getattr(WebAPI, subcommand) func(args)
def setUp(self): """Execute steps before each tests.""" with tempfile.TemporaryDirectory() as tmp_dir: self.config_file = '{}.kytosrc'.format(tmp_dir) self.kytos_config = KytosConfig(self.config_file)
class kytos_auth: # pylint: disable=invalid-name """Class to be used as decorator to require authentication.""" def __init__(self, func): """Init method. Save the function on the func attribute and bootstrap a new config. """ self.func = func self.config = KytosConfig().config self.cls = None self.obj = None def __call__(self, *args, **kwargs): """Code run when func is called.""" if not (self.config.has_option('napps', 'api') and self.config.has_option('napps', 'repo')): uri = input("Enter the kytos napps server address: ") self.config.set('napps', 'api', os.path.join(uri, 'api', '')) self.config.set('napps', 'repo', os.path.join(uri, 'repo', '')) if not self.config.has_option('auth', 'user'): user = input("Enter the username: "******"""Deal with owner class.""" self.cls = owner self.obj = instance return self.__call__ def authenticate(self): """Check the user authentication.""" endpoint = os.path.join(self.config.get('napps', 'api'), 'auth', '') username = self.config.get('auth', 'user') password = getpass("Enter the password for {}: ".format(username)) response = requests.get(endpoint, auth=(username, password)) if response.status_code != 201: LOG.error(response.content) LOG.error('ERROR: %s: %s', response.status_code, response.reason) sys.exit(1) else: data = response.json() KytosConfig().save_token(username, data.get('hash')) return data.get('hash')
def call(subcommand, args): """Call a subcommand passing the args.""" KytosConfig.check_versions() func = getattr(UsersAPI, subcommand) func(args)