pax_global_header00006660000000000000000000000064131603150120014502gustar00rootroot0000000000000052 comment=cbc4cbb1218a4b90b8d4ecde2ae4d35616080ce8 kytos-utils-2017.2b1/000077500000000000000000000000001316031501200143275ustar00rootroot00000000000000kytos-utils-2017.2b1/.gitignore000066400000000000000000000021621316031501200163200ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class *.egg-info # C extensions *.so *.swp # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ docs/kytos*.rst # PyBuilder target/ # IPython Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule # dotenv .env # virtualenv venv/ ENV/ # Spyder project settings .spyderproject # Rope project settings .ropeproject # Auxilary files to test the controller aux_*.py aux_*.sh kytos-utils-2017.2b1/.scrutinizer.yml000066400000000000000000000004701316031501200175120ustar00rootroot00000000000000checks: python: code_rating: true duplicate_code: true filter: paths: ['kytos/*', 'tests/*'] excluded_paths: - 'kytos/web-ui/*' build: environment: python: 3.6.0 postgresql: false redis: false dependencies: override: - true kytos-utils-2017.2b1/AUTHORS.rst000066400000000000000000000004051316031501200162050ustar00rootroot00000000000000####### Authors ####### - Beraldo Leal - Carlos Eduardo Moreira dos Santos - Diego Rabatone Oliveira - Macártur de Sousa Carvalho kytos-utils-2017.2b1/CHANGELOG.rst000066400000000000000000000066641316031501200163640ustar00rootroot00000000000000########## Change log ########## This is a log of changes made to the *kytos-utils* project. [UNRELEASED] - Under development ******************************** Added ===== Changed ======= Deprecated ========== Removed ======= Fixed ===== Security ======== [2017.2b1] - "chico" beta1 - 2017-09-19 *************************************** Added ===== - Version tags - now NApps fully support the /: format. - Create an OpenAPI skeleton based on NApp's rest decorators. Changed ======= - NApps will now install other NApps listed as dependencies. - Do not require a running kytosd for some commands. - Yala substitutes Pylama as the main linter checker. - Requirements files updated and restructured. Fixed ===== - Some test features. - Some bug fixes. [2017.1] - 'bethania' - 2017-07-06 ********************************** Fixed ===== - NApp skel to match changes in Kytos [2017.1b3] - "bethania" beta3 - 2017-06-16 ****************************************** Added ===== - Commands to enable/disable all installed NApps (`kytos napps all`). Changed ======= - Install and enable NApps based on Kytos instance. `kytos-utils` will request the configuration loaded by kytos before managing NApps. Removed ======= - Support for NApp management whithout a Kytos running instance. Fixed ===== - A few bug fixes. [2017.1b2] - "bethania" beta2 - 2017-05-05 ****************************************** Added ===== - :code:`kytos users register` command can be used to register a new user in the NApps server. - Now under MIT license. Changed ======= - skel templates updated to match changes in logging and kytos.json. - Improved tests and style check for developers, and added continuous integration. Deprecated ========== - kytos.json 'author' attribute is being replaced by 'username' due to context, and is deprecated. It will be removed in future releases. Removed ======= - kytos.json 'long_description' attribute is no longer necessary nor available. The detailed description shall now be in README.rst. Fixed ===== - Now creates the NApps directory structure when it does not exist. - Pypi package is fixed and working. - Several bug fixes. [2017.1b1] - "bethania" beta1 - 2017-03-24 ****************************************** Added ===== - etc/skel files, with templates to create all the necessary NApp files when executing :code:`kytos napps create`. - Command line tool to manage the kytos NApps. A set of commands to help managing NApps. - May now use the command line to: - Create new NApps. - Install NApps created locally or from the NApps server. - Enable/disable installed NApps. - List installed / enabled NApps. - Search for NApps in the NApps server. - Upload NApps to the server. - Help is available for command line tools. Appending :code:`--help` to the end of a command displays useful information about it. Changed ======= - Setup script now installs all the requirements during the setup process. There is no need to worry about them beforehand. - Updated to Python 3.6. - Several bug fixes. - Separate CLI code from NApps code: refactored code to make clear what is related to the command line tools and what is related to the kytos NApps. - Clean and descriptive log messages. Security ======== - Authentication for NApps upload process - there is need for an account in the `NApps server `__ to upload any NApp. kytos-utils-2017.2b1/LICENSE000066400000000000000000000020651316031501200153370ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Kytos Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. kytos-utils-2017.2b1/MANIFEST.in000066400000000000000000000000671316031501200160700ustar00rootroot00000000000000include *.rst include LICENSE recursive-include etc/ * kytos-utils-2017.2b1/README.rst000066400000000000000000000053401316031501200160200ustar00rootroot00000000000000######## Overview ######## |Experimental| |Tag| |Release| |License| |Build| |Coverage| |Quality| This is a command line interface (cli) for `Kytos SDN Platform `_. With these utilities you can interact with Kytos daemon and manage Network Applications (NApps) on your controller. QuickStart ********** Installing ========== We use python3.6. So in order to use this software please install python3.6 into your environment beforehand. We are doing a huge effort to make Kytos and its components available on all common distros. So, we recommend you to download it from your distro repository. But if you are trying to test, develop or just want a more recent version of our software no problem: Download now, the latest release (it still a beta software), from our repository: First you need to clone *kytos-utils* repository: .. code-block:: shell $ git clone https://github.com/kytos/kytos-utils.git After cloning, the installation process is done by standard `setuptools` install procedure: .. code-block:: shell $ cd kytos-utils $ sudo python3.6 setup.py install Usage ***** In order to execute *kytos* command line, please run: .. code-block:: shell $ kytos --help Authors ******* For a complete list of authors, please open ``AUTHORS.rst`` file. Contributing ************ If you want to contribute to this project, please read `Kytos Documentation `__ website. License ******* This software is under *MIT-License*. For more information please read ``LICENSE`` file. .. |Experimental| image:: https://img.shields.io/badge/stability-experimental-orange.svg .. |Tag| image:: https://img.shields.io/github/tag/kytos/kytos-utils.svg :target: https://github.com/kytos/kytos-utils/tags .. |Release| image:: https://img.shields.io/github/release/kytos/kytos-utils.svg :target: https://github.com/kytos/kytos-utils/releases .. |Tests| image:: https://travis-ci.org/kytos/kytos-utils.svg?branch=master :target: https://travis-ci.org/kytos/kytos-utils .. |License| image:: https://img.shields.io/github/license/kytos/kytos-utils.svg :target: https://github.com/kytos/kytos-utils/blob/master/LICENSE .. |Build| image:: https://scrutinizer-ci.com/g/kytos/kytos-utils/badges/build.png?b=master :alt: Build status :target: https://scrutinizer-ci.com/g/kytos/kytos-utils/?branch=master .. |Coverage| image:: https://scrutinizer-ci.com/g/kytos/kytos-utils/badges/coverage.png?b=master :alt: Code coverage :target: https://scrutinizer-ci.com/g/kytos/kytos-utils/?branch=master .. |Quality| image:: https://scrutinizer-ci.com/g/kytos/kytos-utils/badges/quality-score.png?b=master :alt: Code-quality score :target: https://scrutinizer-ci.com/g/kytos/kytos-utils/?branch=master kytos-utils-2017.2b1/bin/000077500000000000000000000000001316031501200150775ustar00rootroot00000000000000kytos-utils-2017.2b1/bin/kytos000077500000000000000000000026061316031501200162020ustar00rootroot00000000000000#!/usr/bin/env python3 # This file is part of kytos-utils. # # Copyright (c) 2016-2017 by Kytos Team. # # Authors: # Beraldo Leal """kytos - The kytos command line. Usage: kytos [-c |--config ] [...] kytos [-v|--version] kytos [-h|--help] Options: -c , --config Load config file [default: ~/.kytosrc] -h, --help Show this screen. -v, --version Show version. The most commonly used kytos commands are: napps Create, list, enable, install (and other actions) NApps. server Start, Stop your Kytos Controller (Kytos) See 'kytos -h|--help' for more information on a specific command. """ import logging from docopt import docopt logging.basicConfig(format='%(levelname)-5s %(message)s', level=logging.INFO) if __name__ == '__main__': args = docopt(__doc__, version='kytos command line, version 2017.2b1', options_first=True) command = args[''] command_args = args[''] argv = [command] + command_args if command == 'napps': from kytos.cli.commands.napps.parser import parse parse(argv) elif command == 'users': from kytos.cli.commands.users.parser import parse parse(argv) else: print("Error: Invalid syntax") exit(__doc__) kytos-utils-2017.2b1/etc/000077500000000000000000000000001316031501200151025ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/000077500000000000000000000000001316031501200160405ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/kytos/000077500000000000000000000000001316031501200172115ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/000077500000000000000000000000001316031501200222055ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/000077500000000000000000000000001316031501200240245ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/__init__.py000066400000000000000000000000001316031501200261230ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/napp/000077500000000000000000000000001316031501200247625ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/napp/README.rst.template000066400000000000000000000000751316031501200302650ustar00rootroot00000000000000Overview ======== {{description}} Requirements ============ kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/napp/__init__.py000066400000000000000000000000001316031501200270610ustar00rootroot00000000000000kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/napp/kytos.json.template000066400000000000000000000003251316031501200306400ustar00rootroot00000000000000{ "author": "{{username}}", "username": "{{username}}", "name": "{{napp}}", "description": "{{description}}", "version": "latest", "napp_dependencies": [], "license": "", "tags": [], "url": "" } kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/napp/main.py.template000066400000000000000000000020761316031501200300770ustar00rootroot00000000000000"""Main module of {{username}}/{{napp}} Kytos Network Application. {{ description }} """ from kytos.core import KytosNApp, log from napps.{{username}}.{{napp}} import settings class Main(KytosNApp): """Main class of {{username}}/{{napp}} NApp. This class is the entry point for this napp. """ def setup(self): """Replace the '__init__' method for the KytosNApp subclass. The setup method is automatically called by the controller when your application is loaded. So, if you have any setup routine, insert it here. """ pass def execute(self): """This method is executed right after the setup method execution. You can also use this method in loop mode if you add to the above setup method a line like the following example: self.execute_as_loop(30) # 30-second interval. """ pass def shutdown(self): """This method is executed when your napp is unloaded. If you have some cleanup procedure, insert it here. """ pass kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/napp/openapi.yml.template000066400000000000000000000030071316031501200307520ustar00rootroot00000000000000openapi: 3.0.0 info: title: {{napp.username}}/{{napp.name}} version: {{napp.version}} description: {{napp.description}} paths: {% for path, methods in paths.items() %} {{path}}: {% for method, method_info in methods.items() %} {{method}}: summary: {{method_info.summary}} description: {{method_info.description}} parameters: # If you have parameters in the URL - name: Parameter's name as in path. required: true description: Describe parameter here in: path {% if method == "post" %} requestBody: content: application/json: schema: properties: # What the user should post dpid: # "dpid" is just an example. Replace it. type: string description: Switch datapath ID. example: 00:...:01 {% endif %} responses: 200: # You can add more responses description: Describe a successful call. content: application/json: # You can also use text/plain, for example schema: type: object # Adapt to your response properties: prop_one: type: string description: Meaning of prop_one example: an example of prop_one second_prop: type: integer description: Meaning of second_prop example: 42 {% endfor %} {% endfor %} kytos-utils-2017.2b1/etc/skel/kytos/napp-structure/username/napp/settings.py.template000066400000000000000000000000001316031501200307740ustar00rootroot00000000000000kytos-utils-2017.2b1/kytos/000077500000000000000000000000001316031501200155005ustar00rootroot00000000000000kytos-utils-2017.2b1/kytos/__init__.py000066400000000000000000000001451316031501200176110ustar00rootroot00000000000000"""Kytos SDN Platform.""" from pkgutil import extend_path __path__ = extend_path(__path__, __name__) kytos-utils-2017.2b1/kytos/cli/000077500000000000000000000000001316031501200162475ustar00rootroot00000000000000kytos-utils-2017.2b1/kytos/cli/__init__.py000066400000000000000000000000241316031501200203540ustar00rootroot00000000000000"""CLI commands.""" kytos-utils-2017.2b1/kytos/cli/commands/000077500000000000000000000000001316031501200200505ustar00rootroot00000000000000kytos-utils-2017.2b1/kytos/cli/commands/__init__.py000066400000000000000000000000321316031501200221540ustar00rootroot00000000000000"""Basic CLI commands.""" kytos-utils-2017.2b1/kytos/cli/commands/napps/000077500000000000000000000000001316031501200211715ustar00rootroot00000000000000kytos-utils-2017.2b1/kytos/cli/commands/napps/__init__.py000066400000000000000000000000321316031501200232750ustar00rootroot00000000000000"""NAPPS CLI Commands.""" kytos-utils-2017.2b1/kytos/cli/commands/napps/api.py000066400000000000000000000206761316031501200223270ustar00rootroot00000000000000"""Translate cli commands to non-cli code.""" import json import logging import os import re from urllib.error import HTTPError, URLError import requests from kytos.utils.napps import NAppsManager LOG = logging.getLogger(__name__) class NAppsAPI: """An API for the command-line interface. Use the config file only for required options. Static methods are called by the parser and they instantiate an object of this class to fulfill the request. """ @classmethod def disable(cls, args): """Disable subcommand.""" mgr = NAppsManager() if args['all']: napps = mgr.get_enabled() else: napps = args[''] for napp in napps: mgr.set_napp(*napp) LOG.info('NApp %s:', mgr.napp_id) cls.disable_napp(mgr) @staticmethod def disable_napp(mgr): """Disable a NApp.""" if mgr.is_enabled(): LOG.info(' Disabling...') mgr.disable() LOG.info(' Disabled.') @classmethod def enable(cls, args): """Enable subcommand.""" mgr = NAppsManager() if args['all']: napps = mgr.get_disabled() else: napps = args[''] cls.enable_napps(napps) @classmethod def enable_napp(cls, mgr): """Install one NApp using NAppManager object.""" try: if not mgr.is_enabled(): LOG.info(' Enabling...') mgr.enable() LOG.info(' Enabled.') cls.enable_napps(mgr.dependencies()) except (FileNotFoundError, PermissionError) as exception: LOG.error(' %s', exception) @classmethod def enable_napps(cls, napps): """Enable a list of NApps. Args: napps (list): List of NApps. """ mgr = NAppsManager() for napp in napps: mgr.set_napp(*napp) LOG.info('NApp %s:', mgr.napp_id) cls.enable_napp(mgr) @classmethod def create(cls, args): # pylint: disable=unused-argument """Bootstrap a basic NApp structure on the current folder.""" NAppsManager.create_napp() @classmethod def upload(cls, args): # pylint: disable=unused-argument """Upload the NApp to the NApps server. Create the NApp package and upload it to the NApp server. """ try: NAppsManager().upload() except FileNotFoundError: LOG.error("Couldn't find kytos.json in current directory.") @classmethod def uninstall(cls, args): """Uninstall and delete NApps. For local installations, do not delete code outside install_path and enabled_path. """ mgr = NAppsManager() for napp in args['']: mgr.set_napp(*napp) LOG.info('NApp %s:', mgr.napp_id) if mgr.is_installed(): if mgr.is_enabled(): cls.disable_napp(mgr) LOG.info(' Uninstalling...') mgr.uninstall() LOG.info(' Uninstalled.') @classmethod def install(cls, args): """Install local or remote NApps.""" cls.install_napps(args['']) @classmethod def install_napps(cls, napps): """Install local or remote NApps. This method is recursive, it will install each napps and your dependencies. """ mgr = NAppsManager() for napp in napps: mgr.set_napp(*napp) LOG.info('NApp %s:', mgr.napp_id) if not mgr.is_installed(): cls.install_napp(mgr) else: LOG.info(' Installed.') cls.enable_napp(mgr) napp_dependencies = mgr.dependencies() if napp_dependencies: LOG.info('Installing Dependencies:') cls.install_napps(napp_dependencies) @classmethod def install_napp(cls, mgr): """Install a NApp.""" try: LOG.info(' Searching local NApp...') mgr.install_local() LOG.info(' Found and installed.') cls.enable_napp(mgr) except FileNotFoundError: LOG.info(' Not found. Downloading from NApps Server...') try: mgr.install_remote() LOG.info(' Downloaded and installed.') cls.enable_napp(mgr) except HTTPError as exception: if exception.code == 404: LOG.error(' NApp not found.') else: LOG.error(' NApps Server error: %s', exception) except URLError as exception: LOG.error(' NApps Server error: %s', str(exception.reason)) @classmethod def search(cls, args): """Search for NApps in NApps server matching a pattern.""" safe_shell_pat = re.escape(args['']).replace(r'\*', '.*') pat_str = '.*{}.*'.format(safe_shell_pat) pattern = re.compile(pat_str, re.IGNORECASE) remote_json = NAppsManager.search(pattern) remote = set() for napp in remote_json: # WARNING: This will be changed in future versions, when 'author' # will be removed. username = napp.get('username', napp.get('author')) remote.add(((username, napp.get('name')), napp.get('description'))) cls._print_napps(remote) @classmethod def _print_napps(cls, napp_list): """Format the NApp list to be printed.""" mgr = NAppsManager() enabled = mgr.get_enabled() installed = mgr.get_installed() napps = [] for napp, desc in sorted(napp_list): status = 'i' if napp in installed else '-' status += 'e' if napp in enabled else '-' status = '[{}]'.format(status) name = '{}/{}'.format(*napp) napps.append((status, name, desc)) cls.print_napps(napps) @classmethod def list(cls, args): # pylint: disable=unused-argument """List all installed NApps and inform whether they are enabled.""" mgr = NAppsManager() # Add status napps = [napp + ('[ie]',) for napp in mgr.get_enabled()] napps += [napp + ('[i-]',) for napp in mgr.get_disabled()] # Sort, add description and reorder columns napps.sort() napps_ordered = [] for user, name, status in napps: description = mgr.get_description(user, name) version = mgr.get_version(user, name) napp_id = f'{user}/{name}' if version: napp_id += f':{version}' napps_ordered.append((status, napp_id, description)) cls.print_napps(napps_ordered) @staticmethod def print_napps(napps): """Print status, name and description.""" if not napps: print('No NApps found.') return stat_w = 6 # We already know the size of Status col name_w = max(len(n[1]) for n in napps) desc_w = max(len(n[2]) for n in napps) term_w = os.popen('stty size', 'r').read().split()[1] remaining = int(term_w) - stat_w - name_w - 6 desc_w = min(desc_w, remaining) widths = (stat_w, name_w, desc_w) header = '\n{:^%d} | {:^%d} | {:^%d}' % widths row = '{:^%d} | {:<%d} | {:<%d}' % widths print(header.format('Status', 'NApp ID', 'Description')) print('=+='.join('=' * w for w in widths)) for user, name, desc in napps: desc = (desc[:desc_w-3] + '...') if len(desc) > desc_w else desc print(row.format(user, name, desc)) print('\nStatus: (i)nstalled, (e)nabled\n') @staticmethod def delete(args): """Delete NApps from server.""" mgr = NAppsManager() for napp in args['']: mgr.set_napp(*napp) LOG.info('Deleting NApp %s from server...', mgr.napp_id) try: mgr.delete() LOG.info(' Deleted.') except requests.HTTPError as exception: if exception.response.status_code == 405: LOG.error('Delete Napp is not allowed yet.') else: msg = json.loads(exception.response.content) LOG.error(' Server error: %s - ', msg['error']) @classmethod def prepare(cls, args): """Create OpenAPI v3.0 spec skeleton.""" mgr = NAppsManager() mgr.prepare() kytos-utils-2017.2b1/kytos/cli/commands/napps/parser.py000066400000000000000000000065431316031501200230470ustar00rootroot00000000000000"""kytos - The kytos command line. You are at the "napps" command. Usage: kytos napps create kytos napps prepare kytos napps upload kytos napps delete ... kytos napps list kytos napps install ... kytos napps uninstall ... kytos napps enable (all| ...) kytos napps disable (all| ...) kytos napps search kytos napps -h | --help Options: -h, --help Show this screen. Common napps subcommands: create Create a bootstrap NApp structure for development. prepare Prepare NApp to be uploaded (called by "upload"). upload Upload current NApp to Kytos repository. delete Delete NApps from NApps Server. list List all NApps installed into your system. install Install a local or remote NApp into a controller. uninstall Remove a NApp from your controller. enable Enable a installed NApp. disable Disable a NApp. search Search for NApps in NApps Server. """ import re import sys from docopt import docopt from kytos.cli.commands.napps.api import NAppsAPI from kytos.utils.exceptions import KytosException def parse(argv): """Parse cli args.""" args = docopt(__doc__, argv=argv) try: call(sys.argv[2], args) except KytosException as exception: print("Error parsing args: {}".format(exception)) exit() def call(subcommand, args): """Call a subcommand passing the args.""" args[''] = parse_napps(args['']) func = getattr(NAppsAPI, subcommand) func(args) def parse_napps(napp_ids): """Return a list of tuples with username, napp_name and version. napp_ids elements are of the form username/name[:version] (version is optional). If no version is found, it will be None. If napp_ids is equal to 'all', this string will be returned. Args: napp_ids (list): NApps from the cli. Return: list: list of tuples with (username, napp_name, version). Raises: KytosException: If a NApp has not the form _username/name_. """ if 'all' in napp_ids: return 'all' return [parse_napp(napp_id) for napp_id in napp_ids] def parse_napp(napp_id): """Convert a napp_id in tuple with username, napp name and version. Args: napp_id: String with the form 'username/napp[:version]' (version is optional). If no version is found, it will be None. Returns: tuple: A tuple with (username, napp, version) Raises: KytosException: If a NApp has not the form _username/name_. """ # `napp_id` regex, composed by two mandatory parts (username, napp_name) # and one optional (version). # username and napp_name need to start with a letter, are composed of # letters, numbers and uderscores and must have at least three characters. # They are separated by a colon. # version is optional and can take any format. Is is separated by a hyphen, # if a version is defined. regex = r'([a-zA-Z][a-zA-Z0-9_]{2,})/([a-zA-Z][a-zA-Z0-9_]{2,}):?(.+)?' compiled_regex = re.compile(regex) matched = compiled_regex.fullmatch(napp_id) if not matched: msg = '"{}" NApp has not the form username/napp_name[:version].' raise KytosException(msg.format(napp_id)) return matched.groups() kytos-utils-2017.2b1/kytos/cli/commands/users/000077500000000000000000000000001316031501200212115ustar00rootroot00000000000000kytos-utils-2017.2b1/kytos/cli/commands/users/__init__.py000066400000000000000000000000321316031501200233150ustar00rootroot00000000000000"""USERS CLI Commands.""" kytos-utils-2017.2b1/kytos/cli/commands/users/api.py000066400000000000000000000011721316031501200223350ustar00rootroot00000000000000"""Translate cli commands to non-cli code.""" import logging from kytos.utils.users import UsersManager LOG = logging.getLogger(__name__) class UsersAPI: """An API for the command-line interface. Use the config file only for required options. Static methods are called by the parser and they instantiate an object of this class to fulfill the request. """ user_manager = UsersManager() @classmethod def register(cls, args): # pylint: disable=unused-argument """Create a new user and register it on the Napps server.""" result = cls.user_manager.register() print(result) kytos-utils-2017.2b1/kytos/cli/commands/users/parser.py000066400000000000000000000014251316031501200230610ustar00rootroot00000000000000"""kytos - The kytos command line. You are at the "users" command. Usage: kytos users register kytos users -h | --help Options: -h, --help Show this screen. Common user subcommands: create Register a new user to upload napps to Napps Server. """ import sys from docopt import docopt from kytos.cli.commands.users.api import UsersAPI from kytos.utils.exceptions import KytosException def parse(argv): """Parse cli args.""" args = docopt(__doc__, argv=argv) try: call(sys.argv[2], args) except KytosException as exception: print("Error parsing args: {}".format(exception)) exit() def call(subcommand, args): """Call a subcommand passing the args.""" func = getattr(UsersAPI, subcommand) func(args) kytos-utils-2017.2b1/kytos/utils/000077500000000000000000000000001316031501200166405ustar00rootroot00000000000000kytos-utils-2017.2b1/kytos/utils/__init__.py000066400000000000000000000000371316031501200207510ustar00rootroot00000000000000"""Utility modules for CLI.""" kytos-utils-2017.2b1/kytos/utils/client.py000066400000000000000000000103511316031501200204700ustar00rootroot00000000000000"""REST communication with NApps Server.""" # This file is part of kytos-utils. # # Copyright (c) 2016 by Kytos Team. # # Authors: # Beraldo Leal import json import logging import os import sys import requests from kytos.utils.config import KytosConfig from kytos.utils.decorators import kytos_auth from kytos.utils.exceptions import KytosException LOG = logging.getLogger(__name__) class CommonClient: """Generic class used to make request the Napss server.""" def __init__(self, config=None): """Set Kytos config.""" if config is None: config = KytosConfig().config self._config = config @staticmethod def make_request(endpoint, **kwargs): """Send a request to server.""" data = kwargs.get('json', []) package = kwargs.get('package', None) method = kwargs.get('method', 'GET') function = getattr(requests, method.lower()) try: if package: response = function(endpoint, data=data, files={'file': package}) else: response = function(endpoint, json=data) except requests.exceptions.ConnectionError: LOG.error("Couldn't connect to NApps server %s.", endpoint) sys.exit(1) return response class NAppsClient(CommonClient): """Client for the NApps Server.""" def get_napps(self): """Get all NApps from the server.""" endpoint = os.path.join(self._config.get('napps', 'api'), 'napps', '') res = self.make_request(endpoint) if res.status_code != 200: msg = 'Error getting NApps from server (%s) - %s' LOG.error(msg, res.status_code, res.reason) sys.exit(1) return json.loads(res.content.decode('utf-8'))['napps'] def get_napp(self, username, name): """Return napp metadata or None if not found.""" endpoint = os.path.join(self._config.get('napps', 'api'), 'napps', username, name, '') res = self.make_request(endpoint) if res.status_code == 404: # We need to know if NApp is not found return None elif res.status_code != 200: raise KytosException('Error getting %s/%s from server: (%d) - %s', username, name, res.status_code, res.reason) return json.loads(res.content) @kytos_auth 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)) @kytos_auth def delete(self, username, napp): """Delete a NApp. Raises: requests.HTTPError: If 400 <= status < 600. """ api = self._config.get('napps', 'api') endpoint = os.path.join(api, 'napps', username, napp, '') content = {'token': self._config.get('auth', 'token')} response = self.make_request(endpoint, json=content, method='DELETE') response.raise_for_status() class UsersClient(CommonClient): """Client for the NApps Server.""" def register(self, user_dict): """Send an user_dict to NApps server using POST request. Args: user_dict(dict): Dictionary with user attributes. Returns: result(string): Return the response of Napps server. """ endpoint = os.path.join(self._config.get('napps', 'api'), 'users', '') res = self.make_request(endpoint, method='POST', json=user_dict) return res.content.decode('utf-8') kytos-utils-2017.2b1/kytos/utils/config.py000066400000000000000000000111541316031501200204610ustar00rootroot00000000000000"""Kytos utils configuration.""" # This file is part of kytos-utils. # # Copyright (c) 2016 Kytos Team # # Authors: # Beraldo Leal import logging import os from collections import namedtuple from configparser import ConfigParser LOG = logging.getLogger(__name__) class KytosConfig(): """Kytos Configs. Read the config file for kytos utils and/or request data for the user in order to get the correct paths and links. """ def __init__(self, config_file='~/.kytosrc'): """Init method. Receive the confi_file as argument. """ self.config_file = os.path.expanduser(config_file) self.debug = False if self.debug: LOG.setLevel(logging.DEBUG) # allow_no_value=True is used to keep the comments on the config file. self.config = ConfigParser(allow_no_value=True) # Parse the config file. If no config file was found, then create some # default sections on the config variable. self.config.read(self.config_file) self.check_sections(self.config) self.set_env_or_defaults() if not os.path.exists(self.config_file): LOG.warning("Config file %s not found.", self.config_file) LOG.warning("Creating a new empty config file.") with open(self.config_file, 'w') as output_file: os.chmod(self.config_file, 0o0600) self.config.write(output_file) def log_configs(self): """Log the read configs if debug is enabled.""" for sec in self.config.sections(): LOG.debug(' %s: %s', sec, self.config.options(sec)) def set_env_or_defaults(self): """Read some environment variables and set them on the config. If no environment variable is found and the config section/key is empty, then set some default values. """ option = namedtuple('Option', ['section', 'name', 'env_var', 'default_value']) options = [option('auth', 'user', 'NAPPS_USER', None), option('auth', 'token', 'NAPPS_TOKEN', None), option('napps', 'api', 'NAPPS_API_URI', 'https://napps.kytos.io/api/'), option('napps', 'repo', 'NAPPS_REPO_URI', 'https://napps.kytos.io/repo'), option('kytos', 'api', 'KYTOS_API', 'http://localhost:8181/')] for option in options: if not self.config.has_option(option.section, option.name): env_value = os.environ.get(option.env_var, option.default_value) if env_value: self.config.set(option.section, option.name, env_value) self.config.set('global', 'debug', str(self.debug)) @staticmethod def check_sections(config): """Create a empty config file.""" default_sections = ['global', 'auth', 'napps', 'kytos'] for section in default_sections: if not config.has_section(section): config.add_section(section) def save_token(self, user, token): """Save the token on the config file.""" self.config.set('auth', 'user', user) self.config.set('auth', 'token', token) # allow_no_value=True is used to keep the comments on the config file. new_config = ConfigParser(allow_no_value=True) # Parse the config file. If no config file was found, then create some # default sections on the config variable. new_config.read(self.config_file) self.check_sections(new_config) new_config.set('auth', 'user', user) new_config.set('auth', 'token', token) filename = os.path.expanduser(self.config_file) with open(filename, 'w') as out_file: os.chmod(filename, 0o0600) new_config.write(out_file) def clear_token(self): """Clear Token information on config file.""" # allow_no_value=True is used to keep the comments on the config file. new_config = ConfigParser(allow_no_value=True) # Parse the config file. If no config file was found, then create some # default sections on the config variable. new_config.read(self.config_file) self.check_sections(new_config) new_config.remove_option('auth', 'user') new_config.remove_option('auth', 'token') filename = os.path.expanduser(self.config_file) with open(filename, 'w') as out_file: os.chmod(filename, 0o0600) new_config.write(out_file) kytos-utils-2017.2b1/kytos/utils/decorators.py000066400000000000000000000051521316031501200213620ustar00rootroot00000000000000"""Decorators for Kytos-utils.""" import logging import os import sys from getpass import getpass import requests from kytos.utils.config import KytosConfig LOG = logging.getLogger(__name__) # This class is used as decorator, so this class name is lowercase and the # invalid-name warning from pylint is disabled below. 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: ") self.config.set('auth', 'user', user) else: user = self.config.get('auth', 'user') if not self.config.has_option('auth', 'token'): token = self.authenticate() else: token = self.config.get('auth', 'token') # Ignore private attribute warning. We don't wanna make it public only # because of a decorator. config = self.obj._config # pylint: disable=protected-access config.set('auth', 'user', user) config.set('auth', 'token', token) self.func.__call__(self.obj, *args, **kwargs) def __get__(self, instance, owner): """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') kytos-utils-2017.2b1/kytos/utils/exceptions.py000066400000000000000000000001471316031501200213750ustar00rootroot00000000000000"""Kytos utils exceptions.""" class KytosException(Exception): """Kytos utils main exception.""" kytos-utils-2017.2b1/kytos/utils/napps.py000066400000000000000000000457571316031501200203550ustar00rootroot00000000000000"""Manage Network Application files.""" import json import logging import os import re import shutil import sys import tarfile import urllib from pathlib import Path from random import randint from jinja2 import Environment, FileSystemLoader from ruamel.yaml import YAML from kytos.utils.client import NAppsClient from kytos.utils.config import KytosConfig from kytos.utils.openapi import OpenAPI LOG = logging.getLogger(__name__) # pylint: disable=too-many-instance-attributes,too-many-public-methods 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): """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): """Method used to 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): """Generic method used to 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. Raises: FileNotFoundError: If there is no such local NApp. Return: pathlib.Path: NApp root folder. """ 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. Raises: urllib.error.HTTPError: If download is not successful. Return: str: Downloaded temp filename. """ 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: ' while not cls.valid_name(username): username = input(msg) while not cls.valid_name(napp_name): napp_name = input('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') 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 / 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 # pylint: enable=too-many-instance-attributes,too-many-public-methods kytos-utils-2017.2b1/kytos/utils/openapi.py000066400000000000000000000124271316031501200206530ustar00rootroot00000000000000"""Deal with OpenAPI v3.""" import json import re from jinja2 import Environment, FileSystemLoader from kytos.core.api_server import APIServer from kytos.core.napps.base import NApp class OpenAPI: # pylint: disable=too-few-public-methods """Create OpenAPI skeleton.""" def __init__(self, napp_path, tpl_path): self._napp_path = napp_path self._template = tpl_path / 'openapi.yml.template' self._api_file = napp_path / 'openapi.yml' metadata = napp_path / 'kytos.json' self._napp = NApp.create_from_json(metadata) # Data for a path self._summary = None self._description = None # Part of template context self._paths = {} def render_template(self): """Render and save API doc in openapi.yml.""" self._parse_paths() context = dict(napp=self._napp.__dict__, paths=self._paths) self._save(context) def _parse_paths(self): main_file = self._napp_path / 'main.py' code = main_file.open().read() return self._parse_decorated_functions(code) def _parse_decorated_functions(self, code): """Return URL rule, HTTP methods and docstring.""" matches = re.finditer(r""" # @rest decorators (?P (?:@rest\(.+?\)\n)+ # one or more @rest decorators inside ) # docstring delimited by 3 double quotes .+?"{3}(?P.+?)"{3} """, code, re.VERBOSE | re.DOTALL) for function_match in matches: m_dict = function_match.groupdict() self._parse_docstring(m_dict['docstring']) self._add_function_paths(m_dict['decorators']) def _add_function_paths(self, decorators_str): for rule, parsed_methods in self._parse_decorators(decorators_str): absolute_rule = APIServer.get_absolute_rule(rule, self._napp) path_url = self._rule2path(absolute_rule) path_methods = self._paths.setdefault(path_url, {}) self._add_methods(parsed_methods, path_methods) def _parse_docstring(self, docstring): """Parse the method docstring.""" match = re.match(r""" # Following PEP 257 \s* (?P[^\n]+?) \s* # First line ( # Description and YAML are optional (\n \s*){2} # Blank line # Description (optional) ( (?!-{3,}) # Don't use YAML as description \s* (?P.+?) \s* # Third line and maybe others (?=-{3,})? # Stop if "---" is found )? # YAML spec (optional) **currently not used** ( -{3,}\n # "---" begins yaml spec (?P.+) )? )? $""", docstring, re.VERBOSE | re.DOTALL) summary = 'TODO write the summary.' description = 'TODO write/remove the description' if match: m_dict = match.groupdict() summary = m_dict['summary'] if m_dict['description']: description = re.sub(r'(\s|\n){2,}', ' ', m_dict['description']) self._summary = summary self._description = description def _parse_decorators(self, decorators_str): matches = re.finditer(r""" @rest\( ## Endpoint rule (?P['"]) # inside single or double quotes (?P.+?) (?P=quote) ## HTTP methods (optional) (\s*,\s* methods=(?P\[.+?\]) )? .*?\)\s*$ """, decorators_str, re.VERBOSE) for match in matches: rule = match.group('rule') methods = self._parse_methods(match.group('methods')) yield rule, methods @classmethod def _parse_methods(cls, list_string): """Return HTTP method list. Use json for security reasons.""" if list_string is None: return APIServer.DEFAULT_METHODS # json requires double quotes json_list = list_string.replace("'", '"') return json.loads(json_list) def _add_methods(self, methods, path_methods): for method in methods: path_method = dict(summary=self._summary, description=self._description) path_methods[method.lower()] = path_method @classmethod def _rule2path(cls, rule): """Convert relative Flask rule to absolute OpenAPI path.""" typeless = re.sub(r'<\w+?:', '<', rule) # remove Flask types return typeless.replace('<', '{').replace('>', '}') # <> -> {} def _read_napp_info(self): filename = self._napp_path / 'kytos.json' return json.load(filename.open()) def _save(self, context): tpl_env = Environment( loader=FileSystemLoader(str(self._template.parent)), trim_blocks=True) content = tpl_env.get_template( 'openapi.yml.template').render(context) with self._api_file.open('w') as openapi: openapi.write(content) kytos-utils-2017.2b1/kytos/utils/users.py000066400000000000000000000115471316031501200203630ustar00rootroot00000000000000"""Module used to handle Users in Napps Server.""" import logging import re from getpass import getpass from kytos.utils.client import UsersClient LOG = logging.getLogger(__name__) NAME_PATTERN = ("\t- insert only letters", r'[a-zA-Z][a-zA-Z]{2,}$') USERNAME_PATTERN = ("\t- start with letter\n" "\t- insert only numbers and letters", r'[a-zA-Z][a-zA-Z0-9_]{2,}$') PASSWORD_PATTERN = ("\t- insert only the caracters:" " [letters, numbers, _, %, &, -, $]" "\n\t- must be at least 6 characters", r'[a-zA-Z0-9_%\-&$]{6,}$') EMAIL_PATTERN = ("\t- follow the format: @\n" "\t\te.g. john@test.com", r'[^@]+@[^@]+\.[^@]+') PHONE_PATTERN = ("\t- insert only numbers", r'\d*$') class UsersManager: """Class used to handle users stored by Napps server.""" attributes = { "username": {"field_name": "Username (Required)", "pattern": USERNAME_PATTERN}, "first_name": {"field_name": "First Name (Required)", "pattern": NAME_PATTERN}, "last_name": {"field_name": "Last Name", "pattern": NAME_PATTERN}, "password": {"field_name": "Password (Required)", "pattern": PASSWORD_PATTERN}, "email": {"field_name": "Email (Required)", "pattern": EMAIL_PATTERN}, "phone": {"field_name": "Phone", "pattern": PHONE_PATTERN}, "city": {"field_name": "City", "pattern": NAME_PATTERN}, "state": {"field_name": "State", "pattern": NAME_PATTERN}, "country": {"field_name": "Country", "pattern": NAME_PATTERN} } required = ["username", "first_name", "password", "email"] def __init__(self): """Constructor of UsersManager do not need parameters.""" self._users_client = UsersClient() def register(self): """Method used to register a new user. This method will ask for user attributes and create the user in Napps server, when All required fields is filled. Returns: result(string): Response of user registration process. """ user = {} print('--------------------------------------------------------------') print('Welcome to the user registration process.') print('--------------------------------------------------------------') print("To continue you must fill the following fields.") for attribute, value in self.attributes.items(): is_required = attribute in self.required field_name = value['field_name'] pattern = value['pattern'] if attribute != 'password': user[attribute] = self.ask_question(field_name, pattern, is_required) else: user[attribute] = self.ask_question(field_name, pattern, password=True) return self._users_client.register(user) def ask_question(self, field_name, pattern=NAME_PATTERN, is_required=False, password=False): """Method used to ask a question and get the input values. This method will validade the input values. Args: field_name(string): Field name used to ask for input value. pattern(tuple): Pattern to validate the input value. is_required(bool): Boolean value if the input value is required. password(bool): Boolean value to get input password with mask. Returns: input_value(string): Input value validated. """ input_value = "" question = ("Insert the field using the pattern below:" "\n{}\n{}: ".format(pattern[0], field_name)) while not input_value: input_value = getpass(question) if password else input(question) if not (input_value or is_required): break if password: confirm_password = getpass('Confirm your password: ') if confirm_password != input_value: print("Password does not match") input_value = "" if not self.valid_attribute(input_value, pattern[1]): error_message = "The content must fit the pattern: {}\n" print(error_message.format(pattern[0])) input_value = "" return input_value @classmethod def valid_attribute(cls, attribute, pattern): """Check the validity of the given 'attribute' using the given pattern. Args: attribute(string): String with the value of an attribute pattern(string): Pattern used to validate the attribute value. Returns: pattern_found(bool): Return True if the pattern match. """ return attribute and re.match(pattern, attribute) kytos-utils-2017.2b1/requirements/000077500000000000000000000000001316031501200170525ustar00rootroot00000000000000kytos-utils-2017.2b1/requirements/dev.in000066400000000000000000000000121316031501200201510ustar00rootroot00000000000000-e .[dev] kytos-utils-2017.2b1/requirements/dev.txt000066400000000000000000000023641316031501200203760ustar00rootroot00000000000000# # This file is autogenerated by pip-compile # To update, run: # # pip-compile --output-file requirements/dev.txt requirements/dev.in # -e . astroid==1.5.3 # via pylint certifi==2017.4.17 # via requests chardet==3.0.4 # via requests click==6.7 # via pip-tools colorama==0.3.9 # via radon coverage==4.4.1 docopt==0.6.2 first==2.0.1 # via pip-tools flake8-polyfill==1.0.1 # via radon flake8==3.3.0 # via flake8-polyfill idna==2.5 # via requests isort==4.2.15 # via pylint, yala jinja2==2.9.6 lazy-object-proxy==1.3.1 # via astroid mando==0.3.3 # via radon markupsafe==1.0 # via jinja2 mccabe==0.6.1 # via flake8, pylint pip-tools==1.9.0 pluggy==0.4.0 # via tox py==1.4.34 # via tox pycodestyle==2.3.1 # via flake8, yala pydocstyle==1.1.1 # via yala pyflakes==1.5.0 # via flake8 pylint==1.7.2 # via yala radon==1.5.0 # via yala requests==2.18.1 six==1.10.0 # via astroid, pip-tools, pylint tox==2.7.0 urllib3==1.21.1 # via requests virtualenv==15.1.0 # via tox wrapt==1.10.10 # via astroid yala==1.1.1 kytos-utils-2017.2b1/requirements/run.txt000066400000000000000000000005711316031501200204220ustar00rootroot00000000000000# # This file is autogenerated by pip-compile # To update, run: # # pip-compile --output-file requirements/run.txt setup.py # certifi==2017.4.17 # via requests chardet==3.0.4 # via requests docopt==0.6.2 idna==2.5 # via requests jinja2==2.9.6 markupsafe==1.0 # via jinja2 requests==2.18.1 urllib3==1.21.1 # via requests kytos-utils-2017.2b1/setup.cfg000066400000000000000000000004471316031501200161550ustar00rootroot00000000000000[pycodestyle] exclude = .eggs,ENV,build,docs/conf.py,venv [yala] radon mi args = --min C pylint args = --disable=too-few-public-methods [pydocstyle] add-ignore = D105 # D105: Missing docstring in magic method [isort] # The first party was necessary. known_first_party = kytos.utils,kytos.cli kytos-utils-2017.2b1/setup.py000066400000000000000000000122511316031501200160420ustar00rootroot00000000000000"""Setup script. Run "python3 setup --help-commands" to list all available commands and their descriptions. """ import os from abc import abstractmethod # Disabling checks due to https://github.com/PyCQA/pylint/issues/73 from distutils.command.clean import clean # pylint: disable=E0401,E0611 from subprocess import CalledProcessError, call, check_call from setuptools import Command, find_packages, setup from setuptools.command.develop import develop if 'VIRTUAL_ENV' in os.environ: BASE_ENV = os.environ['VIRTUAL_ENV'] else: BASE_ENV = '/' SKEL_PATH = 'etc/skel' KYTOS_SKEL_PATH = os.path.join(SKEL_PATH, 'kytos') USERNAME_PATH = os.path.join(KYTOS_SKEL_PATH, 'napp-structure/username') NAPP_PATH = os.path.join(USERNAME_PATH, 'napp') ETC_FILES = [(os.path.join(BASE_ENV, USERNAME_PATH), [os.path.join(USERNAME_PATH, '__init__.py')]), (os.path.join(BASE_ENV, NAPP_PATH), [os.path.join(NAPP_PATH, '__init__.py'), os.path.join(NAPP_PATH, 'kytos.json.template'), os.path.join(NAPP_PATH, 'openapi.yml.template'), os.path.join(NAPP_PATH, 'main.py.template'), os.path.join(NAPP_PATH, 'README.rst.template'), os.path.join(NAPP_PATH, 'settings.py.template')])] class SimpleCommand(Command): """Make Command implementation simpler.""" user_options = [] def __init__(self, *args, **kwargs): """Store arguments so it's possible to call other commands later.""" super().__init__(*args, **kwargs) self._args = args self._kwargs = kwargs @abstractmethod def run(self): """Run when command is invoked. Use *call* instead of *check_call* to ignore failures. """ pass def initialize_options(self): """Set defa ult values for options.""" pass def finalize_options(self): """Post-process options.""" pass class Cleaner(clean): """Custom clean command to tidy up the project root.""" description = 'clean build, dist, pyc and egg from package and docs' def run(self): """Clean build, dist, pyc and egg from package and docs.""" super().run() call('rm -vrf ./build ./dist ./*.egg-info', shell=True) call('find . -name __pycache__ -type d | xargs rm -rf', shell=True) call('test -d docs && make -C docs/ clean', shell=True) class TestCoverage(SimpleCommand): """Display test coverage.""" description = 'run unit tests and display code coverage' def run(self): """Run unittest quietly and display coverage report.""" cmd = 'coverage3 run --source=kytos setup.py test && coverage3 report' check_call(cmd, shell=True) class CITest(SimpleCommand): """Run all CI tests.""" description = 'run all CI tests: unit and doc tests, linter' def run(self): """Run unit tests with coverage, doc tests and linter.""" for command in TestCoverage, Linter: command(*self._args, **self._kwargs).run() class Linter(SimpleCommand): """Code linters.""" description = 'lint Python source code' def run(self): """Run yala.""" print('Yala is running. It may take several seconds...') try: check_call('yala setup.py tests kytos', shell=True) print('No linter error found.') except CalledProcessError: print('Linter check failed. Fix the error(s) above and try again.') class DevelopMode(develop): """Recommended setup for kytos-utils developers. Instead of copying the files to the expected directories, a symlink is created on the system aiming the current source code. """ def run(self): """Install the package in a developer mode.""" super().run() self._create_data_files_directory() @staticmethod def _create_data_files_directory(): current_directory = os.path.abspath(os.path.dirname(__file__)) etc_dir = os.path.join(BASE_ENV, 'etc') if not os.path.exists(etc_dir): os.mkdir(etc_dir) dst_dir = os.path.join(BASE_ENV, SKEL_PATH) if not os.path.exists(dst_dir): os.mkdir(dst_dir) src = os.path.join(current_directory, KYTOS_SKEL_PATH) dst = os.path.join(BASE_ENV, KYTOS_SKEL_PATH) if not os.path.exists(dst): os.symlink(src, dst) setup(name='kytos-utils', version='2017.2b1', description='Command line utilities to use with Kytos.', url='http://github.com/kytos/kytos-utils', author='Kytos Team', author_email='devel@lists.kytos.io', license='MIT', test_suite='tests', include_package_data=True, scripts=['bin/kytos'], install_requires=['docopt', 'requests', 'jinja2>=2.9.5', 'ruamel.yaml'], extras_require={ 'dev': [ 'tox', 'coverage', 'pip-tools', 'yala' ] }, data_files=ETC_FILES, packages=find_packages(exclude=['tests']), cmdclass={ 'ci': CITest, 'clean': Cleaner, 'coverage': TestCoverage, 'develop': DevelopMode, 'lint': Linter }, zip_safe=False) kytos-utils-2017.2b1/tests/000077500000000000000000000000001316031501200154715ustar00rootroot00000000000000kytos-utils-2017.2b1/tests/__init__.py000066400000000000000000000000411316031501200175750ustar00rootroot00000000000000"""Test suit for kytos-utils.""" kytos-utils-2017.2b1/tox.ini000066400000000000000000000003211316031501200156360ustar00rootroot00000000000000[tox] envlist = py36 [testenv] whitelist_externals = rm commands= ; Force packaging even if setup.{py,cfg} haven't changed rm -rf ./*.egg-info/ python setup.py ci deps= -rrequirements/dev.txt