diff --git a/.gitignore b/.gitignore index b78a4e9d0..fcb4e99da 100644 --- a/.gitignore +++ b/.gitignore @@ -110,5 +110,5 @@ kytos/web-ui/index.html etc/kytos/kytos.conf etc/kytos/logging.ini -# ignore Visual Studio folders -*.vs/ +# Visual Studio +.vs/ diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 19faec252..c1bc8a8f6 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -8,7 +8,7 @@ filter: - 'kytos/web-ui/*' build: environment: - python: 3.6.3 + python: 3.6 postgresql: false redis: false dependencies: diff --git a/.travis.yml b/.travis.yml index 51bcc285a..859b57769 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,9 @@ language: python python: - "3.6" install: - - pip install tox + - pip install pip==20.2.2 + - pip install --use-feature=2020-resolver -r requirements/dev.txt script: - - tox \ No newline at end of file + - python setup.py lint + - python setup.py coverage + - python setup.py doctest diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b0645ba6..f1e00eb6b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -27,6 +27,53 @@ Changed ======= +[2020.2b1] - "itamar" beta1 - 2020-09-08 +**************************************** +Added +===== +- Added Blueprints section to the "How to Contribute" guide. + +Fixed +===== +- Fixed bug when two NApps had methods with the same name + decorated with the ``@rest`` decorator. +- Fixed authentication URLs in documentation. +- Fixed interface tests. + +Changed +======= +- Changed ``dev`` requirements to install ``run`` requirements. +- Changed Makefile to use ``python3`` instead of ``python3.6``. +- Updated ``.travis.yml`` to use newest pip dependency resolver for tests. +- Changed ``setup.py`` to alert when a test fails on Travis. + + +[2020.1] - "helena" stable - 2020-08-07 +*************************************** +Added +===== +- Improve unit tests coverage from 55% to 93%. +- Added new method to handle HTTPException - now it returns a JSON + with an error code. +- Added tags decorator to run tests by type and size. +- Added instruction for opening issues with traffic files in Dev Guide. +- Added Pull Request Guidelines to the Developer Guide. + +Fixed +===== +- Fixed duplicated endpoint error in available_vlans method. +- Fixed error when creating an EVC without a Tag. +- Fixed packaging error by changing the ``six`` version. + +Changed +======= +- Updated setup.py to use native setuptools install. +- Make speed property checks compliant with OF1.3 spec. +- Updated controller mock method to accept loop parameter. +- Changed API server status HTTP code to 200. +- Updated documentation images, dates and links. + + [2020.1rc1] - "helena" release candidate 1 - 2020-06-17 ******************************************************* diff --git a/Makefile b/Makefile index da5050587..1ffa95181 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,14 @@ build: clean prepare - python3.6 setup.py sdist bdist_wheel + python3 setup.py sdist bdist_wheel ls -l dist/ clean: - rm -rf build/ dist/ *.egg-info/ kytos/web-ui-* + rm -vrf build/ dist/ *.egg-info/ kytos/web-ui-* + find . -name __pycache__ -type d | xargs rm -rf + test -d docs && make -C docs/ clean prepare: - pip3.6 install --upgrade pip setuptools wheel twine + pip3 install --upgrade pip setuptools wheel twine testupload: build twine upload -r pypitest dist/* diff --git a/README.rst b/README.rst index 5d599d584..d85f5704d 100644 --- a/README.rst +++ b/README.rst @@ -38,11 +38,12 @@ Just install docker from your package provider and run: Installing ========== -We use Python 3.6, so you'll have to install it into your environment beforehand: +If you don't have Python 3 installed, please install it. Please make +sure that you're using ``python3.6`` or a later version: .. code-block:: shell - $ apt-get install python3.6 + $ apt install python3 Then, the first step is to clone *kytos* repository: @@ -56,7 +57,7 @@ install procedure: .. code-block:: shell $ cd kytos - $ sudo python3.6 setup.py install + $ sudo python3 setup.py install Configuring =========== diff --git a/bin/kytosd b/bin/kytosd index af6b417ab..054f44a34 100755 --- a/bin/kytosd +++ b/bin/kytosd @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.6 +#!/usr/bin/env python3 """Start Kytos SDN Platform core.""" from kytos.core import kytosd diff --git a/docs/Makefile b/docs/Makefile index 00def2bd7..448f83c76 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -21,7 +21,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . ifeq (${PYTHON}, ) - override PYTHON = python3.6 + override PYTHON = python3 endif default: diff --git a/docs/README.rst b/docs/README.rst index 110686dd6..c04efc4a3 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -5,7 +5,7 @@ Kytos Documentation Requirements ============ -To install the docs requirements you must have Python 3.6 and NodeJS +To install the docs requirements you must have Python 3 and NodeJS 9.5.0. After that, install the requirements to build the docs using the command below. @@ -32,4 +32,4 @@ access the browser using the address http://localhost:8000 . .. code-block:: sh - python3.6 -m http.server + python3 -m http.server diff --git a/docs/_static/images/components/accordion/k-accordion-item.png b/docs/_static/images/components/accordion/k-accordion-item.png new file mode 100644 index 000000000..7311a32f4 Binary files /dev/null and b/docs/_static/images/components/accordion/k-accordion-item.png differ diff --git a/docs/_static/images/components/accordion/k-accordion.png b/docs/_static/images/components/accordion/k-accordion.png new file mode 100644 index 000000000..7e0ef5b86 Binary files /dev/null and b/docs/_static/images/components/accordion/k-accordion.png differ diff --git a/docs/_static/images/components/input/k-button-group.png b/docs/_static/images/components/input/k-button-group.png new file mode 100644 index 000000000..cb62b983a Binary files /dev/null and b/docs/_static/images/components/input/k-button-group.png differ diff --git a/docs/_static/images/components/input/k-button.png b/docs/_static/images/components/input/k-button.png new file mode 100644 index 000000000..a6e0711b5 Binary files /dev/null and b/docs/_static/images/components/input/k-button.png differ diff --git a/docs/_static/images/components/input/k-checkbox.png b/docs/_static/images/components/input/k-checkbox.png new file mode 100644 index 000000000..4725e6683 Binary files /dev/null and b/docs/_static/images/components/input/k-checkbox.png differ diff --git a/docs/_static/images/components/input/k-dropdown.png b/docs/_static/images/components/input/k-dropdown.png new file mode 100644 index 000000000..57edbc7de Binary files /dev/null and b/docs/_static/images/components/input/k-dropdown.png differ diff --git a/docs/_static/images/components/input/k-input.png b/docs/_static/images/components/input/k-input.png new file mode 100644 index 000000000..08d0c4629 Binary files /dev/null and b/docs/_static/images/components/input/k-input.png differ diff --git a/docs/_static/images/components/input/k-select.png b/docs/_static/images/components/input/k-select.png new file mode 100644 index 000000000..2428958b5 Binary files /dev/null and b/docs/_static/images/components/input/k-select.png differ diff --git a/docs/_static/images/components/input/k-slider.png b/docs/_static/images/components/input/k-slider.png new file mode 100644 index 000000000..6bad3f453 Binary files /dev/null and b/docs/_static/images/components/input/k-slider.png differ diff --git a/docs/_static/images/components/misc/k-action-menu.png b/docs/_static/images/components/misc/k-action-menu.png new file mode 100644 index 000000000..8e545ed76 Binary files /dev/null and b/docs/_static/images/components/misc/k-action-menu.png differ diff --git a/docs/_static/images/components/misc/k-info-panel.png b/docs/_static/images/components/misc/k-info-panel.png new file mode 100644 index 000000000..c93e9ce1e Binary files /dev/null and b/docs/_static/images/components/misc/k-info-panel.png differ diff --git a/docs/_static/images/components/misc/k-status-bar.png b/docs/_static/images/components/misc/k-status-bar.png new file mode 100644 index 000000000..a9b5712b1 Binary files /dev/null and b/docs/_static/images/components/misc/k-status-bar.png differ diff --git a/docs/_static/images/components/ppanel/k-property-panel-item.png b/docs/_static/images/components/ppanel/k-property-panel-item.png new file mode 100644 index 000000000..730ada016 Binary files /dev/null and b/docs/_static/images/components/ppanel/k-property-panel-item.png differ diff --git a/docs/_static/images/components/ppanel/k-property-panel.png b/docs/_static/images/components/ppanel/k-property-panel.png new file mode 100644 index 000000000..21a5d0f6f Binary files /dev/null and b/docs/_static/images/components/ppanel/k-property-panel.png differ diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html index 2072afc0e..6139e9c8a 100644 --- a/docs/_templates/layout.html +++ b/docs/_templates/layout.html @@ -51,13 +51,13 @@
@@ -66,7 +66,7 @@

- © 2016 - 2018 Kytos is an Open Source software under MIT license.
+ © 2016 - 2020 Kytos is an Open Source software under MIT license.

diff --git a/docs/admin/installing.rst b/docs/admin/installing.rst index 9fad31fc6..764cc9d67 100644 --- a/docs/admin/installing.rst +++ b/docs/admin/installing.rst @@ -5,8 +5,8 @@ Installing Installing required dependencies ================================ -We use Python 3.6, so in order to use this software please install it -into your environment beforehand. Note that an additional step is +Kytos requires at least Python 3.6, so in order to use this software please +install it into your environment beforehand. Note that an additional step is needed for Ubuntu releases older than 16.10. @@ -20,15 +20,16 @@ install Python 3.6 packages. To add this PPA, use the commands: $ sudo add-apt-repository ppa:jonathonf/python-3.6 $ sudo apt update + $ sudo apt install python-pip python3.6 Required packages ================= -The required Ubuntu packages can be installed by: +The required Debian/Ubuntu packages can be installed by: .. code-block:: bash - $ sudo apt install python-pip python3.6 + $ sudo apt install python3-pip python3 Installing with pip =================== @@ -38,7 +39,7 @@ previous steps, then: .. code-block:: bash - $ pip install kytos kytos-utils + $ pip3 install kytos kytos-utils This will install the Kytos server and client, and also their dependencies, like the python-openflow library. diff --git a/docs/blueprints/EP000.rst b/docs/blueprints/EP000.rst new file mode 100644 index 000000000..4dfbc3cb3 --- /dev/null +++ b/docs/blueprints/EP000.rst @@ -0,0 +1,157 @@ +:EP: 0 +:Title: Enhancement Proposals +:Authors: + - José Mauro Ribeiro +:Reviewers: + - Antonio Francisco + - Beraldo Leal + - Carlos Magno + - Gleyberson Andrade + - Humberto Diógenes + - Rogério Motitsuki +:Created: 2020-05-14 +:Kytos-Version: 2020.1 +:Status: Draft +:Type: Standards Track + + +######## +Abstract +######## +This blueprint is proposed to clarify what blueprints are, their purpose, list the types of blueprints, explain which sections should be included in a blueprint, the information that should be included in each topic and the workflow involving a blueprint proposal. Therefore, this blueprint is intended to work as a meta blueprint. + +########## +Motivation +########## + +The Kytos Project uses the word 'blueprint' to refer to a specific type of document that plays a very important role in the development process. Anyone who is not familiar with the concept should be able to understand and start a draft blueprint using the EP0 (blueprint 0). +A blueprint is a document used to specify the details of any process, features and anything else that is desirable to be implemented in Kytos Project and cannot be reduced to a GitHub issue or a meeting to be outlined, although a blueprint could start as an issue and become a blueprint after some discussion. +This blueprint is intended to help anyone who wants to start a new blueprint. Besides that, the standard proposed here could save time for the author of a blueprint who doesn't have to think about the structure of the project and avoid that different authors write blueprints in a very different way. Separate blueprints with common sections between them could help even the readers who are familiar with the structure once they could search for specific sections in it. + +######### +Rationale +######### +This meta blueprint is intended to make the contributor's workflow more clear about how to write a new blueprint. + +############# +Specification +############# + +Workflow +************** +Every blueprint should follow the steps below in order to be finished: + +Outline the subject +=================== +The process for writing a blueprint should begin by describing a new idea, a specification to something that has been defined to be implemented. It is recommended that a single blueprint contain a single proposal. The more focused the blueprint, the more successful it tends to be. The Kytos team have the right to reject or approve any proposal. If in doubt, create an issue in the Kytos Project on Github and add a comment asking if the issue could be a blueprint. + +Submitting a blueprint +====================== +After creating a first blueprint by adding every necessary section (check the sections under "Sections that a blueprint should have") you are encouraged to create a pull request in Kytos repository attending the following criteria: + + - The status of the blueprint should be "draft"; + - The file containing the proposal must follow the naming convention "EP[number].rst", where [number] is a sequential number, e.g. "EP001.rst"; + + - The file must contain a header following the template available in Blueprint Header section; + + - The file must be written in restructured text (RST) format like the other blueprints (as you can see at 'kytos/docs/blueprints'). + + +Blueprint review +================ +In the following days after you submit your pull request, the Kytos team will review the document adding comments and suggestions. So the author of the pull request must pay attention to the Kytos team feedback to make the review a quick process. + + +Sections that a blueprint should have +************************************* + +Header +====== +Every blueprint proposal should use this template to build their own header: + +:EP: <\*EP number> +:Title: +:Authors: +:Reviewers: +:Created: +:Kytos-Version: +:Status: +:Type: +:\**Replaces: + +\*EP number: should be fixed by Kytos team after the author submit the PR. + +\**Optional field. + + +Abstract +======== +A short (~200 word) description of the technical issue being addressed. + +Motivation +========== +It should clearly explain why the blueprint is being proposed describing any technical issue that is intended to be covered by the proposal. + +Rationale +========= +The rationale fleshes out the specification by describing why particular design decisions were made. It should describe alternate designs that were considered and related work, e.g. how the feature is supported in other environments or scenarios. + +Specification +============= +The technical specification should describe the aspects of what is being proposed. + +Backwards Compatibility +======================= +All EPs that introduce backwards incompatibilities must include a section describing these incompatibilities and their severity. The EP must explain how the author proposes to deal with these incompatibilities. + +Security Implications +===================== +If there are security concerns in relation to the EP, those concerns should be explicitly written out to make sure reviewers of the EP are aware of them. + +Rejected Ideas +============== +Throughout the discussion of an EP, various proposed ideas end up not being accepted. Those rejected ideas should be recorded along with the reasoning as to why they were rejected. This both helps record the thought process behind the final version of the EP as well as preventing people from bringing up the same rejected ideas again in subsequent discussions. + +References +========== +References -- A collection of URLs used as references through the EP. + +Copyright/license +================= + +Types of blueprints +******************* + +Standard: Describes a new feature and its implementation. + +Process: Guidelines or information for the community and developers, but does not propose a new feature. + +Epic: Describes a problem and its solution. + +####################### +Backwards Compatibility +####################### +At the moment that this blueprint is being proposed Kytos Project have the total of 21 blueprints created. The standard proposed in EP0 should be followed by any new blueprint proposed after the blueprint 21, and is established here that the update of the previous blueprints are not mandatory. + +##################### +Security Implications +##################### +Not applicable here + +############## +Rejected Ideas +############## +Not applicable here + +########## +References +########## + +[1] https://www.python.org/dev/peps/pep-0001/#pep-audience + +######### +Copyright +######### + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. diff --git a/docs/blueprints/EP001.rst b/docs/blueprints/EP001.rst index 3dd536414..4324939ae 100644 --- a/docs/blueprints/EP001.rst +++ b/docs/blueprints/EP001.rst @@ -1,3 +1,14 @@ +:EP: 1 +:Title: Kytos testing pipeline and definitions +:Authors: + - Renan Rodrigo Renan Rodrigo + - Macártur de Sousa Carvalho + - Erick Vermot +:Created: 2017-03-31 +:Kytos-Version: 2017.1 +:Status: Accepted +:Type: Informational + ######################## Enhancement Proposal 001 ######################## diff --git a/docs/blueprints/EP002.rst b/docs/blueprints/EP002.rst index d2fbf6759..482ce2270 100644 --- a/docs/blueprints/EP002.rst +++ b/docs/blueprints/EP002.rst @@ -1,13 +1,6 @@ -################## -OpenFlow Handshake -################## - - -Status -###### - -Accepted - +:EP: 2 +:Title: OpenFlow Handshake +:Status: Accepted Abstract ######## diff --git a/docs/blueprints/EP003.rst b/docs/blueprints/EP003.rst index e6edfa645..9b1949888 100644 --- a/docs/blueprints/EP003.rst +++ b/docs/blueprints/EP003.rst @@ -1,13 +1,6 @@ -##################### -python-openflow Tests -##################### - - -Status -###### - -Accepted - +:EP: 3 +:Title: python-openflow Tests +:Status: Accepted Abstract ######## diff --git a/docs/blueprints/EP004.rst b/docs/blueprints/EP004.rst index 369b57c59..f1053a9e7 100644 --- a/docs/blueprints/EP004.rst +++ b/docs/blueprints/EP004.rst @@ -1,12 +1,6 @@ -###################### -Updating specification -###################### - - -Status -###### - -Accepted +:EP: 4 +:Title: Updating specification +:Status: Accepted Note: all the (draft) code is available at https://transfer.sh/ac8jc/version-inheritance.tar.gz. diff --git a/docs/blueprints/EP005.rst b/docs/blueprints/EP005.rst index 8b48dc618..5e9e1c643 100644 --- a/docs/blueprints/EP005.rst +++ b/docs/blueprints/EP005.rst @@ -1,13 +1,6 @@ -########################### -Version inheritance problem -########################### - - -Status -###### - -Accepted - +:EP: 5 +:Title: Version inheritance problem +:Status: Accepted Description ########### diff --git a/docs/blueprints/EP006.rst b/docs/blueprints/EP006.rst index bfcbabc55..5af7f940e 100644 --- a/docs/blueprints/EP006.rst +++ b/docs/blueprints/EP006.rst @@ -1,12 +1,6 @@ -################### -Version Inheritance -################### - - -Status -###### - -Accepted +:EP: 6 +:Title: Version Inheritance +:Status: Accepted Description diff --git a/docs/blueprints/EP007.rst b/docs/blueprints/EP007.rst index 7ae242afc..c591fc5ce 100644 --- a/docs/blueprints/EP007.rst +++ b/docs/blueprints/EP007.rst @@ -1,3 +1,7 @@ +:EP: 7 +:Title: OpenFlow handshake process on Kytos +:Status: Draft + ######################## Enhancement Proposal 007 ######################## diff --git a/docs/blueprints/EP008.rst b/docs/blueprints/EP008.rst index 05733d166..ed44e871b 100644 --- a/docs/blueprints/EP008.rst +++ b/docs/blueprints/EP008.rst @@ -1,3 +1,7 @@ +:EP: 8 +:Title: Pack/unpack tests and how to capture/dump binary message packets +:Status: Draft + ######################## Enhancement Proposal 008 ######################## diff --git a/docs/blueprints/EP009.rst b/docs/blueprints/EP009.rst index 2fa9e5ecf..7a0920b2e 100644 --- a/docs/blueprints/EP009.rst +++ b/docs/blueprints/EP009.rst @@ -1,7 +1,6 @@ -######################## -Enhancement Proposal 009 -######################## - +:EP: 9 +:Title: Improve packing/unpacking message +:Status: Draft Objective ######### diff --git a/docs/blueprints/EP010.rst b/docs/blueprints/EP010.rst index ec2b6ef5b..679149943 100644 --- a/docs/blueprints/EP010.rst +++ b/docs/blueprints/EP010.rst @@ -1,7 +1,6 @@ -######################## -Enhancement Proposal 010 -######################## - +:EP: 10 +:Title: Implement OpenFlow protocol parsing headers +:Status: Draft Objective ######### diff --git a/docs/blueprints/EP011.rst b/docs/blueprints/EP011.rst index 6db4d8748..bb5536b76 100644 --- a/docs/blueprints/EP011.rst +++ b/docs/blueprints/EP011.rst @@ -1,13 +1,6 @@ -########################## -Multipart Request Creation -########################## - - -Status -###### - -Proposed - +:EP: 11 +:Title: Multipart Request Creation +:Status: Proposed Abstract ######## diff --git a/docs/blueprints/EP012.rst b/docs/blueprints/EP012.rst index daf6a330f..6031d560d 100644 --- a/docs/blueprints/EP012.rst +++ b/docs/blueprints/EP012.rst @@ -1,20 +1,15 @@ +:EP: 12 +:Title: MEF E-Line (or Point-to-Point Ethernet Virtual Circuit) Service Provisioning NApp +:Status: Work-in-progress + Summary ======= -Title ------ -MEF E-Line (or Point-to-Point Ethernet Virtual Circuit) Service Provisioning -NApp - Authors ------- Jeronimo Bezerra (Amlight), Vinicius Arcanjo (RNP), Antonio Francisco (ANSP), Rogerio Motitsuki (ANSP), Beraldo Leal, (Kytos), Renan Rodrigo (Kytos) -Blueprint Status ----------------- -work-in-progress - Priority -------- high diff --git a/docs/blueprints/EP013.rst b/docs/blueprints/EP013.rst index a9c15c90b..4dee58889 100644 --- a/docs/blueprints/EP013.rst +++ b/docs/blueprints/EP013.rst @@ -1,19 +1,16 @@ +:EP: 13 +:Title: Topology NApp Improvements +:Status: Finished + + Summary ======= -Title ------ -Topology NApp Improvements - Authors ------- Jeronimo Bezerra (Amlight), Vinicius Arcanjo (RNP), Antonio Francisco (ANSP), Rogerio Motitsuki (ANSP), Beraldo Leal, (Kytos), Renan Rodrigo (Kytos) -Blueprint Status ----------------- -finished - Priority -------- high diff --git a/docs/blueprints/EP014.rst b/docs/blueprints/EP014.rst index 564ae4f37..f9a61d517 100644 --- a/docs/blueprints/EP014.rst +++ b/docs/blueprints/EP014.rst @@ -1,19 +1,15 @@ +:EP: 14 +:Title: Pathfinder NApp Improvements +:Status: Finished + Summary ======= -Title ------ -Pathfinder NApp Improvements - Authors ------- Jeronimo Bezerra (Amlight), Vinicius Arcanjo (RNP), Antonio Francisco (ANSP), Rogerio Motitsuki (ANSP), Beraldo Leal, (Kytos), Renan Rodrigo (Kytos) -Blueprint Status ----------------- -finished - Priority -------- high diff --git a/docs/blueprints/EP015.rst b/docs/blueprints/EP015.rst index 8d8b2fe40..25f4dd4ec 100644 --- a/docs/blueprints/EP015.rst +++ b/docs/blueprints/EP015.rst @@ -1,18 +1,14 @@ +:EP: 15 +:Title: System tests for NApps validation. +:Status: Working-in-progress + Summary ======= -Title ------ -System tests for NApps validation. - Authors ------- Vinicius Arcanjo (RNP/Kytos), Jeronimo Bezerra (FIU/Kytos), Antonio Francisco (AmLight/Kytos), Beraldo Leal (Kytos), Macartur Souza (Kytos), Humberto Diogenes (Kytos) -Blueprint Status ----------------- -work-in-progress - Priority -------- high diff --git a/docs/blueprints/EP017.rst b/docs/blueprints/EP017.rst index f1a6c4109..87e347e2d 100644 --- a/docs/blueprints/EP017.rst +++ b/docs/blueprints/EP017.rst @@ -1,18 +1,14 @@ +:EP: 17 +:Title: Improvement of `kytos/flow_manager` logic. +:Status: Draft + Summary ======= -Title ------ -Improvement of `kytos/flow_manager` logic. - Authors ------- Antonio Francisco (AmLight/Kytos), Beraldo Leal (Kytos) -Blueprint Status ----------------- -draft - Priority -------- high diff --git a/docs/blueprints/EP021.rst b/docs/blueprints/EP021.rst index d88fcdefd..9b873a91e 100644 --- a/docs/blueprints/EP021.rst +++ b/docs/blueprints/EP021.rst @@ -1,4 +1,4 @@ -:EP: BP021 +:EP: 21 :Title: Kytos testing pipeline and definitions :Authors: - Humberto Diógenes diff --git a/docs/conf.py b/docs/conf.py index 42d6bbabf..c84f2ac21 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,10 +64,10 @@ # built documents. # # The short X.Y version. -version = u'2020.1' +version = u'2020.2b1' show_version = False # The full version, including alpha/beta/rc tags. -release = u'2020.1rc1' +release = u'2020.2b1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -368,6 +368,9 @@ .. |port_features| replace:: :class:`~pyof.v0x01.common.phy_port.PortFeatures` +.. |port_config| replace:: + :class:`~pyof.v0x01.common.phy_port.PortConfig` + .. |desc_stats| replace:: :class:`~pyof.v0x01.controller2switch.common.DescStats` diff --git a/docs/developer/auth.rst b/docs/developer/auth.rst index 6383a86ff..965af34f4 100644 --- a/docs/developer/auth.rst +++ b/docs/developer/auth.rst @@ -15,7 +15,7 @@ This endpoint creates new users: .. code-block:: shell - POST http://127.0.0.1:8181/api/kytos/auth/users/ + POST http://127.0.0.1:8181/api/kytos/core/auth/users/ $ curl -X POST \ -H 'Content-Type: application/json' \ @@ -29,7 +29,7 @@ is correct: .. code-block:: shell - GET http://127.0.0.1:8181/api/kytos/auth/login/ + GET http://127.0.0.1:8181/api/kytos/core/auth/login/ $ curl -X GET \ -H 'Accept:application/json' \ @@ -40,7 +40,7 @@ This endpoint lists the registered users: .. code-block:: shell - GET http://127.0.0.1:8181/api/kytos/auth/v1/users/ + GET http://127.0.0.1:8181/api/kytos/core/auth/v1/users/ $ curl -X GET \ -H 'Accept:application/json' \ @@ -51,7 +51,7 @@ This endpoint gets details about a specific user: .. code-block:: shell - GET http://127.0.0.1:8181/api/kytos/auth/users// + GET http://127.0.0.1:8181/api/kytos/core/auth/users// $ curl -X GET \ -H 'Content-type: application/json' \ @@ -65,7 +65,7 @@ This endpoint deletes a specific user. .. code-block:: shell - DELETE http://127.0.0.1:8181/api/kytos/auth/v1/users// + DELETE http://127.0.0.1:8181/api/kytos/core/auth/v1/users// $ curl -X DELETE \ -H 'Content-type: application/json' \ @@ -78,7 +78,7 @@ This endpoint update a specific user: .. code-block:: shell - PATCH http://127.0.0.1:8181/api/kytos/auth/v1/users// + PATCH http://127.0.0.1:8181/api/kytos/core/auth/v1/users// $ curl -X PATCH \ -H 'Content-Type: application/json' \ diff --git a/docs/developer/components.rst b/docs/developer/components.rst new file mode 100644 index 000000000..ea5879a04 --- /dev/null +++ b/docs/developer/components.rst @@ -0,0 +1,680 @@ +========= +accordion +========= + +k-accordion +----------- + +A GUI widget with a list of items (``k-accordion-item``) that can be switched between hiding and showing content. + +**Image** + +.. image:: /_static/images/components/accordion/k-accordion.png + :align: center + +**Example** + +.. code-block:: shell + + + + + + + + + + + + + + + + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**Slots** + +======= ===================================== +name description +======= ===================================== +default Slot to be filled with accordion item +======= ===================================== + +k-accordion-item +---------------- + +A GUI widget that can be switched between hiding and showing content + +**Example** + +.. code-block:: shell + + + + + + + + + + + + +**Image** + +.. image:: /_static/images/components/accordion/k-accordion-item.png + :align: center + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +checked boolean false true Boolean value to represent whether the accordion item is checked. +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**Slots** + +======= ============================================== +name description +======= ============================================== +default Empty Pannel, please define some items inside. +======= ============================================== + +====== +inputs +====== + +k-button +-------- + +This component represents a button that triggers an event when clicked. + +**Example** + +.. code-block:: shell + + + +**Image** + +.. image:: /_static/images/components/input/k-button.png + :align: center + +**Parameters** + +========== ======= ======== ========================= ================================================================== +name type required default description +========== ======= ======== ========================= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +on_click func false function(val) { return; } Function called after the button is clicked. +========== ======= ======== ========================= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**click**: Call on_click event. + +**Parameters** + +===== ====== ============= +name type description +===== ====== ============= +event object trigged event +===== ====== ============= + +k-button-group +-------------- + +Allows to group buttons (``k-button``), which trigger events when clicked. + +**Example** + +.. code-block:: shell + + + + + + + +**Image** + +.. image:: /_static/images/components/input/k-button-group.png + :align: center + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**Slots** + +======= ==================================== +name description +======= ==================================== +default Slot to be filled with a buttons set +======= ==================================== + +k-checkbox +---------- + +A GUI widget that permits the user to make a binary choice, checked (ticked) when activated or not checked when disable. + +**Image** + +.. image:: /_static/images/components/input/k-checkbox.png + :align: center + +**Parameters** + +========== ======= ======== =========================== ======================================================================================= +name type required default description +========== ======= ======== =========================== ======================================================================================= +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +model array false Model store the checked values. +value number false 0 The value to checkbox button. +checked boolean false false Initial value to checkbox, when true the checkbox will be checked, otherwise unchecked. +action func false function(value) { return; } Optinal action called after check a checkbox button. +========== ======= ======== =========================== ======================================================================================= + +**Methods** + +**uuid4**: Return a uuid string + +k-dropdown +---------- + +A toggleable menu that allows the user to choose one value from a predefined list. + +**Example** + +.. code-block:: shell + + k-dropdown title="Switch Labels:" icon="circle-o" :options="switchLabels" :event="{name: 'topology-toggle-label', content: {node_type: 'switch'}}"> + +**Image** + +.. image:: /_static/images/components/input/k-dropdown.png + :align: center + +**Parameters** + +========== ======= ======== =========================== ======================================================================= +name type required default description +========== ======= ======== =========================== ======================================================================= +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +value string false "" Property with the selected option. +options array true A collection with all options that could be selected. +event object false An event triggered when the dropdown change, this event should have the + following content: {**name**: 'event_name', **content**: {} } +action func false function(value) { return; } Optinal action called after select a dropdown option. +========== ======= ======== =========================== ======================================================================= + +**Methods** + +**uuid4**: Return a uuid string + +k-input +------- + +An input field where the user can enter data. + +**Image** + +.. image:: /_static/images/components/input/k-input.png + :align: center + +**Parameters** + +=========== ======= ======== ========================= ================================================================== +name type required default description +=========== ======= ======== ========================= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +value string false "" The value to input button. +placeholder string false Placeholder string displayed in input field. +action func false function(val) { return; } Function called after input changes. +=========== ======= ======== ========================= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +k-select +-------- + +This component is a form control and can be used to collect the selected user +input from a list of options. + +**Example** + +.. code-block:: shell + + + +**Image** + +.. image:: /_static/images/components/input/k-select.png + :align: center + +**Parameters** + +========== ======= ======== =========================== ================================================================== +name type required default description +========== ======= ======== =========================== ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +value array false +options array true +event object false +action func false function(value) { return; } +========== ======= ======== =========================== ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +k-slider +-------- + +A GUI widget that allows the users specify a numeric value which must be no less than a given value, and no more than another given value. + +**Example** + +.. code-block:: shell + + + +**Image** + +.. image:: /_static/images/components/input/k-slider.png + :align: center + +**Parameters** + +============ ======= ======== ========================= ================================================================== +name type required default description +============ ======= ======== ========================= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +initialValue number false 0 Initial value assigned to slider input. +action func false function(val) { return; } Optinal action called after change the range of slider input. +min number false 0 Minimum value assigned to slider input. +max number false 100 Maximum value assigned to slider input. +step number false 1 The minimum change when the slider increase or decrease. +============ ======= ======== ========================= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +k-textarea +---------- + +A text input field with multi-line. + +**Parameters** + +=========== ======= ======== =========================== ================================================================== +name type required default description +=========== ======= ======== =========================== ================================================================== +title string false Property representing a title +tooltip string false A tooltip displayed in the text-area +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +value string false The value text used in TextArea. +modelValue string false "" +placeholder string false String displayed when the text-area is empty. +action func false function(value) { return; } Optimal action called after textarea changes. +=========== ======= ======== =========================== ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +==== +misc +==== + +k-action-menu +------------- + +Menu with a list of actions and their shortcuts. The menu can be shown or +hidden using the shortcut *Ctrl+Alt+Space*. + +**Image** + +.. image:: /_static/images/components/misc/k-action-menu.png + :align: center + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**add_action_menu_item**: Method to add new action menu item + +**Parameters** + +======= ====== =========================================================== +name type description +======= ====== =========================================================== +options object An object with the params [name, author, shortkey, content] +======= ====== =========================================================== + +k-context-panel +--------------- + +Represents a context where the developer can add any desired content. + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +subtitle string false +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**Slots** + +======= ==================================== +name description +======= ==================================== +default Can be filled with the panel content +======= ==================================== + +k-info-panel +------------ + +Shows details about selected kytos components. This panel appears on the +right of the Kytos GUI and the NApp developer can choose what to display on +the panel. + +**Image** + +.. image:: /_static/images/components/misc/k-info-panel.png + :align: center + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +subtitle string false +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**show**: Show the Info Panel displayed in the right. + +**Parameters** + +======= ====== ======================================================= +name type description +======= ====== ======================================================= +content object An object filled with: + + { + **component**: "search-hosts", + **content**: {**msg**:"content used in the component"}, + **title**: "Search Hosts", + **icon**: "desktop" + } +======= ====== ======================================================= + +k-menu-bar +---------- + +A base components with icon in property. + +**Parameters** + +========== ========= ======== ======= ================================================================== +name type required default description +========== ========= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +toggle undefined false +compacted undefined false +========== ========= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +k-status-bar +------------ + +A GUI widget the shows notifications and System Information. + +**Image** + +.. image:: /_static/images/components/misc/k-status-bar.png + :align: center + +**Methods** + +**set_status**: Display a message inside the k-status-bar. + +**Parameters** + +======= ======= ========================================================= +name type description +======= ======= ========================================================= +message string Message to be displayed. +error boolean If true will display the message in red, default is false +======= ======= ========================================================= + +k-toolbar-item +-------------- + +Component representing a toolbar item that create a new item in the +``k-menu-bar`` and shows the content in the ``k-toolbar``. + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +icon string false An Icon string representing a awesome icon. +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**Slots** + +======= =========================== +name description +======= =========================== +default Slot with the toolbar item. +======= =========================== + +====== +ppanel +====== + +k-property-panel +---------------- + +This component allows to create a table with two columns ( *name* and *value*). Each row in the table is a ``k-property-panel-item`` component, with the *value* and *name*. + +**Image** + +.. image:: /_static/images/components/ppanel/k-property-panel.png + :align: center + +**Example** + +.. code-block:: shell + + + + + + +**Parameters** + +========== ======= ======== ======= ================================================================== +name type required default description +========== ======= ======== ======= ================================================================== +title string false Property representing a title +tooltip string false Property representing a tooltip +isDisabled boolean false false Property to represent show if the component is enabled ou disabled +========== ======= ======== ======= ================================================================== + +**Methods** + +**uuid4**: Return a uuid string + +**Slots** + +======= =============================================================== +name description +======= =============================================================== +default Define a table content inside, a PropertyPanelItem can be used. +======= =============================================================== + +k-property-panel-item +--------------------- + +This component create a row in the table (``k-property-panel``) with two columns, *name* and *value*. + +**Image** + +.. image:: /_static/images/components/ppanel/k-property-panel-item.png + :align: center + +**Example** + +.. code-block:: shell + + + - - - // todo: create a example with some inputs and buttons - - - - - - - - - -You can use any **font-awesome icon**. Kytos ui interface will read that icon -and create a new button into the **k-menu-bar**. - Create your Meta-NApp ===================== diff --git a/docs/developer/events.rst b/docs/developer/events.rst new file mode 100644 index 000000000..8e61c3d4b --- /dev/null +++ b/docs/developer/events.rst @@ -0,0 +1,25 @@ +**k-action-menu** + +================= ====== =========================================== +name type description +================= ====== =========================================== +addActionMenuItem object Add a new action item in the k-action-menu. +================= ====== =========================================== + +**k-info-panel** + +============= ====== =========================================== +name type description +============= ====== =========================================== +showInfoPanel Object Show the info panel in the right. +hideInfoPanel NULL Hide the info panel displayed in the right. +============= ====== =========================================== + +**k-status-bar** + +============= ====== ================================== +name type description +============= ====== ================================== +statusMessage string Show a status message in StatusBar +============= ====== ================================== + diff --git a/docs/developer/hacking.rst b/docs/developer/hacking.rst index 6620a20d8..3857eceb1 100644 --- a/docs/developer/hacking.rst +++ b/docs/developer/hacking.rst @@ -50,19 +50,18 @@ html_cov/index.html``). Pyenv ===== -Some distributions do not provide all python releases, specially the most -recent ones such as ``>= 3.6``. The easiest *pythonic* way to setup your python -environment with the correct python version and its dependencies is to use -|pyenv|_ and |venv|_. So, here we will guide you on how to install those tools -and use them. +Some distributions do not provide all Python releases. The easiest *pythonic* +way to setup your Python environment with the correct version and its +dependencies is to use |pyenv|_ and |venv|_. So, here we will guide you on how +to install those tools and use them. The following steps are focusing on Debian-Like systems, so some tweaks may be -necessary for other distro based systems. +necessary for other Linux distributions. On the end of this chapter there are also other reference links for you to -better understand this projects and tools. +better understand this project and its tools. -.. note:: If you already have python 3.6 installed, you do not need to follow +.. note:: If you already have Python 3.6+ installed, you do not need to follow this chapter. In such cases you can go straightforward to our :doc:`tutorial:napps/development_environment_setup` tutorial. @@ -74,7 +73,7 @@ Here is the `tl;dr `__ version to install .. code-block:: console - $ sudo apt-get install -y \ + $ apt install -y \ make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \ xz-utils tk-dev git @@ -96,14 +95,14 @@ Now, to activate the environment you just need to run: ``pyenv activate kytos`` Long version ------------ -Now the detailed/explained version. +Now the detailed version. |pyenv_cbp|_ also recommends you to install some system tools to be able to build python versions (we have added git to that list): .. code-block:: bash - $ sudo apt-get install -y make \ + $ apt install -y \ make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ libsqlite3-dev wget curl llvm libncurses5-dev libncursesw5-dev \ xz-utils tk-dev git @@ -150,18 +149,18 @@ Now you have |pyenv|_ and |pyenv_venv|_ tools available to be used on your shell installed it, you may need to run the commands on the shell or source your .bashrc file. -Install python specific version +Install specific Python version ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To install a specific version of python, such as 3.6.2, you just neeed to run: +To install a specific version of Python, such as 3.6.2, you just need to run: .. code:: bash $ pyenv install 3.6.2 -Python community considers a best practice to use "virtual environments" +The Python community considers a best practice to use "virtual environments" |venv|_ in order to avoid conflict between python dependencies among multiple -projects, and even project and system python libraries. So, you may want to +projects, and even project and system Python libraries. So, you may want to create a |venv|_ for kytos by doing: .. code:: bash diff --git a/docs/developer/how_to_contribute.rst b/docs/developer/how_to_contribute.rst index 49ea0cc5c..d76d14e69 100644 --- a/docs/developer/how_to_contribute.rst +++ b/docs/developer/how_to_contribute.rst @@ -7,8 +7,7 @@ feedback is important for us to fix bugs, add new features and make it easier to create and share Network Applications. To give us your feedback, check the most appropriate instructions in the next -section. You can also contact us directly and subscribe to mailing lists as -described in the :doc:`/home/get_help` section. +section. You can also contact us directly on the `Slack channel`_. Contributing Guidelines ----------------------- @@ -20,6 +19,7 @@ Here are the guidelines we follow for different ways of contributing: - `Want a Feature?`_ - `Want a Doc Fix?`_ - `Submission Guidelines`_ +- `Blueprints`_ - `Coding Style`_ - `Signing the CLA`_ @@ -27,8 +27,7 @@ Got a Question or Problem? -------------------------- If you have questions about how to use any component of the Kytos project, -direct them to our dev mailing list. We are also available on IRC. Check the -details in the :doc:`/home/get_help` section. +direct them to our `Slack channel`_. .. _contributing-issue: @@ -50,10 +49,9 @@ If you would like to implement a new feature, then consider what kind of change it is: - **Major Changes** that you wish to contribute to the project should be - discussed first in our dev mailing list or IRC (see: :doc:`/home/get_help` - section), so that we can better coordinate our efforts, prevent work - duplication, and help you to craft the changes so they are successfully - integrated into the project. + discussed first in our `Slack channel`_, so that we can better coordinate our + efforts, prevent work duplication, and help you craft the changes so they + are successfully integrated into the project. - **Small Changes** can be crafted and submitted to the |repo| as Pull Requests. @@ -116,6 +114,9 @@ possible: - **Error output** or logs pasted in your issue or in a `Gist `__. When pasting them in the issue, wrap it with three backticks: **\`\`\`** so it renders nicely, like ``this``; +- **Traffic files** - if your issue is related to network traffic, attach as + many traffic files as possible (i.e.: .pcap), as long as they don't contain + sensitive information; - **Steps to reproduce** - please inform all the steps to reproduce the error; - **Motivation or use case** - explain why this is a bug for you; - **System details** like what library or operating system you’re @@ -208,6 +209,20 @@ Code contribution steps review #. Create a new PR in GitHub #. Respond to any code review feedback +Blueprints +---------- + +We have a type of document used to describe our architectural choices, called +‘blueprint’. This document is written for our developers and users and records +all the main architectural decision proposals that were accepted or not in the +kytos project. Blueprints are used to specify the details of any process, +resource, and anything else that is desirable to be implemented, resulting in a +major architectural change. Anyone can make enhancement proposals +writing new `blueprints`, but first it is highly recommended that you take +a look at our `blueprints section`_ to read about our past choices. +Remember that this document is not just for you, but mainly for other +developers. + Coding style ------------ @@ -232,4 +247,7 @@ must be signed. It's a quick process, we promise! .. _GitHub repository: https://github.com/kytos/ .. _forking workflow: https://www.atlassian.com/git/tutorials/comparing-workflows#forking-workflow .. _Google-style docstrings: https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments - +.. _Slack channel: https://join.slack.com/t/kytos/shared_ + invite/enQtMjk0MTM0NjQwOTE1LTE2N2UyNWE2YjNjNzY0MTNiNDNiY2JmNGFlMGQxY2I5Y2IxY + jBhMTkwZjZjNDQ4Zjk3ZjExZGFjNGYzMzRjMDM +.. _blueprints section: https://github.com/kytos/kytos/tree/master/docs/blueprints diff --git a/docs/developer/img/kytos-ui-parts.png b/docs/developer/img/kytos-ui-parts.png index 5788c5a4a..dacad4405 100644 Binary files a/docs/developer/img/kytos-ui-parts.png and b/docs/developer/img/kytos-ui-parts.png differ diff --git a/docs/developer/img/kytos-ui-parts.svg b/docs/developer/img/kytos-ui-parts.svg new file mode 100644 index 000000000..f40b61a1e --- /dev/null +++ b/docs/developer/img/kytos-ui-parts.svg @@ -0,0 +1,345 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + Kytos Logo + k-menu-bar + k-toolbar + Kytos-map + k-tabs + + + k-tabs + Kytos-map + + Kytos-info-panel + + Kytos-info-panel + + logo + k-menu-bar + k-toolbar + + k-tabs + Expanded + Compact + diff --git a/docs/developer/index.rst b/docs/developer/index.rst index 4649efff4..7daee6d55 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -88,6 +88,7 @@ Kytos Summit provides an opportunity for developers, contributors and other inte creating_a_napp auth web-ui + kytos_ui_components how_to_contribute hacking security diff --git a/docs/developer/intro.rst b/docs/developer/intro.rst index b5135921d..8ac887ab4 100644 --- a/docs/developer/intro.rst +++ b/docs/developer/intro.rst @@ -13,9 +13,11 @@ In this section you will learn about: - :doc:`Architecture Overview ` - :doc:`Preparing your Development Environment ` - :doc:`Kytos API ` +- :doc:`REST Endpoints ` - :doc:`How to develop your NApp ` - :doc:`Authentication Module ` - :doc:`Web Interface ` +- :doc:`Kytos UI Components ` - :doc:`How to Contribute ` - :doc:`Hacking ` - :doc:`Security ` diff --git a/docs/developer/kytos_ui_components.rst b/docs/developer/kytos_ui_components.rst new file mode 100644 index 000000000..fd270bba3 --- /dev/null +++ b/docs/developer/kytos_ui_components.rst @@ -0,0 +1,17 @@ +#################### +Kytos UI Components +#################### + +Bellow you can find all Kytos UI components that you can use in your code to +implement the NApp UI. + +Kytos UI Components +******************* + +.. include:: ./components.rst + + +Kytos Events +************ + +.. include:: ./events.rst diff --git a/docs/developer/rest_endpoints.rst b/docs/developer/rest_endpoints.rst index 925f7b4bc..2781ed4e0 100644 --- a/docs/developer/rest_endpoints.rst +++ b/docs/developer/rest_endpoints.rst @@ -341,6 +341,18 @@ See more details about of_lldp REST Endpoints in `kytos/of_lldp POST http://0.0.0.0:8181/api/kytos/of_lldp/v1/interfaces/enable +.. code:: console + + GET http://0.0.0.0:8181/api/kytos/of_lldp/v1/polling_time + +.. code:: console + + POST http://0.0.0.0:8181/api/kytos/of_lldp/v1/polling_time + +.. code:: console + + GET http://0.0.0.0:8181/api/kytos/of_lldp/v1/ + **kytos/of_stats** See more details about of_stats REST Endpoints in `kytos/of_stats diff --git a/docs/developer/setup_develop_environment.rst b/docs/developer/setup_develop_environment.rst index 5bceaaf83..3709657f4 100644 --- a/docs/developer/setup_develop_environment.rst +++ b/docs/developer/setup_develop_environment.rst @@ -9,20 +9,8 @@ locally. System Requirements =================== -Before create your development environment you need install some libraries to -support run `Python 3.6` packages and `git` commands. - -Ubuntu Old releases -------------------- - -If are you using an Ubuntu version less than 17.10 you need to add a PPA to -able to install Python 3.6 packages. This step is made running the command -below. - -.. code-block:: shell - - $ sudo add-apt-repository ppa:jonathonf/python-3.6 - $ sudo apt update +Before creating your development environment you need to install some libraries +to support running `Python 3` packages and `git` commands. Installing the required packages @@ -33,7 +21,7 @@ command below. .. code-block:: shell - $ sudo apt install git libpython3.6-dev python3.6 python3-venv + $ apt install git python3 python3-venv libpython3-dev Setup a Virtual Environment @@ -56,7 +44,7 @@ To create a new virtual environment named `kytos-environment` you will run: .. code-block:: shell - $ python3.6 -m venv kytos-environment + $ python3 -m venv kytos-environment After ran the command above a new folder named `kytos-environment` will be created. @@ -97,29 +85,30 @@ If you want to remove an existing virtualenv, just delete its folder .. code-block:: shell - $ rm -rf kytos-environment + $ rm -vrf kytos-environment Installing from Source ====================== -To install the kytos from source you need follow the below steps. +To install kytos from source you need to follow the steps below. Update the virtualenv --------------------- -First of all activate the virtual environment and update the pip package that +First activate the virtual environment and update the pip package that is already installed in the virtualenv, with setuptools and wheel. .. code-block:: shell (kytos-environment) $ pip install --upgrade pip setuptools wheel -Download the kytos project from github +Download the Kytos project from github -------------------------------------- -First, you need to run the commands below to clone the python-openflow, kytos-utils and kytos projects locally. +Run the commands below to clone the python-openflow, kytos-utils and kytos +projects locally. .. code-block:: shell diff --git a/docs/developer/web-ui.rst b/docs/developer/web-ui.rst index 241520152..b24e9a83f 100644 --- a/docs/developer/web-ui.rst +++ b/docs/developer/web-ui.rst @@ -116,3 +116,132 @@ If you try to press `ctrl + space` a list of actions will appear at the center of Kytos UI. This is another way to trigger actions that you think that is not suitable for the toolbar, for instance. + +Check all UI components in `Kytos UI components section <../kytos_ui_components>`_ + +Creating a NApp with UI +*********************** + +Most of the times, it is necessary for a NApp to interact with the user, for +through a graphical interface (not just REST APIs). + +We have developed `vue-js` based components (`Vuejs `_), so +that you, developer, worry as little as possible about interface. Do not worry, +you don't have to understand how `vue-js` works. + +In theory, during the construction of your NApp UI you can write any HTML code +that Kytos will render. However, we strongly recommend that you use the +components we have developed. The so-called ``k-*`` components. + + +NApp Structure with a UI +======================== + +After create a new NApp (see this `NApp tutorial +`_) the structure below +will be created. + +.. code-block:: sh + + + ├── __init__.py + └── / + ├── __init__.py + ├── kytos.json + ├── main.py + ├── README.rst + ├── settings.py + └── ui/ + ├── k-action-menu/ + ├── k-info-panel/ + ├── k-toolbar/ + └── README.rst + + +To add a new components into the kytos UI you must create a new file following +the pattern ``ui/
/.kytos``, where **
** should be +the section name where the new component will be fixed and the **** +should be the component name. You are free to choose any file name here. + +Sections +======== + +* ``k-action-menu``: Add actions and their shortcuts in `k-action-menu`; + +* ``k-info-panel``: Show generic information in `k-info-panel`; + +* ``k-toolbar``: Create the NApp UI. + + +Whether you have created a new NApp component into the sections +``k-action-menu``, ``k-toolbar`` and ``k-info-panel`` the NApp structure should +have the following structure. + + +.. code-block:: sh + + / + ├── __init__.py + └── / + ├── __init__.py + ├── kytos.json + ├── main.py + ├── README.rst + ├── settings.py + └── ui/ + ├── k-action-menu/ + │   └── main.kytos + ├── k-info-panel/ + │   └── main.kytos + ├── k-toolbar/ + │   └── main.kytos + └── README.rst + + +When the administrator enables your NApp, Kytos will automatically scan the +``ui/*`` structure to properly export to GUI. + +Component Template +================== + +The Kytos component should follow some rules: + +#. The component file should have the extension ``.kytos`` + +#. The component file must have 2 sections: + + #. template: This section must be filled with html contents to display your + graphical component.This section should have a html syntax and you can + use the Kytos components. + #. script (optional): This section has javascript code that implements your + component behaviour. + +Below an example of a Kytos component (`ui/k-toolbar/main.kytos`), that will be +displayed on our UI. + +.. code-block:: html + + + + + + +You can use any **font-awesome icon**. Kytos ui interface will read that icon +and create a new button into the **k-menu-bar**. diff --git a/kytos/core/api_server.py b/kytos/core/api_server.py index b14d80d0a..758d51baa 100644 --- a/kytos/core/api_server.py +++ b/kytos/core/api_server.py @@ -12,7 +12,7 @@ from urllib.error import HTTPError, URLError from urllib.request import urlopen, urlretrieve -from flask import Flask, jsonify, request, send_file +from flask import Blueprint, Flask, jsonify, request, send_file from flask_cors import CORS from flask_socketio import SocketIO, join_room, leave_room from werkzeug.exceptions import HTTPException @@ -65,7 +65,7 @@ def __init__(self, app_name, listen='0.0.0.0', port=8181, @self.app.errorhandler(HTTPException) def handle_exception(exception): # pylint: disable=unused-variable, invalid-name - """Return a json for HTTP errors + """Return a json for HTTP errors. This handler allows NApps to return HTTP error messages by raising a HTTPException. When raising the Exception, @@ -98,7 +98,8 @@ def register_rest_endpoint(self, url, function, methods): stacklevel=2) if url.startswith('/'): url = url[1:] - self._start_endpoint(f'/kytos/{url}', function, methods=methods) + self._start_endpoint(self.app, f'/kytos/{url}', function, + methods=methods) def start_api(self): """Start this APIServer instance API. @@ -123,7 +124,8 @@ def register_core_endpoint(self, rule, function, **options): Not used by NApps, but controller. """ - self._start_endpoint(self._CORE_PREFIX + rule, function, **options) + self._start_endpoint(self.app, self._CORE_PREFIX + rule, function, + **options) def _register_web_ui(self): """Register routes to the admin-ui homepage.""" @@ -198,7 +200,10 @@ def get_ui_components(self, section_name): def web_ui(self): """Serve the index.html page for the admin-ui.""" - return send_file(f"{self.flask_dir}/index.html") + index_path = f"{self.flask_dir}/index.html" + if os.path.exists(index_path): + return send_file(index_path) + return f"File '{index_path}' not found.", HTTPStatus.NOT_FOUND.value def update_web_ui(self, version='latest', force=True): """Update the static files for the Web UI. @@ -225,6 +230,9 @@ def update_web_ui(self, version='latest', force=True): package = urlretrieve(uri)[0] except HTTPError: return f"Uri not found {uri}." + except URLError: + self.log.warning("Error accessing URL %s.", uri) + return f"Error accessing URL {uri}." # test downloaded zip file zip_ref = zipfile.ZipFile(package, 'r') @@ -291,15 +299,27 @@ def store_route_params(function): def register_napp_endpoints(self, napp): """Add all NApp REST endpoints with @rest decorator. + We are using Flask Blueprints to register these endpoints. Blueprints + are essentially the Flask equivalent of Python modules and are used to + keep related logic and assets grouped and separated from one another. + URLs will be prefixed with ``/api/{username}/{napp_name}/``. Args: napp (Napp): Napp instance to register new endpoints. """ + # Create a Flask Blueprint for a specific NApp + napp_blueprint = Blueprint(napp.napp_id, __name__) + + # Start all endpoints for this NApp for function in self._get_decorated_functions(napp): for rule, options in function.route_params: absolute_rule = self.get_absolute_rule(rule, napp) - self._start_endpoint(absolute_rule, function, **options) + self._start_endpoint(napp_blueprint, absolute_rule, function, + **options) + + # Register this Flask Blueprint in the Flask App + self.app.register_blueprint(napp_blueprint) @staticmethod def _get_decorated_functions(napp): @@ -323,14 +343,14 @@ def get_absolute_rule(cls, rule, napp): # END decorator methods - def _start_endpoint(self, rule, function, **options): + def _start_endpoint(self, app, rule, function, **options): """Start ``function``'s endpoint. Forward parameters to ``Flask.add_url_rule`` mimicking Flask ``@route`` decorator. """ endpoint = options.pop('endpoint', None) - self.app.add_url_rule(rule, endpoint, function, **options) + app.add_url_rule(rule, endpoint, function, **options) self.log.info('Started %s - %s', rule, ', '.join(options.get('methods', self.DEFAULT_METHODS))) @@ -357,6 +377,9 @@ def remove_napp_endpoints(self, napp): self.app.url_map._rules.pop(index) # pylint: enable=protected-access + # Remove the Flask Blueprint of this NApp from the Flask App + self.app.blueprints.pop(napp.napp_id) + self.log.info('The Rest endpoints from %s were disabled.', prefix) def register_core_napp_services(self): diff --git a/kytos/core/config.py b/kytos/core/config.py index 4e62f4509..552b60b62 100644 --- a/kytos/core/config.py +++ b/kytos/core/config.py @@ -176,6 +176,9 @@ def _parse_options(self, argv): napps = options.napps_pre_installed options.napps_pre_installed = json.loads(napps) + if isinstance(options.vlan_pool, str): + options.vlan_pool = json.loads(options.vlan_pool) + return options diff --git a/kytos/core/controller.py b/kytos/core/controller.py index c30a684f6..68576314d 100644 --- a/kytos/core/controller.py +++ b/kytos/core/controller.py @@ -628,12 +628,9 @@ def set_switch_options(self, dpid): return vlan_pool = {} - try: - vlan_pool = json.loads(self.options.vlan_pool) - if not vlan_pool: - return - except (TypeError, json.JSONDecodeError) as err: - self.log.error("Invalid vlan_pool settings: %s", err) + vlan_pool = self.options.vlan_pool + if not vlan_pool: + return if vlan_pool.get(dpid): self.log.info("Loading vlan_pool configuration for dpid %s", dpid) diff --git a/kytos/core/exceptions.py b/kytos/core/exceptions.py index fad1a0a60..87a3b3f0c 100644 --- a/kytos/core/exceptions.py +++ b/kytos/core/exceptions.py @@ -77,6 +77,10 @@ def __str__(self): return msg +class KytosLinkCreationError(Exception): + """Exception thrown when the link has an empty endpoint.""" + + # Exceptions related to NApps diff --git a/kytos/core/interface.py b/kytos/core/interface.py index c0e810d9a..e9da6ff71 100644 --- a/kytos/core/interface.py +++ b/kytos/core/interface.py @@ -28,10 +28,12 @@ class TAG: """Class that represents a TAG.""" def __init__(self, tag_type, value): - self.tag_type = tag_type + self.tag_type = TAGType(tag_type) self.value = value def __eq__(self, other): + if not other: + return False return self.tag_type == other.tag_type and self.value == other.value def as_dict(self): @@ -61,7 +63,7 @@ class Interface(GenericEntity): # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-arguments def __init__(self, name, port_number, switch, address=None, state=None, - features=None, speed=None): + features=None, speed=None, config=None): """Assign the parameters to instance attributes. Args: @@ -76,6 +78,11 @@ def __init__(self, name, port_number, switch, address=None, state=None, speed (int, float): Interface speed in bytes per second. Defaults to what is informed by the switch. Return ``None`` if not set and switch does not inform the speed. + config(|port_config|): Port config used to indicate interface + behavior. In general, the port config bits are set by the + controller and are not changed by the switch. Options + are: administratively down, ignore received packets, drop + forwarded packets, and/or do not send packet-in messages. """ self.name = name self.port_number = int(port_number) @@ -83,6 +90,7 @@ def __init__(self, name, port_number, switch, address=None, state=None, self.address = address self.state = state self.features = features + self.config = config self.nni = False self.endpoints = [] self.stats = None @@ -259,6 +267,11 @@ def speed(self): int, None: Link speed in bytes per second or ``None``. """ + speed = self.get_of_features_speed() + + if speed is not None: + return speed + if self._custom_speed is not None: return self._custom_speed @@ -268,7 +281,16 @@ def speed(self): if not self._is_v0x04() and self.port_number == PortNo01.OFPP_LOCAL: return 0 - return self.get_of_features_speed() + # Warn unknown speed + # Use shorter switch ID with its beginning and end + if isinstance(self.switch.id, str) and len(self.switch.id) > 20: + switch_id = self.switch.id[:3] + '...' + self.switch.id[-3:] + else: + switch_id = self.switch.id + LOG.warning("Couldn't get port %s speed, sw %s, feats %s", + self.port_number, switch_id, self.features) + + return None def set_custom_speed(self, bytes_per_second): """Set a speed that overrides switch OpenFlow information. @@ -298,17 +320,7 @@ def get_of_features_speed(self): # Don't use switch.is_connected() because we can have the protocol if speed is None and self._is_v0x04(): speed = self._get_v0x04_speed() - if speed is not None: - return speed - # Warn unknown speed - # Use shorter switch ID with its beginning and end - if isinstance(self.switch.id, str) and len(self.switch.id) > 20: - switch_id = self.switch.id[:3] + '...' + self.switch.id[-3:] - else: - switch_id = self.switch.id - LOG.warning("Couldn't get port %s speed, sw %s, feats %s", - self.port_number, switch_id, self.features) - return None + return speed def _is_v0x04(self): """Whether the switch is connected using OpenFlow 1.3.""" @@ -448,8 +460,10 @@ def is_valid(self): def as_dict(self): """Return a dict representating a UNI object.""" - return {'interface_id': self.interface.id, - 'tag': self.user_tag.as_dict()} + return { + 'interface_id': self.interface.id, + 'tag': self.user_tag.as_dict() if self.user_tag else None + } def as_json(self): """Return a json representating a UNI object.""" diff --git a/kytos/core/kytosd.py b/kytos/core/kytosd.py index 52f943b38..e04b544ad 100644 --- a/kytos/core/kytosd.py +++ b/kytos/core/kytosd.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3.6 +#!/usr/bin/env python3 """Start Kytos SDN Platform core.""" import asyncio import functools diff --git a/kytos/core/link.py b/kytos/core/link.py index 94735362e..812017760 100644 --- a/kytos/core/link.py +++ b/kytos/core/link.py @@ -9,7 +9,8 @@ import random from kytos.core.common import GenericEntity -from kytos.core.exceptions import KytosNoTagAvailableError +from kytos.core.exceptions import (KytosLinkCreationError, + KytosNoTagAvailableError) from kytos.core.interface import TAGType @@ -22,9 +23,9 @@ def __init__(self, endpoint_a, endpoint_b): Two kytos.core.interface.Interface are required as parameters. """ if endpoint_a is None: - raise ValueError("endpoint_a cannot be None") + raise KytosLinkCreationError("endpoint_a cannot be None") if endpoint_b is None: - raise ValueError("endpoint_b cannot be None") + raise KytosLinkCreationError("endpoint_b cannot be None") self.endpoint_a = endpoint_a self.endpoint_b = endpoint_b super().__init__() diff --git a/kytos/core/metadata.py b/kytos/core/metadata.py index eeef67eeb..076972b19 100644 --- a/kytos/core/metadata.py +++ b/kytos/core/metadata.py @@ -2,7 +2,7 @@ The present metadata is intended to be used mainly on the setup. """ -__version__ = '2020.1rc1' +__version__ = '2020.2b1' __author__ = 'Kytos Team' __author_email__ = 'devel@lists.kytos.io' __license__ = 'MIT' diff --git a/kytos/lib/helpers.py b/kytos/lib/helpers.py index 1f9b771fa..5c12f862e 100644 --- a/kytos/lib/helpers.py +++ b/kytos/lib/helpers.py @@ -36,7 +36,7 @@ def get_link_mock(endpoint_a, endpoint_b): link = create_autospec(Link) link.endpoint_a = endpoint_a link.endpoint_b = endpoint_b - link.metadata = {"A": 0} + link.metadata = {"A": 0, "BB": 0.0, "CCC": "test"} return link diff --git a/requirements/dev.in b/requirements/dev.in index 54e4903bf..5e275113e 100644 --- a/requirements/dev.in +++ b/requirements/dev.in @@ -2,6 +2,7 @@ -e git+git://github.com/kytos/sphinx-theme.git#egg=kytos-sphinx-theme pip-tools >= 2.0 coverage +pydocstyle pytest yala tox diff --git a/requirements/dev.txt b/requirements/dev.txt index 36c3e54bb..a70da9edf 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -2,51 +2,73 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file=requirements/dev.txt requirements/dev.in +# pip-compile --output-file=requirements/dev.txt requirements/dev.in requirements/run.txt # -e git+git://github.com/kytos/sphinx-theme.git#egg=kytos-sphinx-theme # via -r requirements/dev.in --e git+git://github.com/kytos/python-openflow.git#egg=python-openflow # via -r requirements/dev.in +-e git+git://github.com/kytos/python-openflow.git#egg=python-openflow # via -r requirements/dev.in, -r requirements/run.txt alabaster==0.7.12 # via sphinx appdirs==1.4.3 # via virtualenv +appnope==0.1.0 # via -r requirements/run.txt argh==0.26.2 # via sphinx-autobuild astroid==2.3.3 # via pylint attrs==19.3.0 # via pytest babel==2.8.0 # via sphinx +backcall==0.1.0 # via -r requirements/run.txt, ipython certifi==2019.11.28 # via requests chardet==3.0.4 # via requests -click==7.1.1 # via pip-tools +click==7.1.1 # via -r requirements/run.txt, flask, pip-tools coverage==5.0.4 # via -r requirements/dev.in +decorator==4.4.2 # via -r requirements/run.txt, ipython, traitlets distlib==0.3.0 # via virtualenv docopt==0.6.2 # via yala -docutils==0.16 # via sphinx +docutils==0.16 # via -r requirements/run.txt, python-daemon, sphinx filelock==3.0.12 # via tox, virtualenv +flask-cors==3.0.8 # via -r requirements/run.txt +flask-socketio==4.2.1 # via -r requirements/run.txt +flask==1.1.2 # via -r requirements/run.txt, flask-cors, flask-socketio idna==2.9 # via requests imagesize==1.2.0 # via sphinx -importlib-metadata==1.6.0 # via importlib-resources, pluggy, pytest, tox, virtualenv -importlib-resources==1.4.0 # via virtualenv +importlib-metadata==1.7.0 # via importlib-resources, pluggy, pytest, tox, virtualenv +importlib-resources==1.5.0 # via virtualenv +ipython-genutils==0.2.0 # via -r requirements/run.txt, traitlets +ipython==7.13.0 # via -r requirements/run.txt isort==4.3.21 # via pylint, yala -jinja2==2.11.1 # via sphinx +itsdangerous==1.1.0 # via -r requirements/run.txt, flask +janus==0.4.0 # via -r requirements/run.txt +jedi==0.16.0 # via -r requirements/run.txt, ipython +jinja2==2.11.1 # via -r requirements/run.txt, flask, sphinx lazy-object-proxy==1.4.3 # via astroid livereload==2.6.1 # via sphinx-autobuild -markupsafe==1.1.1 # via jinja2 +lockfile==0.12.2 # via -r requirements/run.txt, python-daemon +markupsafe==1.1.1 # via -r requirements/run.txt, jinja2 mccabe==0.6.1 # via pylint more-itertools==8.2.0 # via pytest packaging==20.3 # via pytest, sphinx, tox -pathtools==0.1.2 # via sphinx-autobuild, watchdog +parso==0.6.2 # via -r requirements/run.txt, jedi +pathtools==0.1.2 # via -r requirements/run.txt, sphinx-autobuild, watchdog +pexpect==4.8.0 # via -r requirements/run.txt, ipython +pickleshare==0.7.5 # via -r requirements/run.txt, ipython pip-tools==4.5.1 # via -r requirements/dev.in pluggy==0.13.1 # via pytest, tox port_for==0.3.1 # via sphinx-autobuild +prompt-toolkit==3.0.5 # via -r requirements/run.txt, ipython +ptyprocess==0.6.0 # via -r requirements/run.txt, pexpect py==1.8.1 # via pytest, tox pycodestyle==2.5.0 # via yala -pygments==2.6.1 # via sphinx +pydocstyle==5.1.1 # via -r requirements/dev.in +pygments==2.6.1 # via -r requirements/run.txt, ipython, sphinx +pyjwt==1.7.1 # via -r requirements/run.txt pylint==2.4.4 # via yala pyparsing==2.4.6 # via packaging pytest==5.4.1 # via -r requirements/dev.in +python-daemon==2.2.4 # via -r requirements/run.txt +python-engineio==3.12.1 # via -r requirements/run.txt, python-socketio +python-socketio==4.5.1 # via -r requirements/run.txt, flask-socketio pytz==2019.3 # via babel pyyaml==5.3.1 # via sphinx-autobuild requests==2.23.0 # via sphinx -six==1.14.0 # via astroid, livereload, packaging, pip-tools, tox, virtualenv -snowballstemmer==2.0.0 # via sphinx +six==1.15.0 # via -r requirements/run.txt, astroid, flask-cors, livereload, packaging, pip-tools, python-engineio, python-socketio, tox, traitlets, virtualenv +snowballstemmer==2.0.0 # via pydocstyle, sphinx sphinx-autobuild==0.7.1 # via kytos-sphinx-theme sphinx==2.0.1 # via kytos-sphinx-theme sphinxcontrib-applehelp==1.0.2 # via sphinx @@ -58,14 +80,16 @@ sphinxcontrib-serializinghtml==1.1.4 # via sphinx toml==0.10.0 # via tox tornado==6.0.4 # via livereload, sphinx-autobuild tox==3.14.6 # via -r requirements/dev.in +traitlets==4.3.3 # via -r requirements/run.txt, ipython typed-ast==1.4.1 # via astroid urllib3==1.25.8 # via requests virtualenv==20.0.15 # via tox -watchdog==0.10.2 # via sphinx-autobuild -wcwidth==0.1.9 # via pytest +watchdog==0.10.2 # via -r requirements/run.txt, sphinx-autobuild +wcwidth==0.1.9 # via -r requirements/run.txt, prompt-toolkit, pytest +werkzeug==1.0.1 # via -r requirements/run.txt, flask wrapt==1.11.2 # via astroid yala==2.2.0 # via -r requirements/dev.in -zipp==3.1.0 # via importlib-metadata, importlib-resources +zipp==3.3.0 # via importlib-metadata, importlib-resources # The following packages are considered to be unsafe in a requirements file: # setuptools diff --git a/requirements/run.in b/requirements/run.in index 1b3775945..6a49b2742 100644 --- a/requirements/run.in +++ b/requirements/run.in @@ -2,7 +2,7 @@ # pin versions outside pypi, i.e. latest git master branch version. ipython lockfile -python-openflow>=2020.1rc1 +python-openflow>=2020.2b1 flask-socketio flask_cors flask diff --git a/requirements/run.txt b/requirements/run.txt index 4562a43d7..c464f2651 100644 --- a/requirements/run.txt +++ b/requirements/run.txt @@ -30,9 +30,9 @@ pygments==2.6.1 # via ipython pyjwt==1.7.1 # via -r requirements/run.in python-daemon==2.2.4 # via -r requirements/run.in python-engineio==3.12.1 # via python-socketio -python-openflow==2020.1rc1 # via -r requirements/run.in +python-openflow==2020.2b1 # via -r requirements/run.in python-socketio==4.5.1 # via flask-socketio -six==1.14.0 # via flask-cors, python-socketio, traitlets +six==1.15.0 # via flask-cors, python-socketio, traitlets traitlets==4.3.3 # via ipython watchdog==0.10.2 # via -r requirements/run.in wcwidth==0.1.9 # via prompt-toolkit diff --git a/setup.py b/setup.py index bd56bc68e..b7770e03d 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ import sys 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 pathlib import Path from subprocess import CalledProcessError, call, check_call @@ -17,7 +16,6 @@ import pip # pylint: disable=unused-import from setuptools import Command, find_packages, setup from setuptools.command.egg_info import egg_info - from setuptools.command.install import install except ModuleNotFoundError: print('Please install python3-pip and run setup.py again.') sys.exit(-1) @@ -100,17 +98,14 @@ def finalize_options(self): sys.exit(-1) -class Cleaner(clean): +class Cleaner(SimpleCommand): """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) + call('make clean', shell=True) class Test(TestCommand): @@ -133,6 +128,8 @@ def run(self): check_call(cmd, shell=True) except CalledProcessError as exc: print(exc) + print('Unit tests failed. Fix the error(s) above and try again.') + sys.exit(-1) class TestCoverage(Test): @@ -148,6 +145,8 @@ def run(self): check_call(cmd, shell=True) except CalledProcessError as exc: print(exc) + print('Coverage tests failed. Fix the errors above and try again.') + sys.exit(-1) class DocTest(SimpleCommand): @@ -191,21 +190,21 @@ def run(self): check_call(cmd, shell=True) -class InstallMode(install): - """Class used to overwrite the default installation using setuptools.""" +# class InstallMode(install): +# """Class used to overwrite the default installation using setuptools.""" - def run(self): - """Install the package in install mode. +# def run(self): +# """Install the package in install mode. - super().run() does not install dependencies when running - ``python setup.py install`` (pypa/setuptools#456). - """ - if 'bdist_wheel' in sys.argv: - # do not use eggs, but wheels - super().run() - else: - # force install of deps' eggs during setup.py install - self.do_egg_install() +# super().run() does not install dependencies when running +# ``python setup.py install`` (pypa/setuptools#456). +# """ +# if 'bdist_wheel' in sys.argv: +# # do not use eggs, but wheels +# super().run() +# else: +# # force install of deps' eggs during setup.py install +# self.do_egg_install() # class DevelopMode(develop): @@ -256,7 +255,6 @@ def run(self): 'coverage': TestCoverage, 'doctest': DocTest, 'egg_info': EggInfo, - 'install': InstallMode, 'lint': Linter, 'test': Test }, diff --git a/tests/unit/test_core/test_api_server.py b/tests/unit/test_core/test_api_server.py index da88e9d12..20ff2bd19 100644 --- a/tests/unit/test_core/test_api_server.py +++ b/tests/unit/test_core/test_api_server.py @@ -1,19 +1,31 @@ """APIServer tests.""" +import json import unittest import warnings # Disable not-grouped imports that conflicts with isort -from unittest.mock import Mock, sentinel # pylint: disable=C0412 +from unittest.mock import (MagicMock, Mock, patch, # pylint: disable=C0412 + sentinel) +from urllib.error import HTTPError from kytos.core.api_server import APIServer from kytos.core.napps import rest +KYTOS_CORE_API = "http://127.0.0.1:8181/api/kytos/" +API_URI = KYTOS_CORE_API+"core" + +# pylint: disable=protected-access, too-many-public-methods class TestAPIServer(unittest.TestCase): """Test the class APIServer.""" def setUp(self): """Instantiate a APIServer.""" self.api_server = APIServer('CustomName', False) + self.napps_manager = MagicMock() + self.api_server.server = MagicMock() + self.api_server.napps_manager = self.napps_manager + self.api_server.napps_dir = 'napps_dir' + self.api_server.flask_dir = 'flask_dir' def test_deprecation_warning(self): """Deprecated method should suggest @rest decorator.""" @@ -26,6 +38,359 @@ def test_deprecation_warning(self): self.assertEqual(warning.category, DeprecationWarning) self.assertIn('@rest', str(warning.message)) + def test_run(self): + """Test run method.""" + self.api_server.run() + + self.api_server.server.run.assert_called_with(self.api_server.app, + self.api_server.listen, + self.api_server.port) + + @patch('sys.exit') + def test_run_error(self, mock_exit): + """Test run method to error case.""" + self.api_server.server.run.side_effect = OSError + self.api_server.run() + + mock_exit.assert_called() + + @patch('kytos.core.api_server.request') + def test_shutdown_api(self, mock_request): + """Test shutdown_api method.""" + mock_request.host = 'localhost:8181' + + self.api_server.shutdown_api() + + self.api_server.server.stop.assert_called() + + @patch('kytos.core.api_server.request') + def test_shutdown_api__error(self, mock_request): + """Test shutdown_api method to error case.""" + mock_request.host = 'any:port' + + self.api_server.shutdown_api() + + self.api_server.server.stop.assert_not_called() + + def test_status_api(self): + """Test status_api method.""" + status = self.api_server.status_api() + self.assertEqual(status, ('{"response": "running"}', 200)) + + @patch('kytos.core.api_server.urlopen') + def test_stop_api_server(self, mock_urlopen): + """Test stop_api_server method.""" + self.api_server.stop_api_server() + + url = "%s/shutdown" % API_URI + mock_urlopen.assert_called_with(url) + + @patch('kytos.core.api_server.send_file') + @patch('os.path.exists', return_value=True) + def test_static_web_ui__success(self, *args): + """Test static_web_ui method to success case.""" + (_, mock_send_file) = args + self.api_server.static_web_ui('kytos', 'napp', 'filename') + + mock_send_file.assert_called_with('napps_dir/kytos/napp/ui/filename') + + @patch('os.path.exists', return_value=False) + def test_static_web_ui__error(self, _): + """Test static_web_ui method to error case.""" + resp, code = self.api_server.static_web_ui('kytos', 'napp', 'filename') + + self.assertEqual(resp, '') + self.assertEqual(code, 404) + + @patch('kytos.core.api_server.glob') + def test_get_ui_components(self, mock_glob): + """Test get_ui_components method.""" + with self.api_server.app.app_context(): + mock_glob.return_value = ['napps_dir/*/*/ui/*/*.kytos'] + response = self.api_server.get_ui_components('all') + + expected_json = [{'name': '*-*-*-*', 'url': 'ui/*/*/*/*.kytos'}] + self.assertEqual(response.json, expected_json) + self.assertEqual(response.status_code, 200) + + @patch('os.path') + @patch('kytos.core.api_server.send_file') + def test_web_ui__success(self, mock_send_file, ospath_mock): + """Test web_ui method.""" + ospath_mock.exists.return_value = True + self.api_server.web_ui() + + mock_send_file.assert_called_with('flask_dir/index.html') + + @patch('os.path') + def test_web_ui__error(self, ospath_mock): + """Test web_ui method.""" + ospath_mock.exists.return_value = False + _, error = self.api_server.web_ui() + + self.assertEqual(error, 404) + + @patch('kytos.core.api_server.urlretrieve') + @patch('kytos.core.api_server.urlopen') + @patch('zipfile.ZipFile') + @patch('os.path.exists') + @patch('os.mkdir') + @patch('shutil.move') + def test_update_web_ui(self, *args): + """Test update_web_ui method.""" + (_, _, mock_exists, mock_zipfile, mock_urlopen, + mock_urlretrieve) = args + zipfile = MagicMock() + zipfile.testzip.return_value = None + mock_zipfile.return_value = zipfile + + data = json.dumps({'tag_name': 1.0}) + url_response = MagicMock() + url_response.readlines.return_value = [data] + mock_urlopen.return_value = url_response + + mock_exists.side_effect = [False, True] + + response = self.api_server.update_web_ui() + + url = 'https://github.com/kytos/ui/releases/download/1.0/latest.zip' + mock_urlretrieve.assert_called_with(url) + self.assertEqual(response, 'updated the web ui') + + @patch('kytos.core.api_server.urlretrieve') + @patch('kytos.core.api_server.urlopen') + @patch('os.path.exists') + def test_update_web_ui__http_error(self, *args): + """Test update_web_ui method to http error case.""" + (mock_exists, mock_urlopen, mock_urlretrieve) = args + + data = json.dumps({'tag_name': 1.0}) + url_response = MagicMock() + url_response.readlines.return_value = [data] + mock_urlopen.return_value = url_response + mock_urlretrieve.side_effect = HTTPError('url', 123, 'msg', 'hdrs', + MagicMock()) + + mock_exists.return_value = False + + response = self.api_server.update_web_ui() + + expected_response = 'Uri not found https://github.com/kytos/ui/' + \ + 'releases/download/1.0/latest.zip.' + self.assertEqual(response, expected_response) + + @patch('kytos.core.api_server.urlretrieve') + @patch('kytos.core.api_server.urlopen') + @patch('zipfile.ZipFile') + @patch('os.path.exists') + def test_update_web_ui__zip_error(self, *args): + """Test update_web_ui method to error case in zip file.""" + (mock_exists, mock_zipfile, mock_urlopen, _) = args + zipfile = MagicMock() + zipfile.testzip.return_value = 'any' + mock_zipfile.return_value = zipfile + + data = json.dumps({'tag_name': 1.0}) + url_response = MagicMock() + url_response.readlines.return_value = [data] + mock_urlopen.return_value = url_response + + mock_exists.return_value = False + + response = self.api_server.update_web_ui() + + expected_response = 'Zip file from https://github.com/kytos/ui/' + \ + 'releases/download/1.0/latest.zip is corrupted.' + self.assertEqual(response, expected_response) + + def test_enable_napp__error_not_installed(self): + """Test _enable_napp method error case when napp is not installed.""" + self.napps_manager.is_installed.return_value = False + + resp, code = self.api_server._enable_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "not installed"}') + self.assertEqual(code, 400) + + def test_enable_napp__error_not_enabling(self): + """Test _enable_napp method error case when napp is not enabling.""" + self.napps_manager.is_installed.return_value = True + self.napps_manager.is_enabled.side_effect = [False, False] + + resp, code = self.api_server._enable_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "error"}') + self.assertEqual(code, 500) + + def test_enable_napp__success(self): + """Test _enable_napp method success case.""" + self.napps_manager.is_installed.return_value = True + self.napps_manager.is_enabled.side_effect = [False, True] + + resp, code = self.api_server._enable_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "enabled"}') + self.assertEqual(code, 200) + + def test_disable_napp__error_not_installed(self): + """Test _disable_napp method error case when napp is not installed.""" + self.napps_manager.is_installed.return_value = False + + resp, code = self.api_server._disable_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "not installed"}') + self.assertEqual(code, 400) + + def test_disable_napp__error_not_enabling(self): + """Test _disable_napp method error case when napp is not enabling.""" + self.napps_manager.is_installed.return_value = True + self.napps_manager.is_enabled.side_effect = [True, True] + + resp, code = self.api_server._disable_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "error"}') + self.assertEqual(code, 500) + + def test_disable_napp__success(self): + """Test _disable_napp method success case.""" + self.napps_manager.is_installed.return_value = True + self.napps_manager.is_enabled.side_effect = [True, False] + + resp, code = self.api_server._disable_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "disabled"}') + self.assertEqual(code, 200) + + def test_install_napp__error_not_installing(self): + """Test _install_napp method error case when napp is not installing.""" + self.napps_manager.is_installed.return_value = False + self.napps_manager.install.return_value = False + + resp, code = self.api_server._install_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "error"}') + self.assertEqual(code, 500) + + def test_install_napp__http_error(self): + """Test _install_napp method to http error case.""" + self.napps_manager.is_installed.return_value = False + self.napps_manager.install.side_effect = HTTPError('url', 123, 'msg', + 'hdrs', MagicMock()) + + resp, code = self.api_server._install_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "error"}') + self.assertEqual(code, 123) + + def test_install_napp__success_is_installed(self): + """Test _install_napp method success case when napp is installed.""" + self.napps_manager.is_installed.return_value = True + + resp, code = self.api_server._install_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "installed"}') + self.assertEqual(code, 200) + + def test_install_napp__success(self): + """Test _install_napp method success case.""" + self.napps_manager.is_installed.return_value = False + self.napps_manager.install.return_value = True + + resp, code = self.api_server._install_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "installed"}') + self.assertEqual(code, 200) + + def test_uninstall_napp__error_not_uninstalling(self): + """Test _uninstall_napp method error case when napp is not + uninstalling. + """ + self.napps_manager.is_installed.return_value = True + self.napps_manager.uninstall.return_value = False + + resp, code = self.api_server._uninstall_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "error"}') + self.assertEqual(code, 500) + + def test_uninstall_napp__success_not_installed(self): + """Test _uninstall_napp method success case when napp is not + installed. + """ + self.napps_manager.is_installed.return_value = False + + resp, code = self.api_server._uninstall_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "uninstalled"}') + self.assertEqual(code, 200) + + def test_uninstall_napp__success(self): + """Test _uninstall_napp method success case.""" + self.napps_manager.is_installed.return_value = True + self.napps_manager.uninstall.return_value = True + + resp, code = self.api_server._uninstall_napp('kytos', 'napp') + + self.assertEqual(resp, '{"response": "uninstalled"}') + self.assertEqual(code, 200) + + def test_list_enabled_napps(self): + """Test _list_enabled_napps method.""" + napp = MagicMock() + napp.username = 'kytos' + napp.name = 'name' + self.napps_manager.get_enabled_napps.return_value = [napp] + + enabled_napps, code = self.api_server._list_enabled_napps() + + self.assertEqual(enabled_napps, '{"napps": [["kytos", "name"]]}') + self.assertEqual(code, 200) + + def test_list_installed_napps(self): + """Test _list_installed_napps method.""" + napp = MagicMock() + napp.username = 'kytos' + napp.name = 'name' + self.napps_manager.get_installed_napps.return_value = [napp] + + enabled_napps, code = self.api_server._list_installed_napps() + + self.assertEqual(enabled_napps, '{"napps": [["kytos", "name"]]}') + self.assertEqual(code, 200) + + def test_get_napp_metadata__not_installed(self): + """Test _get_napp_metadata method to error case when napp is not + installed.""" + self.napps_manager.is_installed.return_value = False + resp, code = self.api_server._get_napp_metadata('kytos', 'napp', + 'version') + + self.assertEqual(resp, 'NApp is not installed.') + self.assertEqual(code, 400) + + def test_get_napp_metadata__invalid_key(self): + """Test _get_napp_metadata method to error case when key is invalid.""" + self.napps_manager.is_installed.return_value = True + resp, code = self.api_server._get_napp_metadata('kytos', 'napp', + 'any') + + self.assertEqual(resp, 'Invalid key.') + self.assertEqual(code, 400) + + def test_get_napp_metadata(self): + """Test _get_napp_metadata method.""" + data = '{"username": "kytos", \ + "name": "napp", \ + "version": "1.0"}' + self.napps_manager.is_installed.return_value = True + self.napps_manager.get_napp_metadata.return_value = data + resp, code = self.api_server._get_napp_metadata('kytos', 'napp', + 'version') + + expected_metadata = json.dumps({'version': data}) + self.assertEqual(resp, expected_metadata) + self.assertEqual(code, 200) + @staticmethod def __custom_endpoint(): """Custom method used by APIServer.""" @@ -38,13 +403,15 @@ class RESTNApp: # pylint: disable=too-few-public-methods def __init__(self): self.username = 'test' self.name = 'MyNApp' + self.napp_id = 'test/MyNApp' class TestAPIDecorator(unittest.TestCase): """@rest should have the same effect as ``Flask.route``.""" @classmethod - def test_flask_call(cls): + @patch('kytos.core.api_server.Blueprint') + def test_flask_call(cls, mock_blueprint): """@rest params should be forwarded to Flask.""" rule = 'rule' # Use sentinels to be sure they are not changed. @@ -57,9 +424,12 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods def my_endpoint(self): """Do nothing.""" + blueprint = Mock() + mock_blueprint.return_value = blueprint + napp = MyNApp() - server = cls._mock_api_server(napp) - server.app.add_url_rule.assert_called_once_with( + cls._mock_api_server(napp) + blueprint.add_url_rule.assert_called_once_with( '/api/test/MyNApp/' + rule, None, napp.my_endpoint, **options) @classmethod @@ -71,6 +441,7 @@ class MyNApp: # pylint: disable=too-few-public-methods def __init__(self): self.username = 'test' self.name = 'MyNApp' + self.napp_id = 'test/MyNApp' napp = MyNApp() server = cls._mock_api_server(napp) @@ -92,9 +463,11 @@ def __init__(self): # pylint: disable=protected-access server.app.url_map._rules.pop.assert_called_once_with(0) # pylint: enable=protected-access + server.app.blueprints.pop.assert_called_once_with(napp.napp_id) @classmethod - def test_rule_with_slash(cls): + @patch('kytos.core.api_server.Blueprint') + def test_rule_with_slash(cls, mock_blueprint): """There should be no double slashes in a rule.""" class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods """API decorator example usage.""" @@ -103,10 +476,13 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods def my_endpoint(self): """Do nothing.""" - cls._assert_rule_is_added(MyNApp) + blueprint = Mock() + mock_blueprint.return_value = blueprint + cls._assert_rule_is_added(MyNApp, blueprint) @classmethod - def test_rule_from_classmethod(cls): + @patch('kytos.core.api_server.Blueprint') + def test_rule_from_classmethod(cls, mock_blueprint): """Use class methods as endpoints as well.""" class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods """API decorator example usage.""" @@ -116,10 +492,13 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods def my_endpoint(cls): """Do nothing.""" - cls._assert_rule_is_added(MyNApp) + blueprint = Mock() + mock_blueprint.return_value = blueprint + cls._assert_rule_is_added(MyNApp, blueprint) @classmethod - def test_rule_from_staticmethod(cls): + @patch('kytos.core.api_server.Blueprint') + def test_rule_from_staticmethod(cls, mock_blueprint): """Use static methods as endpoints as well.""" class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods """API decorator example usage.""" @@ -129,14 +508,16 @@ class MyNApp(RESTNApp): # pylint: disable=too-few-public-methods def my_endpoint(): """Do nothing.""" - cls._assert_rule_is_added(MyNApp) + blueprint = Mock() + mock_blueprint.return_value = blueprint + cls._assert_rule_is_added(MyNApp, blueprint) @classmethod - def _assert_rule_is_added(cls, napp_class): + def _assert_rule_is_added(cls, napp_class, blueprint): """Assert Flask's add_url_rule was called with the right parameters.""" napp = napp_class() - server = cls._mock_api_server(napp) - server.app.add_url_rule.assert_called_once_with( + cls._mock_api_server(napp) + blueprint.add_url_rule.assert_called_once_with( '/api/test/MyNApp/rule', None, napp.my_endpoint) @staticmethod diff --git a/tests/unit/test_core/test_atcp_server.py b/tests/unit/test_core/test_atcp_server.py index ce74a084e..db0d93939 100644 --- a/tests/unit/test_core/test_atcp_server.py +++ b/tests/unit/test_core/test_atcp_server.py @@ -3,6 +3,7 @@ import asyncio import errno import logging +from unittest.mock import MagicMock, patch from kytos.core.atcp_server import (KytosServer, KytosServerProtocol, exception_handler) @@ -63,3 +64,50 @@ def test_exception_handler_oserror(self, caplog): logging.INFO, "Socket closed: 'unit_tests'"), ] + + +class TestKytosServerProtocol: + """KytosServerProtocol tests.""" + + def setup(self): + """Instantiate a KytosServerProtocol.""" + # pylint: disable=attribute-defined-outside-init + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + self.connection = MagicMock() + self.connection.address = 'addr' + self.connection.port = 123 + + self.server_protocol = KytosServerProtocol() + self.server_protocol.server = MagicMock() + self.server_protocol.connection = self.connection + + @patch('kytos.core.atcp_server.KytosEvent') + def test_data_received(self, mock_kytos_event): + """Test data_received method.""" + buffers = self.server_protocol.server.controller.buffers + self.connection.protocol.name = 'protocol' + + self.server_protocol.data_received(b'data') + + expected_content = {'source': self.connection, 'new_data': b'data'} + expected_name = 'kytos/core.protocol.raw.in' + mock_kytos_event.assert_called_with(content=expected_content, + name=expected_name) + buffers.raw.aput.assert_called_with(mock_kytos_event.return_value) + + @patch('kytos.core.atcp_server.KytosEvent') + def test_connection_lost(self, mock_kytos_event): + """Test connection_lost method.""" + buffers = self.server_protocol.server.controller.buffers + self.connection.protocol.name = 'protocol' + + self.server_protocol.connection_lost('exc') + + self.connection.close.assert_called() + expected_content = {'source': self.connection, 'exception': 'exc'} + expected_name = 'kytos/core.protocol.connection.lost' + mock_kytos_event.assert_called_with(content=expected_content, + name=expected_name) + buffers.app.aput.assert_called_with(mock_kytos_event.return_value) diff --git a/tests/unit/test_core/test_auth.py b/tests/unit/test_core/test_auth.py index b84d772d7..3f6f46763 100644 --- a/tests/unit/test_core/test_auth.py +++ b/tests/unit/test_core/test_auth.py @@ -56,7 +56,6 @@ def _get_controller_mock(self): @staticmethod def get_auth_test_client(auth): """Return a flask api test client.""" - auth.controller.api_server.register_napp_endpoints(auth) return auth.controller.api_server.app.test_client() @patch('kytos.core.auth.Auth._create_superuser') diff --git a/tests/unit/test_core/test_buffers.py b/tests/unit/test_core/test_buffers.py new file mode 100644 index 000000000..e2376f6a8 --- /dev/null +++ b/tests/unit/test_core/test_buffers.py @@ -0,0 +1,128 @@ +"""Test kytos.core.buffers module.""" +import asyncio +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from kytos.core.buffers import KytosBuffers, KytosEventBuffer + + +# pylint: disable=protected-access +class TestKytosEventBuffer(TestCase): + """KytosEventBuffer tests.""" + + def setUp(self): + """Instantiate a KytosEventBuffer.""" + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + self.kytos_event_buffer = KytosEventBuffer('name', loop=self.loop) + + @staticmethod + def create_event_mock(name='any'): + """Create a new event mock.""" + event = MagicMock() + event.name = name + return event + + def test_put_get(self): + """Test put and get methods.""" + event = self.create_event_mock() + + self.kytos_event_buffer.put(event) + queue_event = self.kytos_event_buffer.get() + + self.assertEqual(queue_event, event) + + def test_put__shutdown(self): + """Test put method to shutdown event.""" + event = self.create_event_mock('kytos/core.shutdown') + self.kytos_event_buffer.put(event) + + self.assertTrue(self.kytos_event_buffer._reject_new_events) + + def test_aput(self): + """Test aput async method.""" + event = MagicMock() + event.name = 'kytos/core.shutdown' + + self.loop.run_until_complete(self.kytos_event_buffer.aput(event)) + + self.assertTrue(self.kytos_event_buffer._reject_new_events) + + def test_aget(self): + """Test aget async method.""" + event = self.create_event_mock() + self.kytos_event_buffer._queue.sync_q.put(event) + + expected = self.loop.run_until_complete(self.kytos_event_buffer.aget()) + + self.assertEqual(event, expected) + + @patch('janus._SyncQueueProxy.task_done') + def test_task_done(self, mock_task_done): + """Test task_done method.""" + self.kytos_event_buffer.task_done() + + mock_task_done.assert_called() + + @patch('janus._SyncQueueProxy.join') + def test_join(self, mock_join): + """Test join method.""" + self.kytos_event_buffer.join() + + mock_join.assert_called() + + def test_qsize(self): + """Test qsize method to empty and with one event in query.""" + qsize_1 = self.kytos_event_buffer.qsize() + + event = self.create_event_mock() + self.kytos_event_buffer._queue.sync_q.put(event) + + qsize_2 = self.kytos_event_buffer.qsize() + + self.assertEqual(qsize_1, 0) + self.assertEqual(qsize_2, 1) + + def test_empty(self): + """Test empty method to empty and with one event in query.""" + empty_1 = self.kytos_event_buffer.empty() + + event = self.create_event_mock() + self.kytos_event_buffer._queue.sync_q.put(event) + + empty_2 = self.kytos_event_buffer.empty() + + self.assertTrue(empty_1) + self.assertFalse(empty_2) + + @patch('janus._SyncQueueProxy.full') + def test_full(self, mock_full): + """Test full method to full and not full query.""" + mock_full.side_effect = [False, True] + + full_1 = self.kytos_event_buffer.full() + full_2 = self.kytos_event_buffer.full() + + self.assertFalse(full_1) + self.assertTrue(full_2) + + +class TestKytosBuffers(TestCase): + """KytosBuffers tests.""" + + def setUp(self): + """Instantiate a KytosBuffers.""" + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + + self.kytos_buffers = KytosBuffers(loop=self.loop) + + def test_send_stop_signal(self): + """Test send_stop_signal method.""" + self.kytos_buffers.send_stop_signal() + + self.assertTrue(self.kytos_buffers.raw._reject_new_events) + self.assertTrue(self.kytos_buffers.msg_in._reject_new_events) + self.assertTrue(self.kytos_buffers.msg_out._reject_new_events) + self.assertTrue(self.kytos_buffers.app._reject_new_events) diff --git a/tests/unit/test_core/test_common.py b/tests/unit/test_core/test_common.py new file mode 100644 index 000000000..ce9da6ddc --- /dev/null +++ b/tests/unit/test_core/test_common.py @@ -0,0 +1,177 @@ +"""Test kytos.core.common module.""" +from unittest import TestCase +from unittest.mock import MagicMock + +from kytos.core.common import GenericEntity + + +# pylint: disable=protected-access, too-many-public-methods +class TestGenericEntity(TestCase): + """GenericEntity tests.""" + + def setUp(self): + """Instantiate a GenericEntity.""" + self.generic_entity = GenericEntity() + self.generic_entity._active = True + self.generic_entity._enabled = True + + def test_is_enabled__true(self): + """Test is_enabled method if _enabled is true.""" + enabled = self.generic_entity.is_enabled() + + self.assertTrue(enabled) + + def test_is_enabled__false(self): + """Test is_enabled method if _enabled is false.""" + self.generic_entity._enabled = False + enabled = self.generic_entity.is_enabled() + + self.assertFalse(enabled) + + def test_is_active__true(self): + """Test is_active method if _active is true.""" + active = self.generic_entity.is_active() + + self.assertTrue(active) + + def test_is_active__false(self): + """Test is_active method if _active is false.""" + self.generic_entity._active = False + active = self.generic_entity.is_active() + + self.assertFalse(active) + + def test_activate(self): + """Test activate method.""" + self.generic_entity.activate() + + self.assertTrue(self.generic_entity._active) + + def test_deactivate(self): + """Test deactivate method.""" + self.generic_entity.deactivate() + + self.assertFalse(self.generic_entity._active) + + def test_enable(self): + """Test enable method.""" + self.generic_entity.enable() + + self.assertTrue(self.generic_entity._enabled) + + def test_disable(self): + """Test disable method.""" + self.generic_entity.disable() + + self.assertFalse(self.generic_entity._enabled) + + def test_status__up(self): + """Test status property if _enabled and _active are true.""" + self.generic_entity._enabled = True + self.generic_entity._active = True + + status = self.generic_entity.status + + self.assertEqual(status.value, 1) + + def test_status__disabled(self): + """Test status property if _enabled is false.""" + self.generic_entity._enabled = False + + status = self.generic_entity.status + + self.assertEqual(status.value, 2) + + def test_status__down(self): + """Test status property if _active is false.""" + self.generic_entity._active = False + + status = self.generic_entity.status + + self.assertEqual(status.value, 3) + + def test_add_metadata__exists(self): + """Test add_metadata method if metadata exists.""" + self.generic_entity.metadata = {'ABC': 123} + + self.generic_entity.add_metadata('ABC', 123) + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {'ABC': 123}) + + def test_add_metadata__not_exists(self): + """Test add_metadata method if metadata does not exist.""" + self.generic_entity.add_metadata('ABC', 123) + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {'ABC': 123}) + + def test_remove_metadata__exists(self): + """Test remove_metadata method if metadata exists.""" + self.generic_entity.metadata = {'ABC': 123} + + self.generic_entity.remove_metadata('ABC') + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {}) + + def test_remove_metadata__not_exists(self): + """Test remove_metadata method if metadata does not exist.""" + self.generic_entity.remove_metadata('ABC') + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {}) + + def test_get_metadata(self): + """Test get_metadata method.""" + self.generic_entity.metadata = {'ABC': 123} + + value = self.generic_entity.get_metadata('ABC') + + self.assertEqual(value, 123) + + def test_get_metadata_as_dict(self): + """Test get_metadata_as_dict method.""" + value = MagicMock() + value.as_dict.return_value = {'A': 1, 'B': 2, 'C': 3} + self.generic_entity.metadata = {'ABC': value} + + metadata = self.generic_entity.get_metadata_as_dict() + + self.assertEqual(metadata, {'ABC': {'A': 1, 'B': 2, 'C': 3}}) + + def test_update_metadata(self): + """Test update_metadata method.""" + self.generic_entity.metadata = {'ABC': 123} + + self.generic_entity.update_metadata('ABC', 456) + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {'ABC': 456}) + + def test_clear_metadata(self): + """Test clear_metadata method.""" + self.generic_entity.metadata = {'ABC': 123} + + self.generic_entity.clear_metadata() + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {}) + + def test_extend_metadata__not_force(self): + """Test extend_metadata method by not forcing update.""" + self.generic_entity.metadata = {'ABC': 123} + + self.generic_entity.extend_metadata({'ABC': 456, 'DEF': 789}, False) + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {'ABC': 123, 'DEF': 789}) + + def test_extend_metadata__force(self): + """Test extend_metadata method by forcing update.""" + self.generic_entity.metadata = {'ABC': 123} + + self.generic_entity.extend_metadata({'ABC': 456, 'DEF': 789}, True) + + metadata = self.generic_entity.metadata + self.assertEqual(metadata, {'ABC': 456, 'DEF': 789}) diff --git a/tests/unit/test_core/test_connection.py b/tests/unit/test_core/test_connection.py new file mode 100644 index 000000000..5d726c7f4 --- /dev/null +++ b/tests/unit/test_core/test_connection.py @@ -0,0 +1,113 @@ +"""Test kytos.core.connection module.""" +from socket import error as SocketError +from unittest import TestCase +from unittest.mock import MagicMock + +from kytos.core.connection import Connection, ConnectionState + + +class TestConnection(TestCase): + """Connection tests.""" + + def setUp(self): + """Instantiate a Connection.""" + socket = MagicMock() + switch = MagicMock() + self.connection = Connection('addr', 123, socket, switch) + + switch.connection = self.connection + + def test__str__(self): + """Test __str__ method.""" + self.assertEqual(str(self.connection), "Connection('addr', 123)") + + def test__repr__(self): + """Test __repr__ method.""" + self.connection.socket = 'socket' + self.connection.switch = 'switch' + + expected = "Connection('addr', 123, 'socket', 'switch', " + \ + ")" + self.assertEqual(repr(self.connection), expected) + + def test_state(self): + """Test state property.""" + self.assertEqual(self.connection.state.value, 0) + + self.connection.state = ConnectionState.FINISHED + self.assertEqual(self.connection.state.value, 4) + + def test_state__error(self): + """Test state property to error case.""" + with self.assertRaises(Exception): + self.connection.state = 1000 + + def test_id(self): + """Test id property.""" + self.assertEqual(self.connection.id, ('addr', 123)) + + def test_send(self): + """Test send method.""" + self.connection.send(b'data') + + self.connection.socket.sendall.assert_called_with(b'data') + + def test_send__error(self): + """Test send method to error case.""" + self.connection.socket.sendall.side_effect = SocketError + + self.connection.send(b'data') + + self.assertIsNone(self.connection.socket) + + def test_close(self): + """Test close method.""" + self.connection.close() + + self.assertIsNone(self.connection.socket) + + def test_close__os_error(self): + """Test close method to OSError case.""" + self.connection.socket.shutdown.side_effect = OSError + + with self.assertRaises(OSError): + self.connection.close() + + self.assertIsNotNone(self.connection.socket) + + def test_close__attribute_error(self): + """Test close method to AttributeError case.""" + self.connection.socket = None + + self.connection.close() + + self.assertIsNone(self.connection.socket) + + def test_is_alive(self): + """Test is_alive method to True and False returns.""" + self.assertTrue(self.connection.is_alive()) + + self.connection.state = ConnectionState.FINISHED + self.assertFalse(self.connection.is_alive()) + + def test_is_new(self): + """Test is_new method.""" + self.assertTrue(self.connection.is_new()) + + def test_established_state(self): + """Test set_established_state and is_established methods.""" + self.connection.set_established_state() + self.assertTrue(self.connection.is_established()) + + def test_setup_state(self): + """Test set_setup_state and is_during_setup methods.""" + self.connection.set_setup_state() + self.assertTrue(self.connection.is_during_setup()) + + def test_update_switch(self): + """Test update_switch method.""" + switch = MagicMock() + self.connection.update_switch(switch) + + self.assertEqual(self.connection.switch, switch) + self.assertEqual(switch.connection, self.connection) diff --git a/tests/unit/test_core/test_controller.py b/tests/unit/test_core/test_controller.py index bc1ed3aa7..20524f78b 100644 --- a/tests/unit/test_core/test_controller.py +++ b/tests/unit/test_core/test_controller.py @@ -2,16 +2,19 @@ import asyncio import json import logging +import sys +import tempfile import warnings from copy import copy from unittest import TestCase -from unittest.mock import Mock, patch +from unittest.mock import MagicMock, Mock, call, patch from kytos.core import Controller from kytos.core.config import KytosConfig from kytos.core.logs import LogManager +# pylint: disable=protected-access, too-many-public-methods class TestController(TestCase): """Controller tests.""" @@ -19,10 +22,12 @@ def setUp(self): """Instantiate a controller.""" self.loop = asyncio.new_event_loop() - asyncio.set_event_loop(None) + asyncio.set_event_loop(self.loop) self.options = KytosConfig().options['daemon'] + self.napps_manager = Mock() self.controller = Controller(self.options, loop=self.loop) + self.controller.napps_manager = self.napps_manager self.controller.log = Mock() def test_configuration_endpoint(self): @@ -55,7 +60,8 @@ def test_websocket_log_usage(path, log_manager): # Restore original state logging.root.handlers = handlers_bak - def test_unload_napp_listener(self): + @patch('kytos.core.api_server.APIServer.remove_napp_endpoints') + def test_unload_napp_listener(self, _): """Call NApp shutdown listener on unload.""" username, napp_name = 'test', 'napp' listener = self._add_napp(username, napp_name) @@ -64,7 +70,8 @@ def test_unload_napp_listener(self): self.controller.unload_napp(username, napp_name) listener.assert_called() - def test_unload_napp_other_listener(self): + @patch('kytos.core.api_server.APIServer.remove_napp_endpoints') + def test_unload_napp_other_listener(self, _): """Should not call other NApps' shutdown listener on unload.""" username, napp_name = 'test', 'napp1' self._add_napp(username, napp_name) @@ -156,3 +163,537 @@ def test_debug_wrong_name(self): """Test the enable debug logger with wrong name.""" self.assertRaises(ValueError, self.controller.toggle_debug, name="foobar") + + @patch('kytos.core.controller.Controller.start_controller') + @patch('kytos.core.controller.Controller.create_pidfile') + @patch('kytos.core.controller.Controller.enable_logs') + def test_start(self, *args): + """Test activate method.""" + (mock_enable_logs, mock_create_pidfile, mock_start_controller) = args + self.controller.start() + + mock_enable_logs.assert_called() + mock_create_pidfile.assert_called() + mock_start_controller.assert_called() + + @patch('os.getpid') + @patch('kytos.core.controller.atexit') + def test_create_pidfile(self, *args): + """Test activate method.""" + (_, mock_getpid) = args + mock_getpid.return_value = 2 + with tempfile.NamedTemporaryFile() as tmp_file: + tmp_file.write(b'1') + tmp_file.seek(0) + self.controller.options.pidfile = tmp_file.name + + self.controller.create_pidfile() + + pid = tmp_file.read() + self.assertEqual(pid, b'2') + + @staticmethod + @patch('kytos.core.controller.KytosServer') + @patch('kytos.core.controller.Controller.app_event_handler') + @patch('kytos.core.controller.Controller.msg_out_event_handler') + @patch('kytos.core.controller.Controller.msg_in_event_handler') + @patch('kytos.core.controller.Controller.raw_event_handler') + @patch('kytos.core.controller.Controller.load_napps') + @patch('kytos.core.controller.Controller.pre_install_napps') + def test_start_controller(*args): + """Test activate method.""" + (mock_pre_install_napps, mock_load_napps, mock_raw_event_handler, + mock_msg_in_event_handler, mock_msg_out_event_handler, + mock_app_event_handler, _) = args + + napp = MagicMock() + loop = MagicMock() + options = KytosConfig().options['daemon'] + options.napps_pre_installed = [napp] + controller = Controller(options, loop=loop) + controller.log = Mock() + + controller.start_controller() + + controller.server.serve_forever.assert_called() + calls = [call(mock_raw_event_handler.return_value), + call(mock_msg_in_event_handler.return_value), + call(mock_msg_out_event_handler.return_value), + call(mock_app_event_handler.return_value)] + loop.create_task.assert_has_calls(calls) + mock_pre_install_napps.assert_called_with([napp]) + mock_load_napps.assert_called() + + @patch('kytos.core.controller.Controller.__init__') + @patch('kytos.core.controller.Controller.start') + @patch('kytos.core.controller.Controller.stop') + def test_restart(self, *args): + """Test restart method.""" + (mock_stop, mock_start, mock_init) = args + self.controller.started_at = 1 + + graceful = True + self.controller.restart(graceful) + + mock_stop.assert_called_with(graceful) + mock_init.assert_called_with(self.controller.options) + mock_start.assert_called_with(restart=True) + + @patch('kytos.core.controller.Controller.stop_controller') + def test_stop(self, mock_stop_controller): + """Test stop method.""" + self.controller.started_at = 1 + + graceful = True + self.controller.stop(graceful) + + mock_stop_controller.assert_called_with(graceful) + + @patch('kytos.core.controller.Controller.unload_napps') + @patch('kytos.core.controller.KytosBuffers') + def test_stop_controller(self, *args): + """Test stop_controller method.""" + (_, mock_unload_napps) = args + server = MagicMock() + buffers = MagicMock() + api_server = MagicMock() + napp_dir_listener = MagicMock() + pool = MagicMock() + self.controller.server = server + self.controller.buffers = buffers + self.controller.api_server = api_server + self.controller.napp_dir_listener = napp_dir_listener + self.controller._pool = pool + + self.controller.stop_controller() + + buffers.send_stop_signal.assert_called() + api_server.stop_api_server.assert_called() + napp_dir_listener.stop.assert_called() + pool.shutdown.assert_called() + mock_unload_napps.assert_called() + server.shutdown.assert_called() + + def test_status(self): + """Test status method.""" + status_1 = self.controller.status() + self.controller.started_at = 1 + status_2 = self.controller.status() + + self.assertEqual(status_1, 'Stopped') + self.assertEqual(status_2, 'Running since 1') + + @patch('kytos.core.controller.now') + def test_uptime(self, mock_now): + """Test uptime method.""" + mock_now.return_value = 11 + + uptime_1 = self.controller.uptime() + self.controller.started_at = 1 + uptime_2 = self.controller.uptime() + + self.assertEqual(uptime_1, 0) + self.assertEqual(uptime_2, 10) + + def test_metadata_endpoint(self): + """Test metadata_endpoint method.""" + metadata = self.controller.metadata_endpoint() + json_metadata = json.loads(metadata) + + expected_keys = ['__version__', '__author__', '__license__', '__url__', + '__description__'] + self.assertEqual(list(json_metadata.keys()), expected_keys) + + def test_notify_listeners(self): + """Test notify_listeners method.""" + method = MagicMock() + self.controller.events_listeners = {'kytos/any': [method]} + + event = MagicMock() + event.name = 'kytos/any' + self.controller.notify_listeners(event) + + method.assert_called_with(event) + + def test_get_interface_by_id__not_interface(self): + """Test get_interface_by_id method when interface does not exist.""" + resp_interface = self.controller.get_interface_by_id(None) + + self.assertIsNone(resp_interface) + + def test_get_interface_by_id__not_switch(self): + """Test get_interface_by_id method when switch does not exist.""" + interface = MagicMock() + switch = MagicMock() + switch.interfaces = {123: interface} + self.controller.switches = {'00:00:00:00:00:00:00:02': switch} + + interface_id = '00:00:00:00:00:00:00:01:123' + resp_interface = self.controller.get_interface_by_id(interface_id) + + self.assertIsNone(resp_interface) + + def test_get_interface_by_id(self): + """Test get_interface_by_id method.""" + interface = MagicMock() + switch = MagicMock() + switch.interfaces = {123: interface} + self.controller.switches = {'00:00:00:00:00:00:00:01': switch} + + interface_id = '00:00:00:00:00:00:00:01:123' + resp_interface = self.controller.get_interface_by_id(interface_id) + + self.assertEqual(resp_interface, interface) + + def test_get_switch_by_dpid(self): + """Test get_switch_by_dpid method.""" + dpid = '00:00:00:00:00:00:00:01' + switch = MagicMock(dpid=dpid) + self.controller.switches = {dpid: switch} + + resp_switch = self.controller.get_switch_by_dpid(dpid) + + self.assertEqual(resp_switch, switch) + + def test_get_switch_or_create__exists(self): + """Test status_api method when switch exists.""" + dpid = '00:00:00:00:00:00:00:01' + switch = MagicMock(dpid=dpid) + self.controller.switches = {dpid: switch} + + connection = MagicMock() + resp_switch = self.controller.get_switch_or_create(dpid, connection) + + self.assertEqual(resp_switch, switch) + + def test_get_switch_or_create__not_exists(self): + """Test status_api method when switch does not exist.""" + self.controller.switches = {} + + dpid = '00:00:00:00:00:00:00:01' + connection = MagicMock() + switch = self.controller.get_switch_or_create(dpid, connection) + + expected_switches = {'00:00:00:00:00:00:00:01': switch} + self.assertEqual(self.controller.switches, expected_switches) + + def test_create_or_update_connection(self): + """Test create_or_update_connection method.""" + self.controller.connections = {} + + connection = MagicMock() + connection.id = '123' + self.controller.create_or_update_connection(connection) + + self.assertEqual(self.controller.connections, {'123': connection}) + + def test_get_connection_by_id(self): + """Test get_connection_by_id method.""" + connection = MagicMock() + connection.id = '123' + self.controller.connections = {connection.id: connection} + + resp_connection = self.controller.get_connection_by_id('123') + + self.assertEqual(resp_connection, connection) + + def test_remove_connection(self): + """Test remove_connection method.""" + connection = MagicMock() + connection.id = '123' + self.controller.connections = {connection.id: connection} + + self.controller.remove_connection(connection) + + self.assertEqual(self.controller.connections, {}) + + def test_remove_switch(self): + """Test remove_switch method.""" + switch = MagicMock() + switch.dpid = '00:00:00:00:00:00:00:01' + self.controller.switches = {switch.dpid: switch} + + self.controller.remove_switch(switch) + + self.assertEqual(self.controller.switches, {}) + + def test_remove_switch__error(self): + """Test remove_switch method to error case.""" + switch_1 = MagicMock() + switch_2 = MagicMock() + switch_1.dpid = '00:00:00:00:00:00:00:01' + switch_2.dpid = '00:00:00:00:00:00:00:02' + self.controller.switches = {switch_1.dpid: switch_1} + + self.controller.remove_switch(switch_2) + + self.assertEqual(self.controller.switches, {switch_1.dpid: switch_1}) + + def test_new_connection(self): + """Test new_connection method.""" + self.controller.connections = {} + + connection = MagicMock() + connection.id = '123' + event = MagicMock() + event.source = connection + self.controller.new_connection(event) + + self.assertEqual(self.controller.connections, {'123': connection}) + + def test_add_new_switch(self): + """Test add_new_switch method.""" + self.controller.switches = {} + + switch = MagicMock() + switch.dpid = '00:00:00:00:00:00:00:01' + self.controller.add_new_switch(switch) + + expected_switches = {'00:00:00:00:00:00:00:01': switch} + self.assertEqual(self.controller.switches, expected_switches) + + @patch('kytos.core.controller.module_from_spec') + @patch('kytos.core.controller.spec_from_file_location') + def test_import_napp(self, *args): + """Test _import_napp method.""" + (mock_spec_from_file, mock_module_from_spec) = args + napp_spec = MagicMock() + napp_spec.name = 'spec_name' + mock_spec_from_file.return_value = napp_spec + napp_module = MagicMock() + mock_module_from_spec.return_value = napp_module + + self.controller.options.napps = 'napps' + self.controller._import_napp('kytos', 'napp') + + self.assertEqual(sys.modules[napp_spec.name], napp_module) + mock_spec_from_file.assert_called_with('napps.kytos.napp.main', + 'napps/kytos/napp/main.py') + napp_spec.loader.exec_module.assert_called_with(napp_module) + + def test_load_napp__loaded(self): + """Test load_napp method when napp is already loaded.""" + napp = MagicMock() + self.controller.napps = {('kytos', 'napp'): napp} + + self.controller.load_napp('kytos', 'napp') + + self.assertEqual(self.controller.napps, {('kytos', 'napp'): napp}) + + @patch('kytos.core.controller.Controller._import_napp') + def test_load_napp__module_not_found(self, mock_import_napp): + """Test load_napp method when module is not found.""" + mock_import_napp.side_effect = ModuleNotFoundError + self.controller.napps = {} + + self.controller.load_napp('kytos', 'napp') + + self.assertEqual(self.controller.napps, {}) + + @patch('kytos.core.controller.Controller._import_napp') + def test_load_napp__file_not_found(self, mock_import_napp): + """Test load_napp method when file is not found.""" + mock_import_napp.side_effect = FileNotFoundError + self.controller.napps = {} + + self.controller.load_napp('kytos', 'napp') + + self.assertEqual(self.controller.napps, {}) + + @patch('kytos.core.api_server.APIServer.register_napp_endpoints') + @patch('kytos.core.controller.Controller._import_napp') + def test_load_napp__error(self, *args): + """Test load_napp method when an error is raised on napp module + attribution.""" + (mock_import_napp, _) = args + self.controller.napps = {} + + module = MagicMock() + module.Main.side_effect = Exception + mock_import_napp.return_value = module + + self.controller.load_napp('kytos', 'napp') + + self.assertEqual(self.controller.napps, {}) + + @patch('kytos.core.api_server.APIServer.register_napp_endpoints') + @patch('kytos.core.controller.Controller._import_napp') + def test_load_napp(self, *args): + """Test load_napp method.""" + (mock_import_napp, mock_register) = args + self.controller.napps = {} + + napp = MagicMock() + module = MagicMock() + module.Main.return_value = napp + mock_import_napp.return_value = module + + self.controller.load_napp('kytos', 'napp') + + self.assertEqual(self.controller.napps, {('kytos', 'napp'): napp}) + napp.start.assert_called() + mock_register.assert_called_with(napp) + + def test_pre_install_napps(self): + """Test pre_install_napps method.""" + napp_1 = MagicMock() + napp_2 = MagicMock() + installed_napps = [napp_1] + napps = [str(napp_1), str(napp_2)] + self.napps_manager.get_installed_napps.return_value = installed_napps + + self.controller.pre_install_napps(napps) + + self.napps_manager.install.assert_called_with(str(napp_2), enable=True) + + @patch('kytos.core.controller.Controller.load_napp') + def test_load_napps(self, mock_load): + """Test load_napps method.""" + napp = MagicMock() + napp.username = 'kytos' + napp.name = 'name' + enabled_napps = [napp] + self.napps_manager.get_enabled_napps.return_value = enabled_napps + + self.controller.load_napps() + + mock_load.assert_called_with('kytos', 'name') + + @patch('kytos.core.controller.import_module') + def test_reload_napp_module__module_not_found(self, mock_import_module): + """Test reload_napp_module method when module is not found.""" + mock_import_module.side_effect = ModuleNotFoundError + + with self.assertRaises(ModuleNotFoundError): + self.controller.reload_napp_module('kytos', 'napp', 'napp_file') + + @patch('kytos.core.controller.reload_module') + @patch('kytos.core.controller.import_module') + def test_reload_napp_module__import_error(self, *args): + """Test reload_napp_module method when an import error occurs.""" + (mock_import_module, mock_reload_module) = args + napp_module = MagicMock() + mock_import_module.return_value = napp_module + mock_reload_module.side_effect = ImportError + + with self.assertRaises(ImportError): + self.controller.reload_napp_module('kytos', 'napp', 'napp_file') + + @patch('kytos.core.controller.reload_module') + @patch('kytos.core.controller.import_module') + def test_reload_napp_module(self, *args): + """Test reload_napp_module method.""" + (mock_import_module, mock_reload_module) = args + napp_module = MagicMock() + mock_import_module.return_value = napp_module + + self.controller.reload_napp_module('kytos', 'napp', 'napp_file') + + mock_import_module.assert_called_with('napps.kytos.napp.napp_file') + mock_reload_module.assert_called_with(napp_module) + + @patch('kytos.core.controller.Controller.load_napp') + @patch('kytos.core.controller.Controller.unload_napp') + @patch('kytos.core.controller.Controller.reload_napp_module') + def test_reload_napp(self, *args): + """Test reload_napp method.""" + (mock_reload_napp_module, mock_unload, mock_load) = args + + code = self.controller.reload_napp('kytos', 'napp') + + mock_unload.assert_called_with('kytos', 'napp') + calls = [call('kytos', 'napp', 'settings'), + call('kytos', 'napp', 'main')] + mock_reload_napp_module.assert_has_calls(calls) + mock_load.assert_called_with('kytos', 'napp') + self.assertEqual(code, 200) + + @patch('kytos.core.controller.Controller.unload_napp') + @patch('kytos.core.controller.Controller.reload_napp_module') + def test_reload_napp__error(self, *args): + """Test reload_napp method to error case.""" + (mock_reload_napp_module, _) = args + mock_reload_napp_module.side_effect = ModuleNotFoundError + + code = self.controller.reload_napp('kytos', 'napp') + + self.assertEqual(code, 400) + + @patch('kytos.core.controller.Controller.reload_napp', return_value=200) + def test_rest_reload_napp(self, mock_reload_napp): + """Test rest_reload_napp method.""" + resp, code = self.controller.rest_reload_napp('kytos', 'napp') + + mock_reload_napp.assert_called_with('kytos', 'napp') + self.assertEqual(resp, 'reloaded') + self.assertEqual(code, 200) + + @patch('kytos.core.controller.Controller.reload_napp') + def test_rest_reload_all_napps(self, mock_reload_napp): + """Test rest_reload_all_napps method.""" + self.controller.napps = [('kytos', 'napp')] + resp, code = self.controller.rest_reload_all_napps() + + mock_reload_napp.assert_called_with('kytos', 'napp') + self.assertEqual(resp, 'reloaded') + self.assertEqual(code, 200) + + @patch('kytos.core.controller.Controller.notify_listeners') + def test_raw_event_handler(self, mock_notify_listeners): + """Test raw_event_handler async method by handling a shutdown event.""" + event = MagicMock() + event.name = 'kytos/core.shutdown' + self.controller.buffers.raw._queue.sync_q.put(event) + + self.loop.run_until_complete(self.controller.raw_event_handler()) + + mock_notify_listeners.assert_called_with(event) + + @patch('kytos.core.controller.Controller.notify_listeners') + def test_msg_in_event_handler(self, mock_notify_listeners): + """Test msg_in_event_handler async method by handling a shutdown + event.""" + event = MagicMock() + event.name = 'kytos/core.shutdown' + self.controller.buffers.msg_in._queue.sync_q.put(event) + + self.loop.run_until_complete(self.controller.msg_in_event_handler()) + + mock_notify_listeners.assert_called_with(event) + + @patch('kytos.core.controller.Controller.notify_listeners') + def test_msg_out_event_handler(self, mock_notify_listeners): + """Test msg_out_event_handler async method by handling a common and a + shutdown event.""" + dst = MagicMock() + dst.state = 0 + packet = MagicMock() + msg = MagicMock() + msg.pack.return_value = packet + + event_1 = MagicMock() + event_1.name = 'kytos/core.any' + event_1.destination = dst + event_1.content = {"message": msg} + + event_2 = MagicMock() + event_2.name = 'kytos/core.shutdown' + + self.controller.buffers.msg_out._queue.sync_q.put(event_1) + self.controller.buffers.msg_out._queue.sync_q.put(event_2) + + self.loop.run_until_complete(self.controller.msg_out_event_handler()) + + dst.send.assert_called_with(packet) + mock_notify_listeners.assert_called_with(event_1) + + @patch('kytos.core.controller.Controller.notify_listeners') + def test_app_event_handler(self, mock_notify_listeners): + """Test app_event_handler async method by handling a shutdown event.""" + event = MagicMock() + event.name = 'kytos/core.shutdown' + self.controller.buffers.app._queue.sync_q.put(event) + + self.loop.run_until_complete(self.controller.app_event_handler()) + + mock_notify_listeners.assert_called_with(event) diff --git a/tests/unit/test_core/test_events.py b/tests/unit/test_core/test_events.py new file mode 100644 index 000000000..91d239c28 --- /dev/null +++ b/tests/unit/test_core/test_events.py @@ -0,0 +1,47 @@ +"""Test kytos.core.events module.""" +from unittest import TestCase + +from kytos.core.events import KytosEvent + + +class TestKytosEvent(TestCase): + """KytosEvent tests.""" + + def setUp(self): + """Instantiate a KytosEvent.""" + self.event = KytosEvent('kytos/core.any') + + def test__str__(self): + """Test __str__ method.""" + self.assertEqual(str(self.event), 'kytos/core.any') + + def test__repr__(self): + """Test __repr__ method.""" + self.event.content = {"destination": "dest", + "source": "src", + "message": "msg"} + expected = "KytosEvent('kytos/core.any', {'destination': 'dest', " + \ + "'source': 'src', 'message': 'msg'})" + + self.assertEqual(repr(self.event), expected) + + def test_destination(self): + """Test destination property and set_destination method.""" + self.assertEqual(self.event.destination, None) + + self.event.set_destination('dest') + self.assertEqual(self.event.destination, 'dest') + + def test_source(self): + """Test source property and set_source method.""" + self.assertEqual(self.event.source, None) + + self.event.set_source('src') + self.assertEqual(self.event.source, 'src') + + def test_message(self): + """Test message property.""" + self.assertEqual(self.event.message, None) + + self.event.content = {"message": "msg"} + self.assertEqual(self.event.message, 'msg') diff --git a/tests/unit/test_core/test_exceptions.py b/tests/unit/test_core/test_exceptions.py new file mode 100644 index 000000000..f7df1ecd5 --- /dev/null +++ b/tests/unit/test_core/test_exceptions.py @@ -0,0 +1,59 @@ +"""Test kytos.core.exceptions module.""" +from unittest import TestCase +from unittest.mock import MagicMock + +from kytos.core.exceptions import (KytosCoreException, KytosEventException, + KytosNAppException, + KytosNoTagAvailableError, + KytosSwitchOfflineException) + + +class TestExceptions(TestCase): + """Exceptions tests.""" + + def test_kytos_core_exception(self): + """Test KytosCoreException exception.""" + with self.assertRaises(Exception) as exc: + raise KytosCoreException('msg') + + expected_msg = 'KytosCore exception: msg' + self.assertEqual(str(exc.exception), expected_msg) + + def test_kytos_switch_offline_exception(self): + """Test KytosSwitchOfflineException exception.""" + with self.assertRaises(Exception) as exc: + switch = MagicMock() + switch.dpid = '00:00:00:00:00:00:00:01' + raise KytosSwitchOfflineException(switch) + + expected_msg = 'The switch 00:00:00:00:00:00:00:01 is not reachable. ' + expected_msg += 'Please check the connection between the switch and ' + expected_msg += 'the controller.' + + self.assertEqual(str(exc.exception), expected_msg) + + def test_kytos_event_exception(self): + """Test KytosEventException exception.""" + with self.assertRaises(Exception) as exc: + raise KytosEventException + + expected_msg = 'KytosEvent exception' + self.assertEqual(str(exc.exception), expected_msg) + + def test_kytos_no_tag_avaible_error(self): + """Test KytosNoTagAvailableError exception.""" + with self.assertRaises(Exception) as exc: + link = MagicMock() + link.id = '123' + raise KytosNoTagAvailableError(link) + + expected_msg = 'Link 123 has no vlan available.' + self.assertEqual(str(exc.exception), expected_msg) + + def test_kytos_napp_exception(self): + """Test KytosNAppException exception.""" + with self.assertRaises(Exception) as exc: + raise KytosNAppException() + + expected_msg = 'KytosNApp exception' + self.assertEqual(str(exc.exception), expected_msg) diff --git a/tests/unit/test_core/test_helpers.py b/tests/unit/test_core/test_helpers.py new file mode 100644 index 000000000..9f98dfbc8 --- /dev/null +++ b/tests/unit/test_core/test_helpers.py @@ -0,0 +1,55 @@ +"""Test kytos.core.helpers module.""" +from unittest import TestCase +from unittest.mock import patch + +from kytos.core.helpers import get_time, run_on_thread + + +class TestHelpers(TestCase): + """Test the helpers methods.""" + + @staticmethod + @patch('kytos.core.helpers.Thread') + def test_run_on_thread(mock_thread): + """Test run_on_thread decorator.""" + + @run_on_thread + def test(): + pass + + test() + + mock_thread.return_value.start.assert_called() + + def test_get_time__str(self): + """Test get_time method passing a string as parameter.""" + date = get_time("2000-01-01T00:30:00") + + self.assertEqual(date.year, 2000) + self.assertEqual(date.month, 1) + self.assertEqual(date.day, 1) + self.assertEqual(date.hour, 0) + self.assertEqual(date.minute, 30) + self.assertEqual(date.second, 0) + + def test_get_time__dict(self): + """Test get_time method passing a dict as parameter.""" + date = get_time({"year": 2000, + "month": 1, + "day": 1, + "hour": 00, + "minute": 30, + "second": 00}) + + self.assertEqual(date.year, 2000) + self.assertEqual(date.month, 1) + self.assertEqual(date.day, 1) + self.assertEqual(date.hour, 0) + self.assertEqual(date.minute, 30) + self.assertEqual(date.second, 0) + + def test_get_time__none(self): + """Test get_time method by not passing a parameter.""" + date = get_time() + + self.assertIsNone(date) diff --git a/tests/unit/test_core/test_interface.py b/tests/unit/test_core/test_interface.py index 83734989a..640708872 100644 --- a/tests/unit/test_core/test_interface.py +++ b/tests/unit/test_core/test_interface.py @@ -1,16 +1,53 @@ """Interface tests.""" import logging import unittest -from unittest.mock import Mock +from unittest.mock import MagicMock, Mock from pyof.v0x04.common.port import PortFeatures -from kytos.core.interface import TAG, Interface, TAGType +from kytos.core.interface import TAG, UNI, Interface, TAGType from kytos.core.switch import Switch logging.basicConfig(level=logging.CRITICAL) +class TestTAG(unittest.TestCase): + """TAG tests.""" + + def setUp(self): + """Create TAG object.""" + self.tag = TAG(1, 123) + + def test_from_dict(self): + """Test from_dict method.""" + tag_dict = {'tag_type': 2, 'value': 456} + tag = self.tag.from_dict(tag_dict) + + self.assertEqual(tag.tag_type, 2) + self.assertEqual(tag.value, 456) + + def test_as_dict(self): + """Test as_dict method.""" + self.assertEqual(self.tag.as_dict(), {'tag_type': 1, 'value': 123}) + + def test_from_json(self): + """Test from_json method.""" + tag_json = '{"tag_type": 2, "value": 456}' + tag = self.tag.from_json(tag_json) + + self.assertEqual(tag.tag_type, 2) + self.assertEqual(tag.value, 456) + + def test_as_json(self): + """Test as_json method.""" + self.assertEqual(self.tag.as_json(), '{"tag_type": 1, "value": 123}') + + def test__repr__(self): + """Test __repr__ method.""" + self.assertEqual(repr(self.tag), 'TAG(, 123)') + + +# pylint: disable=protected-access, too-many-public-methods class TestInterface(unittest.TestCase): """Test Interfaces.""" @@ -88,7 +125,6 @@ def test_10_mega_speed(self): def test_speed_setter(self): """Should return speed that was set and not features'.""" expected_speed = 12345 - self.iface.features = PortFeatures.OFPPF_10MB_FD self.iface.set_custom_speed(expected_speed) actual_speed = self.iface.speed self.assertEqual(expected_speed, actual_speed) @@ -96,20 +132,18 @@ def test_speed_setter(self): def test_speed_in_constructor(self): """Custom speed should override features'.""" expected_speed = 6789 - iface = self._get_v0x04_iface(speed=expected_speed, - features=PortFeatures.OFPPF_10MB_FD) - actual_speed = iface.speed - self.assertEqual(expected_speed, actual_speed) + iface = self._get_v0x04_iface(speed=expected_speed) + self.assertEqual(expected_speed, iface.speed) - def test_remove_custom_speed(self): - """Should return features' speed again when custom's becomes None.""" + def test_speed_removing_features(self): + """Should return custom speed again when features becomes None.""" custom_speed = 101112 of_speed = 10 * 10**6 / 8 iface = self._get_v0x04_iface(speed=custom_speed, features=PortFeatures.OFPPF_10MB_FD) - self.assertEqual(custom_speed, iface.speed) - iface.set_custom_speed(None) self.assertEqual(of_speed, iface.speed) + iface.features = None + self.assertEqual(custom_speed, iface.speed) def test_interface_available_tags(self): """Test available_tags on Interface class.""" @@ -166,3 +200,124 @@ def test_interface_use_tags(self): self.iface.make_tag_available(tag) is_success = self.iface.use_tag(tag) self.assertTrue(is_success) + + def test_enable(self): + """Test enable method.""" + self.iface.switch = MagicMock() + + self.iface.enable() + + self.iface.switch.enable.assert_called() + self.assertTrue(self.iface._enabled) + + def test_get_endpoint(self): + """Test get_endpoint method.""" + endpoint = ('endpoint', 'time') + self.iface.endpoints = [endpoint] + + return_endpoint = self.iface.get_endpoint('endpoint') + + self.assertEqual(return_endpoint, endpoint) + + def test_add_endpoint(self): + """Test add_endpoint method.""" + self.iface.add_endpoint('endpoint') + + self.assertEqual(len(self.iface.endpoints), 1) + + def test_delete_endpoint(self): + """Test delete_endpoint method.""" + endpoint = ('endpoint', 'time') + self.iface.endpoints = [endpoint] + + self.iface.delete_endpoint('endpoint') + + self.assertEqual(len(self.iface.endpoints), 0) + + def test_update_endpoint(self): + """Test update_endpoint method.""" + endpoint = ('endpoint', 'time') + self.iface.endpoints = [endpoint] + + self.iface.update_endpoint('endpoint') + + self.assertEqual(len(self.iface.endpoints), 1) + + def test_update_link__none(self): + """Test update_link method when this interface is not in link + endpoints.""" + link = MagicMock() + link.endpoint_a = MagicMock() + link.endpoint_b = MagicMock() + + result = self.iface.update_link(link) + + self.assertFalse(result) + + def test_update_link__endpoint_a(self): + """Test update_link method when this interface is the endpoint a.""" + interface = MagicMock() + interface.link = None + link = MagicMock() + link.endpoint_a = self.iface + link.endpoint_b = interface + + self.iface.update_link(link) + + self.assertEqual(self.iface.link, link) + self.assertEqual(interface.link, link) + + def test_update_link__endpoint_b(self): + """Test update_link method when this interface is the endpoint b.""" + interface = MagicMock() + interface.link = None + link = MagicMock() + link.endpoint_a = interface + link.endpoint_b = self.iface + + self.iface.update_link(link) + + self.assertEqual(self.iface.link, link) + self.assertEqual(interface.link, link) + + +class TestUNI(unittest.TestCase): + """UNI tests.""" + + def setUp(self): + """Create UNI object.""" + switch = MagicMock() + switch.dpid = '00:00:00:00:00:00:00:01' + interface = Interface('name', 1, switch) + user_tag = TAG(1, 123) + self.uni = UNI(interface, user_tag) + + def test__eq__(self): + """Test __eq__ method.""" + user_tag = TAG(2, 456) + interface = Interface('name', 2, MagicMock()) + other = UNI(interface, user_tag) + + self.assertFalse(self.uni.__eq__(other)) + + def test_is_valid(self): + """Test is_valid method for a valid, invalid and none tag.""" + self.assertTrue(self.uni.is_valid()) + + with self.assertRaises(ValueError): + TAG(999999, 123) + + self.uni.user_tag = None + self.assertTrue(self.uni.is_valid()) + + def test_as_dict(self): + """Test as_dict method.""" + expected_dict = {'interface_id': '00:00:00:00:00:00:00:01:1', + 'tag': {'tag_type': 1, 'value': 123}} + self.assertEqual(self.uni.as_dict(), expected_dict) + + def test_as_json(self): + """Test as_json method.""" + expected_json = '{"interface_id": "00:00:00:00:00:00:00:01:1", ' + \ + '"tag": {"tag_type": 1, "value": 123}}' + self.assertEqual(self.uni.as_json(), expected_json) diff --git a/tests/unit/test_core/test_kytosd.py b/tests/unit/test_core/test_kytosd.py new file mode 100644 index 000000000..ed571c4fc --- /dev/null +++ b/tests/unit/test_core/test_kytosd.py @@ -0,0 +1,90 @@ +"""Test kytos.core.kytosd module.""" +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from kytos.core.kytosd import _create_pid_dir, async_main, main, start_shell + + +class TestKytosd(TestCase): + """Kytosd tests.""" + + @staticmethod + @patch('os.makedirs') + @patch('kytos.core.kytosd.BASE_ENV', '/tmp/') + def test_create_pid_dir__env(mock_mkdirs): + """Test _create_pid_dir method with env.""" + _create_pid_dir() + + mock_mkdirs.assert_called_with('/tmp/var/run/kytos', exist_ok=True) + + @staticmethod + @patch('os.chmod') + @patch('os.makedirs') + @patch('kytos.core.kytosd.BASE_ENV', '/') + def test_create_pid_dir__system(*args): + """Test _create_pid_dir method with system dir.""" + (mock_mkdirs, mock_chmod) = args + _create_pid_dir() + + mock_mkdirs.assert_called_with('/var/run/kytos', exist_ok=True) + mock_chmod.assert_called_with('/var/run/kytos', 0o1777) + + @staticmethod + @patch('kytos.core.kytosd.InteractiveShellEmbed') + def test_start_shell(mock_interactive_shell): + """Test stop_api_server method.""" + start_shell(MagicMock()) + + mock_interactive_shell.assert_called() + + @staticmethod + @patch('kytos.core.kytosd.async_main') + @patch('kytos.core.kytosd._create_pid_dir') + @patch('kytos.core.kytosd.KytosConfig') + def test_main__foreground(*args): + """Test main method in foreground.""" + (mock_kytos_config, mock_create_pid_dir, mock_async_main) = args + config = MagicMock(foreground=True) + options = {'daemon': config} + mock_kytos_config.return_value.options = options + + main() + + mock_create_pid_dir.assert_called() + mock_async_main.assert_called() + + @staticmethod + @patch('kytos.core.kytosd.daemon.DaemonContext') + @patch('kytos.core.kytosd.async_main') + @patch('kytos.core.kytosd._create_pid_dir') + @patch('kytos.core.kytosd.KytosConfig') + def test_main__background(*args): + """Test main method in background.""" + (mock_kytos_config, mock_create_pid_dir, mock_async_main, _) = args + config = MagicMock(foreground=False) + options = {'daemon': config} + mock_kytos_config.return_value.options = options + + main() + + mock_create_pid_dir.assert_called() + mock_async_main.assert_called() + + @staticmethod + @patch('kytos.core.kytosd.asyncio') + @patch('kytos.core.kytosd.InteractiveShellEmbed') + @patch('kytos.core.kytosd.Controller') + def test_async_main(*args): + """Test async_main method.""" + (mock_controller, _, mock_asyncio) = args + controller = MagicMock() + controller.options.debug = True + controller.options.foreground = True + mock_controller.return_value = controller + + event_loop = MagicMock() + mock_asyncio.get_event_loop.return_value = event_loop + + async_main(MagicMock()) + + event_loop.call_soon.assert_called_with(controller.start) diff --git a/tests/unit/test_core/test_link.py b/tests/unit/test_core/test_link.py index 3678fc535..ebb8fe4ea 100644 --- a/tests/unit/test_core/test_link.py +++ b/tests/unit/test_core/test_link.py @@ -2,15 +2,17 @@ import logging import time import unittest -from unittest.mock import Mock +from unittest.mock import Mock, patch -from kytos.core.interface import Interface +from kytos.core.exceptions import KytosLinkCreationError +from kytos.core.interface import Interface, TAGType from kytos.core.link import Link from kytos.core.switch import Switch logging.basicConfig(level=logging.CRITICAL) +# pylint: disable=protected-access class TestLink(unittest.TestCase): """Test Links.""" @@ -33,6 +35,37 @@ def _get_v0x04_ifaces(*args, **kwargs): return iface1, iface2 + def test__eq__(self): + """Test __eq__ method.""" + link_1 = Link(self.iface1, self.iface2) + link_2 = Link(self.iface2, self.iface1) + + iface1, iface2 = self._get_v0x04_ifaces() + iface1.port_number = 1 + iface2.port_number = 2 + link_3 = Link(iface1, iface2) + + self.assertTrue(link_1.__eq__(link_2)) + self.assertFalse(link_1.__eq__(link_3)) + + def test_id(self): + """Test id property.""" + link = Link(self.iface1, self.iface2) + ids = [] + + for value in [('A', 1, 'B', 2), ('B', 2, 'A', 1), ('A', 1, 'A', 2), + ('A', 2, 'A', 1)]: + link.endpoint_a.switch.dpid = value[0] + link.endpoint_a.port_number = value[1] + link.endpoint_b.switch.dpid = value[2] + link.endpoint_b.port_number = value[3] + + ids.append(link.id) + + self.assertEqual(ids[0], ids[1]) + self.assertEqual(ids[2], ids[3]) + self.assertNotEqual(ids[0], ids[2]) + def test_init(self): """Test normal Link initialization.""" link = Link(self.iface1, self.iface2) @@ -42,8 +75,11 @@ def test_init(self): def test_init_with_null_endpoints(self): """Test initialization with None as endpoints.""" - with self.assertRaises(ValueError): - Link(None, None) + with self.assertRaises(KytosLinkCreationError): + Link(self.iface1, None) + + with self.assertRaises(KytosLinkCreationError): + Link(None, self.iface2) def test_link_id(self): """Test equality of links with the same values ​​in different order.""" @@ -51,6 +87,58 @@ def test_link_id(self): link2 = Link(self.iface2, self.iface1) self.assertEqual(link1.id, link2.id) + def test_available_tags(self): + """Test available_tags property.""" + link = Link(self.iface1, self.iface2) + tag_1 = Mock(tag_type=TAGType.VLAN) + tag_2 = Mock(tag_type=TAGType.VLAN) + tag_3 = Mock(tag_type=TAGType.VLAN_QINQ) + tag_4 = Mock(tag_type=TAGType.MPLS) + link.endpoint_a.available_tags = [tag_1, tag_2, tag_3, tag_4] + link.endpoint_b.available_tags = [tag_2, tag_3, tag_4] + + self.assertEqual(link.available_tags, [tag_2, tag_3, tag_4]) + + @patch('kytos.core.interface.Interface.is_tag_available') + def test_use_tag__success(self, mock_is_tag_available): + """Test use_tag method to success case.""" + mock_is_tag_available.side_effect = [True, True] + link = Link(self.iface1, self.iface2) + + result = link.use_tag(Mock()) + self.assertTrue(result) + + @patch('kytos.core.interface.Interface.is_tag_available') + def test_use_tag__error(self, mock_is_tag_available): + """Test use_tag method to error case.""" + mock_is_tag_available.side_effect = [True, False] + link = Link(self.iface1, self.iface2) + + result = link.use_tag(Mock()) + self.assertFalse(result) + + @patch('kytos.core.interface.Interface.make_tag_available') + @patch('kytos.core.interface.Interface.is_tag_available') + def test_make_tag_available__success(self, *args): + """Test make_tag_available method to success case.""" + mock_is_tag_available, _ = args + mock_is_tag_available.side_effect = [True, False] + link = Link(self.iface1, self.iface2) + + result = link.make_tag_available(Mock()) + self.assertTrue(result) + + @patch('kytos.core.interface.Interface.make_tag_available') + @patch('kytos.core.interface.Interface.is_tag_available') + def test_make_tag_available__error(self, *args): + """Test make_tag_available method to error case.""" + mock_is_tag_available, _ = args + mock_is_tag_available.side_effect = [True, True] + link = Link(self.iface1, self.iface2) + + result = link.make_tag_available(Mock()) + self.assertFalse(result) + def test_get_next_available_tag(self): """Test get next available tags returns different tags""" link = Link(self.iface1, self.iface2) @@ -119,3 +207,27 @@ def test_get_next_available_tag(): # It happens because we get 2 tags for every iteration self.assertEqual(_initial_size, len(_link.endpoint_a.available_tags) + 40) + + def test_available_vlans(self): + """Test available_vlans method.""" + link = Link(self.iface1, self.iface2) + tag_1 = Mock(tag_type=TAGType.VLAN) + tag_2 = Mock(tag_type=TAGType.VLAN) + tag_3 = Mock(tag_type=TAGType.VLAN_QINQ) + tag_4 = Mock(tag_type=TAGType.MPLS) + link.endpoint_a.available_tags = [tag_1, tag_2, tag_3, tag_4] + link.endpoint_b.available_tags = [tag_2, tag_3, tag_4] + + vlans = link.available_vlans() + self.assertEqual(vlans, [tag_2]) + + def test_get_available_vlans(self): + """Test _get_available_vlans method.""" + link = Link(self.iface1, self.iface2) + tag_1 = Mock(tag_type=TAGType.VLAN) + tag_2 = Mock(tag_type=TAGType.VLAN_QINQ) + tag_3 = Mock(tag_type=TAGType.MPLS) + link.endpoint_a.available_tags = [tag_1, tag_2, tag_3] + + vlans = link._get_available_vlans(link.endpoint_a) + self.assertEqual(vlans, [tag_1]) diff --git a/tests/unit/test_core/test_logs.py b/tests/unit/test_core/test_logs.py index c964e9e86..4c61cdf6e 100644 --- a/tests/unit/test_core/test_logs.py +++ b/tests/unit/test_core/test_logs.py @@ -161,8 +161,7 @@ def test_handler_filter(self): LogManager.add_handler(handler) # Message based on the log output that ends with traceback plaintext as - # seen in lib/python3.6/site-packages/werkzeug/serving.py:225 of - # Werkzeug==0.12.1 + # seen in site-packages/werkzeug/serving.py:225 of Werkzeug==0.12.1 msg = "lorem ipsum KeyError: 'Session is disconnected'" logger = logging.getLogger('werkzeug') logger.setLevel(logging.ERROR) @@ -178,8 +177,7 @@ def test_old_handler_filter(): already existed before importing the "logs" module. Message based on the log output that ends with traceback plaintext as - seen in lib/python3.6/site-packages/werkzeug/serving.py:225 of - Werkzeug==0.12.1: + seen in site-packages/werkzeug/serving.py:225 of Werkzeug==0.12.1: - logger name: werkzeug - level: ERROR diff --git a/tests/unit/test_core/test_napp_dir_listener.py b/tests/unit/test_core/test_napp_dir_listener.py index ab78febc9..06c8e4def 100644 --- a/tests/unit/test_core/test_napp_dir_listener.py +++ b/tests/unit/test_core/test_napp_dir_listener.py @@ -13,8 +13,21 @@ def setUp(self): self.controller = Mock() self.controller.options.napps = '/tmp' self.napp_dir_listener = NAppDirListener(self.controller) + self.napp_dir_listener.observer = Mock() self.event = Mock(src_path='/tmp/username/napp_name/') + def test_start(self): + """Test start method.""" + self.napp_dir_listener.start() + + self.napp_dir_listener.observer.start.assert_called() + + def test_stop(self): + """Test stop method.""" + self.napp_dir_listener.stop() + + self.napp_dir_listener.observer.stop.assert_called() + def test_on_created(self): """Test whether on_created is calling load_napp.""" self.napp_dir_listener.on_created(self.event) diff --git a/tests/unit/test_core/test_napps_base.py b/tests/unit/test_core/test_napps_base.py new file mode 100644 index 000000000..851c5eda1 --- /dev/null +++ b/tests/unit/test_core/test_napps_base.py @@ -0,0 +1,204 @@ +"""kytos.core.napps tests.""" +import unittest +from unittest.mock import MagicMock, patch + +from kytos.core.napps import KytosNApp, NApp + + +class TestNapp(unittest.TestCase): + """NApp tests.""" + + def setUp(self): + """Execute steps before each tests.""" + self.napp = NApp(username='kytos', name='napp', version='1.0', + repository='any') + self.napp.description = 'desc' + self.napp.tags = ['tag1', 'tag2'] + + def test__str__(self): + """Test __str__ method.""" + self.assertEqual(str(self.napp), 'kytos/napp') + + def test__repr__(self): + """Test __repr__ method.""" + self.assertEqual(repr(self.napp), f'NApp(kytos/napp)') + + def test_id(self): + """Test id property.""" + self.assertEqual(self.napp.id, 'kytos/napp') + + @patch('kytos.core.napps.NApp._has_valid_repository', return_value=True) + def test_uri(self, _): + """Test uri property.""" + self.assertEqual(self.napp.uri, 'any/kytos/napp-1.0') + + @patch('kytos.core.napps.NApp._has_valid_repository', return_value=False) + def test_uri__false(self, _): + """Test uri property when repository is invalid.""" + self.assertEqual(self.napp.uri, '') + + def test_package_url(self): + """Test package_url property.""" + self.assertEqual(self.napp.package_url, 'any/kytos/napp-1.0.napp') + + @patch('kytos.core.napps.NApp._has_valid_repository', return_value=False) + def test_package_url__none(self, _): + """Test package_url property when uri does not exist.""" + self.assertEqual(self.napp.package_url, '') + + def test_create_from_uri(self): + """Test create_from_uri method.""" + napp = NApp.create_from_uri('file://any/kytos/napp:1.0') + + self.assertEqual(napp.username, 'kytos') + self.assertEqual(napp.name, 'napp') + self.assertEqual(napp.version, '1.0') + self.assertEqual(napp.repository, 'file://any') + + def test_create_from_uri__not(self): + """Test create_from_uri method when uri does not match.""" + napp = NApp.create_from_uri('') + + self.assertIsNone(napp) + + @patch('builtins.open') + def test_create_from_json(self, mock_open): + """Test create_from_json method.""" + data_file = MagicMock() + data_file.read.return_value = '{"username": "kytos", \ + "name": "napp", \ + "version": "1.0", \ + "repository": "any"}' + + mock_open.return_value.__enter__.return_value = data_file + napp = NApp.create_from_json('filename') + + self.assertEqual(napp.username, 'kytos') + self.assertEqual(napp.name, 'napp') + self.assertEqual(napp.version, '1.0') + self.assertEqual(napp.repository, 'any') + + def test_create_from_dict(self): + """Test create_from_dict method.""" + data = {'username': 'kytos', 'name': 'napp', 'version': '1.0', + 'repository': 'any'} + napp = NApp.create_from_dict(data) + + self.assertEqual(napp.username, 'kytos') + self.assertEqual(napp.name, 'napp') + self.assertEqual(napp.version, '1.0') + self.assertEqual(napp.repository, 'any') + + def test_match(self): + """Test match method.""" + for pattern in ['kytos/napp', 'desc', 'tag1', 'tag2']: + self.assertTrue(self.napp.match(pattern)) + + @patch('os.mkdir') + @patch('tarfile.open') + @patch('builtins.open') + @patch('urllib.request.urlretrieve', return_value=['filename']) + @patch('kytos.core.napps.base.randint', return_value=123) + @patch('kytos.core.napps.base.Path.unlink') + @patch('kytos.core.napps.base.Path.stem', 'stem') + def test_download(self, *args): + """Test download method.""" + (_, _, _, mock_open, mock_tarfile_open, mock_mkdir) = args + tar = MagicMock() + repo_file = MagicMock() + + mock_open.return_value.__enter__.return_value = repo_file + mock_tarfile_open.return_value.__enter__.return_value = tar + + extracted = self.napp.download() + + mock_mkdir.assert_called_with('/tmp/kytos-napp-stem-123') + tar.extractall.assert_called_with('/tmp/kytos-napp-stem-123') + repo_file.write.assert_called_with('any\n') + self.assertEqual(str(extracted), '/tmp/kytos-napp-stem-123') + + @patch('kytos.core.napps.NApp._has_valid_repository', return_value=False) + def test_download__none(self, _): + """Test download method when package_url does not exist.""" + extracted = self.napp.download() + + self.assertIsNone(extracted) + + +# pylint: disable=no-member +class KytosNAppChild(KytosNApp): + """KytosNApp generic class.""" + + def setup(self): + """Setup NApp.""" + + def execute(self): + """Execute NApp.""" + + def shutdown(self): + """End of the NApp.""" + + +# pylint: disable=protected-access +class TestKytosNApp(unittest.TestCase): + """KytosNApp tests.""" + + # pylint: disable=arguments-differ + @patch('kytos.core.napps.base.Event') + @patch('builtins.open') + def setUp(self, *args): + """Execute steps before each tests.""" + (mock_open, mock_event) = args + self.event = MagicMock() + mock_event.return_value = self.event + + data_file = MagicMock() + data_file.read.return_value = '{"username": "kytos", \ + "name": "napp", \ + "version": "1.0", \ + "repository": "any"}' + + mock_open.return_value.__enter__.return_value = data_file + + self.kytos_napp = KytosNAppChild(MagicMock()) + self.kytos_napp.execute = MagicMock() + self.kytos_napp.shutdown = MagicMock() + + def test_napp_id(self): + """Test napp_id property.""" + self.assertEqual(self.kytos_napp.napp_id, 'kytos/napp') + + @patch('builtins.open') + def test_load_json(self, mock_open): + """Test _load_json method.""" + data_file = MagicMock() + data_file.read.return_value = '{"username": "kytos", \ + "name": "napp", \ + "version": "1.0", \ + "repository": "any"}' + + mock_open.return_value.__enter__.return_value = data_file + + self.kytos_napp._load_json() + + self.assertEqual(self.kytos_napp.username, 'kytos') + self.assertEqual(self.kytos_napp.name, 'napp') + self.assertEqual(self.kytos_napp.version, '1.0') + self.assertEqual(self.kytos_napp.repository, 'any') + + def test_execute_as_loop_and_run(self): + """Test execute_as_loop and run methods.""" + self.event.is_set.side_effect = [False, True] + self.kytos_napp.execute_as_loop(1) + + self.kytos_napp.run() + + self.assertEqual(self.kytos_napp.execute.call_count, 2) + + def test_shutdown_handler(self): + """Test _shutdown_handler method.""" + self.event.is_set.return_value = False + + self.kytos_napp._shutdown_handler(MagicMock()) + + self.kytos_napp.shutdown.assert_called_once() diff --git a/tests/unit/test_core/test_napps_manager.py b/tests/unit/test_core/test_napps_manager.py new file mode 100644 index 000000000..c7cd3b288 --- /dev/null +++ b/tests/unit/test_core/test_napps_manager.py @@ -0,0 +1,342 @@ +"""kytos.core.napps tests.""" +import asyncio +import unittest +from unittest.mock import MagicMock, patch + +from kytos.core import Controller +from kytos.core.config import KytosConfig +from kytos.core.napps import NAppsManager + + +# pylint: disable=protected-access, too-many-public-methods +class TestNAppsManager(unittest.TestCase): + """NAppsManager tests.""" + + def setUp(self): + """Execute steps before each tests.""" + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + + self.options = KytosConfig().options['daemon'] + self.controller = Controller(self.options, loop=self.loop) + self.controller.log = MagicMock() + self.controller.load_napp = MagicMock() + self.controller.unload_napp = MagicMock() + + self.napps_manager = NAppsManager(self.controller) + + @staticmethod + def get_path(files): + """Return a Path mock.""" + path = MagicMock() + path.exists.return_value = True + path.glob.return_value = files + return path + + @staticmethod + def get_napp_mock(username='kytos', name='napp'): + """Return a NApp mock.""" + napp = MagicMock() + napp.username = username + napp.name = name + return napp + + @staticmethod + def get_new_napps_manager(): + """Return a NewNappsManager mock.""" + napp = MagicMock() + napp.napp_dependencies = ['file://any/kytos/napp2:1.0'] + + napp_2 = MagicMock() + napp_2.napp_dependencies = [] + + new_napp_manager = MagicMock() + new_napp_manager.napps = {'kytos/napp': napp, 'kytos/napp2': napp_2} + return new_napp_manager + + @patch('shutil.rmtree') + @patch('shutil.move') + @patch('kytos.core.napps.NApp.create_from_json') + @patch('kytos.core.napps.NApp.create_from_uri') + @patch('kytos.core.napps.NAppsManager.get_all_napps') + @patch('kytos.core.napps.NAppsManager._find_napp') + def test_install(self, *args): + """Test install method.""" + (_, _, mock_create_from_uri, mock_create_from_json, _, _) = args + napp = MagicMock() + mock_create_from_uri.return_value = napp + mock_create_from_json.return_value = napp + + uri = 'file://any/kytos/napp:1.0' + self.napps_manager._installed_path = self.get_path(['json']) + installed = self.napps_manager.install(uri, False) + + self.assertTrue(installed) + + @patch('shutil.rmtree') + @patch('shutil.move') + @patch('kytos.core.napps.NApp.create_from_json') + @patch('kytos.core.napps.NApp.create_from_uri') + @patch('kytos.core.napps.NAppsManager.enable') + @patch('kytos.core.napps.NAppsManager.get_all_napps') + @patch('kytos.core.napps.NAppsManager._find_napp') + def test_install_and_enable(self, *args): + """Test install method enabling the napp.""" + (_, _, mock_enable, mock_create_from_uri, mock_create_from_json, _, + _) = args + napp = MagicMock() + napp.username = 'kytos' + napp.name = 'napp' + mock_create_from_uri.return_value = napp + mock_create_from_json.return_value = napp + + uri = 'file://any/kytos/napp:1.0' + self.napps_manager._installed_path = self.get_path(['json']) + installed = self.napps_manager.install(uri, True) + + self.assertTrue(installed) + mock_enable.assert_called_with('kytos', 'napp') + + @patch('kytos.core.napps.NApp.create_from_uri') + @patch('kytos.core.napps.NAppsManager.get_all_napps') + def test_install__installed(self, *args): + """Test install method when napp is already installed.""" + (mock_get_all_napps, mock_create_from_uri) = args + napp = MagicMock() + mock_create_from_uri.return_value = napp + mock_get_all_napps.return_value = [napp] + + uri = 'file://any/kytos/napp:1.0' + installed = self.napps_manager.install(uri, False) + + self.assertFalse(installed) + + @patch('kytos.core.napps.NAppsManager.is_installed', return_value=True) + @patch('kytos.core.napps.manager.NewNAppManager') + def test_uninstall(self, *args): + """Test uninstall method.""" + (mock_new_napp_manager, _) = args + mock_new_napp_manager.return_value = self.get_new_napps_manager() + + self.napps_manager._installed_path = self.get_path(['json']) + uninstalled = self.napps_manager.uninstall('kytos', 'napp') + + self.assertTrue(uninstalled) + + @patch('kytos.core.napps.NAppsManager.is_installed', return_value=False) + @patch('kytos.core.napps.manager.NewNAppManager') + def test_uninstall__not_installed(self, *args): + """Test uninstall method when napp is not installed.""" + (mock_new_napp_manager, _) = args + mock_new_napp_manager.return_value = self.get_new_napps_manager() + + uninstalled = self.napps_manager.uninstall('kytos', 'napp') + + self.assertTrue(uninstalled) + + @patch('kytos.core.napps.NAppsManager.is_enabled', return_value=True) + def test_uninstall__enabled(self, _): + """Test uninstall method when napp is enabled.""" + uninstalled = self.napps_manager.uninstall('kytos', 'napp') + + self.assertFalse(uninstalled) + + @patch('kytos.core.napps.manager.NewNAppManager') + def test_enable(self, mock_new_napp_manager): + """Test enable method.""" + mock_new_napp_manager.return_value = self.get_new_napps_manager() + + self.napps_manager._installed_path = self.get_path(['json']) + self.napps_manager._enabled_path = self.get_path(['json']) + + (self.napps_manager._installed_path / 'kytos/napp').is_dir. \ + return_value = True + (self.napps_manager._enabled_path / 'kytos/napp').exists. \ + return_value = False + + enabled = self.napps_manager.enable('kytos', 'napp') + + self.assertTrue(enabled) + + @patch('kytos.core.napps.manager.NewNAppManager') + def test_disable(self, mock_new_napp_manager): + """Test disable method.""" + mock_new_napp_manager.return_value = self.get_new_napps_manager() + + self.napps_manager._enabled_path = self.get_path(['json']) + + disabled = self.napps_manager.disable('kytos', 'napp') + + self.assertTrue(disabled) + + @patch('kytos.core.napps.NAppsManager.enable') + @patch('kytos.core.napps.NAppsManager.get_disabled_napps') + def test_enable_all(self, *args): + """Test enable_all method.""" + (mock_get_disabled_napps, mock_enable) = args + napp = self.get_napp_mock() + mock_get_disabled_napps.return_value = [napp] + + self.napps_manager.enable_all() + + mock_enable.assert_called_once() + + @patch('kytos.core.napps.NAppsManager.disable') + @patch('kytos.core.napps.NAppsManager.get_enabled_napps') + def test_disable_all(self, *args): + """Test disable_all method.""" + (mock_get_enabled_napps, mock_disable) = args + napp = self.get_napp_mock() + mock_get_enabled_napps.return_value = [napp] + + self.napps_manager.disable_all() + + mock_disable.assert_called_once() + + @patch('kytos.core.napps.NApp.create_from_uri') + @patch('kytos.core.napps.NAppsManager.get_enabled_napps') + def test_is_enabled(self, *args): + """Test is_enabled method.""" + (mock_get_enabled_napps, mock_create_from_uri) = args + napp = MagicMock() + mock_create_from_uri.return_value = napp + mock_get_enabled_napps.return_value = [napp] + + is_enabled = self.napps_manager.is_enabled('kytos', 'napp') + + mock_create_from_uri.assert_called_with('kytos/napp') + self.assertTrue(is_enabled) + + @patch('kytos.core.napps.NApp.create_from_uri') + @patch('kytos.core.napps.NAppsManager.get_installed_napps') + def test_is_installed(self, *args): + """Test is_installed method.""" + (mock_get_installed_napps, mock_create_from_uri) = args + napp = MagicMock() + mock_create_from_uri.return_value = napp + mock_get_installed_napps.return_value = [napp] + + is_installed = self.napps_manager.is_installed('kytos', 'napp') + + mock_create_from_uri.assert_called_with('kytos/napp') + self.assertTrue(is_installed) + + def test_get_napp_fullname_from_uri(self): + """Test get_napp_fullname_from_uri method.""" + uri = 'file://any/kytos/napp:1.0' + username, name = self.napps_manager.get_napp_fullname_from_uri(uri) + + self.assertEqual(username, 'kytos') + self.assertEqual(name, 'napp') + + @patch('kytos.core.napps.NApp.create_from_json') + def test_get_all_napps(self, mock_create_from_json): + """Test get_all_napps method.""" + napp = MagicMock() + mock_create_from_json.return_value = napp + + self.napps_manager._installed_path = self.get_path(['json']) + napps = self.napps_manager.get_all_napps() + + self.assertEqual(napps, [napp]) + + @patch('kytos.core.napps.NApp.create_from_json') + def test_get_enabled_napps(self, mock_create_from_json): + """Test get_enabled_napps method.""" + napp = MagicMock() + napp.enabled = False + mock_create_from_json.return_value = napp + + self.napps_manager._enabled_path = self.get_path(['json']) + napps = self.napps_manager.get_enabled_napps() + + self.assertEqual(napps, [napp]) + self.assertTrue(napp.enabled) + + @patch('kytos.core.napps.NApp.create_from_json') + def test_get_disabled_napps(self, mock_create_from_json): + """Test get_disabled_napps method.""" + napp_1 = MagicMock() + napp_2 = MagicMock() + mock_create_from_json.side_effect = [napp_1, napp_2, napp_1] + + self.napps_manager._installed_path = self.get_path(['json1', 'json2']) + self.napps_manager._enabled_path = self.get_path(['json1']) + napps = self.napps_manager.get_disabled_napps() + + self.assertEqual(napps, [napp_2]) + + @patch('kytos.core.napps.NApp.create_from_json') + def test_get_installed_napps(self, mock_create_from_json): + """Test get_installed_napps method.""" + napp = MagicMock() + mock_create_from_json.return_value = napp + + self.napps_manager._installed_path = self.get_path(['json']) + napps = self.napps_manager.get_installed_napps() + + self.assertEqual(napps, [napp]) + + @patch('pathlib.Path.open') + def test_get_napp_metadata__success(self, mock_open): + """Test get_napp_metadata method to success case.""" + data_file = MagicMock() + data_file.read.return_value = '{"username": "kytos", \ + "name": "napp", \ + "version": "1.0"}' + mock_open.return_value.__enter__.return_value = data_file + + meta = self.napps_manager.get_napp_metadata('kytos', 'napp', 'version') + + self.assertEqual(meta, '1.0') + + def test_get_napp_metadata__error(self): + """Test get_napp_metadata method to error case.""" + meta = self.napps_manager.get_napp_metadata('kytos', 'napp', 'key') + + self.assertEqual(meta, '') + + def test_get_napps_from_path__error(self): + """Test get_napps_from_path method to error case.""" + path = MagicMock() + path.exists.return_value = False + napps = self.napps_manager.get_napps_from_path(path) + + self.assertEqual(napps, []) + + @patch('kytos.core.napps.NApp.create_from_json') + def test_get_napps_from_path__success(self, mock_create_from_json): + """Test get_napps_from_path method to success case.""" + napp = MagicMock() + mock_create_from_json.return_value = napp + + path = self.get_path(['json']) + napps = self.napps_manager.get_napps_from_path(path) + + self.assertEqual(napps, [napp]) + + def test_create_module(self): + """Test _create_module method.""" + path = MagicMock() + path.exists.return_value = False + self.napps_manager._create_module(path) + + path.mkdir.assert_called() + (path / '__init__.py').touch.assert_called() + + @patch('pathlib.Path.open') + @patch('pathlib.Path.parent', 'parent') + @patch('pathlib.Path.exists', return_value=True) + def test_find_napp(self, *args): + """Test _find_napp method.""" + (_, mock_open) = args + data_file = MagicMock() + data_file.read.return_value = '{"username": "kytos", \ + "name": "napp", \ + "version": "1.0"}' + mock_open.return_value.__enter__.return_value = data_file + + napp = self.get_napp_mock() + folder = self.napps_manager._find_napp(napp) + + self.assertEqual(folder, 'parent') diff --git a/tests/unit/test_core/test_switch.py b/tests/unit/test_core/test_switch.py index 33336b9d9..9ef734605 100644 --- a/tests/unit/test_core/test_switch.py +++ b/tests/unit/test_core/test_switch.py @@ -1,14 +1,23 @@ """Test kytos.core.switch module.""" import asyncio +import json +from datetime import datetime from unittest import TestCase -from unittest.mock import Mock +from unittest.mock import MagicMock, Mock, patch from kytos.core import Controller from kytos.core.config import KytosConfig +from kytos.core.constants import FLOOD_TIMEOUT from kytos.core.interface import Interface from kytos.core.switch import Switch +def get_date(): + """Return date with FLOOD_TIMEOUT+1 microseconds.""" + return datetime(2000, 1, 1, 0, 0, 0, FLOOD_TIMEOUT+1) + + +# pylint: disable=protected-access, too-many-public-methods class TestSwitch(TestCase): """Switch tests.""" @@ -22,10 +31,36 @@ def setUp(self): self.controller = Controller(self.options, loop=self.loop) self.controller.log = Mock() + self.switch = self.create_switch() + + @staticmethod + def create_switch(): + """Create a new switch.""" + connection = MagicMock() + connection.address = 'addr' + connection.port = 'port' + connection.protocol.version = 0x04 + switch = Switch('00:00:00:00:00:00:00:01', connection) + switch._enabled = True + return switch + def test_repr(self): """Test repr() output.""" - switch = Switch('some-dpid') - self.assertEqual(repr(switch), "Switch('some-dpid')") + expected_repr = "Switch('00:00:00:00:00:00:00:01')" + self.assertEqual(repr(self.switch), expected_repr) + + def test_id(self): + """Test id property.""" + self.assertEqual(self.switch.id, '00:00:00:00:00:00:00:01') + + def test_ofp_version(self): + """Test ofp_version property.""" + self.assertEqual(self.switch.ofp_version, '0x04') + + def test_ofp_version__none(self): + """Test ofp_version property when connection is none.""" + self.switch.connection = None + self.assertIsNone(self.switch.ofp_version) def tearDown(self): """TearDown.""" @@ -33,19 +68,16 @@ def tearDown(self): def test_switch_vlan_pool_default(self): """Test default vlan_pool value.""" - self.assertEqual(self.options.vlan_pool, '{}') + self.assertEqual(self.options.vlan_pool, {}) def test_switch_vlan_pool_options(self): """Test switch with the example from kytos.conf.""" dpid = "00:00:00:00:00:00:00:01" - vlan_pool_json = '{"00:00:00:00:00:00:00:01": ' \ - + '{"1": [[1, 2], [5, 10]], "4": [[3, 4]]}}' - switch = Switch(dpid) - self.controller.switches[dpid] = switch - self.options.vlan_pool = vlan_pool_json - switch.connection = Mock() - switch.connection.protocol.version = 0x04 - self.controller.get_switch_or_create(dpid, switch.connection) + vlan_pool = {"00:00:00:00:00:00:00:01": + {"1": [[1, 2], [5, 10]], "4": [[3, 4]]}} + self.controller.switches[dpid] = self.switch + self.options.vlan_pool = vlan_pool + self.controller.get_switch_or_create(dpid, self.switch.connection) port_id = 1 intf = self.controller.switches[dpid].interfaces[port_id] @@ -59,7 +91,7 @@ def test_switch_vlan_pool_options(self): # this port number doesn't exist yet. port_7 = 7 - intf = Interface("test", port_7, switch) + intf = Interface("test", port_7, self.switch) # no attr filters, so should associate as it is self.controller.switches[dpid].update_interface(intf) intf_obj = self.controller.switches[dpid].interfaces[port_7] @@ -67,3 +99,238 @@ def test_switch_vlan_pool_options(self): # assert default vlan_pool range (1, 4096) tag_values = [tag.value for tag in intf_obj.available_tags] self.assertEqual(tag_values, list(range(1, 4096))) + + def test_update_description(self): + """Test update_description method.""" + desc = MagicMock() + desc.mfr_desc.value = 'mfr_desc' + desc.hw_desc.value = 'hw_desc' + desc.sw_desc.value = 'sw_desc' + desc.serial_num.value = 'serial_num' + desc.dp_desc.value = 'dp_desc' + + self.switch.update_description(desc) + + self.assertEqual(self.switch.description['manufacturer'], 'mfr_desc') + self.assertEqual(self.switch.description['hardware'], 'hw_desc') + self.assertEqual(self.switch.description['software'], 'sw_desc') + self.assertEqual(self.switch.description['serial'], 'serial_num') + self.assertEqual(self.switch.description['data_path'], 'dp_desc') + + def test_disable(self): + """Test disable method.""" + interface = MagicMock() + self.switch.interfaces = {"1": interface} + + self.switch.disable() + + interface.disable.assert_called() + self.assertFalse(self.switch._enabled) + + def test_disconnect(self): + """Test disconnect method.""" + self.switch.disconnect() + + self.assertIsNone(self.switch.connection) + + def test_get_interface_by_port_no(self): + """Test get_interface_by_port_no method.""" + interface_1 = MagicMock(port_number='1') + interface_2 = MagicMock(port_number='2') + self.switch.interfaces = {'1': interface_1, '2': interface_2} + + expected_interface_1 = self.switch.get_interface_by_port_no('1') + expected_interface_2 = self.switch.get_interface_by_port_no('3') + + self.assertEqual(expected_interface_1, interface_1) + self.assertIsNone(expected_interface_2) + + def test_get_flow_by_id(self): + """Test get_flow_by_id method.""" + flow_1 = MagicMock(id='1') + flow_2 = MagicMock(id='2') + self.switch.flows = [flow_1, flow_2] + + expected_flow_1 = self.switch.get_flow_by_id('1') + expected_flow_2 = self.switch.get_flow_by_id('3') + + self.assertEqual(expected_flow_1, flow_1) + self.assertIsNone(expected_flow_2) + + def test_is_connected__true(self): + """Test is_connected method.""" + connection = MagicMock() + connection.is_alive.return_value = True + connection.is_established.return_value = True + self.switch.connection = connection + self.switch.is_active = MagicMock() + self.switch.is_active.return_value = True + + self.assertTrue(self.switch.is_connected()) + + def test_is_connected__not_connection(self): + """Test is_connected method when connection does not exist.""" + self.switch.connection = None + self.switch.is_active = MagicMock() + self.switch.is_active.return_value = True + + self.assertFalse(self.switch.is_connected()) + + def test_is_connected__not_alive(self): + """Test is_connected method when switch is not active.""" + connection = MagicMock() + connection.is_alive.return_value = True + connection.is_established.return_value = True + self.switch.connection = connection + self.switch.is_active = MagicMock() + self.switch.is_active.return_value = False + + self.assertFalse(self.switch.is_connected()) + + def test_update_connection(self): + """Test update_connection method.""" + connection = MagicMock() + self.switch.update_connection(connection) + + self.assertEqual(self.switch.connection, connection) + self.assertEqual(self.switch.connection.switch, self.switch) + + def test_update_features(self): + """Test update_features method.""" + self.switch.update_features('features') + + self.assertEqual(self.switch.features, 'features') + + def test_send(self): + """Test send method.""" + self.switch.send('buffer') + + self.switch.connection.send.assert_called_with('buffer') + + @patch('kytos.core.switch.now', return_value=get_date()) + def test_update_lastseen(self, mock_now): + """Test update_lastseen method.""" + self.switch.update_lastseen() + + self.assertEqual(self.switch.lastseen, mock_now.return_value) + + def test_update_interface(self): + """Test update_interface method.""" + interface = MagicMock(port_number=1) + self.switch.update_interface(interface) + + self.assertEqual(self.switch.interfaces[1], interface) + + def test_remove_interface(self): + """Test remove_interface method.""" + interface = MagicMock(port_number=1) + self.switch.interfaces[1] = interface + + self.switch.remove_interface(interface) + + self.assertEqual(self.switch.interfaces, {}) + + def test_update_mac_table(self): + """Test update_mac_table method.""" + mac = MagicMock(value='00:00:00:00:00:00') + self.switch.update_mac_table(mac, 1) + self.switch.update_mac_table(mac, 2) + + self.assertEqual(self.switch.mac2port[mac.value], {1, 2}) + + def test_last_flood(self): + """Test last_flood method.""" + self.switch.flood_table['hash'] = 'timestamp' + ethernet_frame = MagicMock() + ethernet_frame.get_hash.return_value = 'hash' + + last_flood = self.switch.last_flood(ethernet_frame) + + self.assertEqual(last_flood, 'timestamp') + + def test_last_flood__error(self): + """Test last_flood method to error case.""" + ethernet_frame = MagicMock() + ethernet_frame.get_hash.return_value = 'hash' + + last_flood = self.switch.last_flood(ethernet_frame) + + self.assertIsNone(last_flood) + + @patch('kytos.core.switch.now', return_value=get_date()) + def test_should_flood(self, _): + """Test should_flood method.""" + self.switch.flood_table['hash1'] = datetime(2000, 1, 1, 0, 0, 0, 0) + self.switch.flood_table['hash2'] = datetime(2000, 1, 1, 0, 0, 0, + FLOOD_TIMEOUT) + + ethernet_frame = MagicMock() + ethernet_frame.get_hash.side_effect = ['hash1', 'hash2'] + + should_flood_1 = self.switch.should_flood(ethernet_frame) + should_flood_2 = self.switch.should_flood(ethernet_frame) + + self.assertTrue(should_flood_1) + self.assertFalse(should_flood_2) + + @patch('kytos.core.switch.now', return_value=get_date()) + def test_update_flood_table(self, mock_now): + """Test update_flood_table method.""" + ethernet_frame = MagicMock() + ethernet_frame.get_hash.return_value = 'hash' + + self.switch.update_flood_table(ethernet_frame) + + self.assertEqual(self.switch.flood_table['hash'], + mock_now.return_value) + + def test_where_is_mac(self): + """Test where_is_mac method.""" + mac = MagicMock(value='00:00:00:00:00:00') + + expected_ports_1 = self.switch.where_is_mac(mac) + + self.switch.mac2port['00:00:00:00:00:00'] = set([1, 2, 3]) + expected_ports_2 = self.switch.where_is_mac(mac) + + self.assertIsNone(expected_ports_1) + self.assertEqual(expected_ports_2, [1, 2, 3]) + + def test_as_dict(self): + """Test as_dict method.""" + expected_dict = {'id': '00:00:00:00:00:00:00:01', + 'name': '00:00:00:00:00:00:00:01', + 'dpid': '00:00:00:00:00:00:00:01', + 'connection': 'addr:port', + 'ofp_version': '0x04', + 'type': 'switch', + 'manufacturer': '', + 'serial': '', + 'hardware': '', + 'software': None, + 'data_path': '', + 'interfaces': {}, + 'metadata': {}, + 'active': True, + 'enabled': True} + self.assertEqual(self.switch.as_dict(), expected_dict) + + def test_as_json(self): + """Test as_json method.""" + expected_json = json.dumps({'id': '00:00:00:00:00:00:00:01', + 'name': '00:00:00:00:00:00:00:01', + 'dpid': '00:00:00:00:00:00:00:01', + 'connection': 'addr:port', + 'ofp_version': '0x04', + 'type': 'switch', + 'manufacturer': '', + 'serial': '', + 'hardware': '', + 'software': None, + 'data_path': '', + 'interfaces': {}, + 'metadata': {}, + 'active': True, + 'enabled': True}) + + self.assertEqual(self.switch.as_json(), expected_json) diff --git a/tests/unit/test_lib/__init__.py b/tests/unit/test_lib/__init__.py new file mode 100644 index 000000000..3d2529790 --- /dev/null +++ b/tests/unit/test_lib/__init__.py @@ -0,0 +1 @@ +"""Test kytos lib functionalities.""" diff --git a/tests/unit/test_lib/test_helpers.py b/tests/unit/test_lib/test_helpers.py new file mode 100644 index 000000000..48b339d0e --- /dev/null +++ b/tests/unit/test_lib/test_helpers.py @@ -0,0 +1,95 @@ +"""Test kytos.lib.helpers module.""" +import asyncio +from unittest import TestCase +from unittest.mock import MagicMock + +from kytos.core.controller import Controller +from kytos.lib.helpers import (get_connection_mock, get_controller_mock, + get_interface_mock, get_kytos_event_mock, + get_link_mock, get_switch_mock, get_test_client) + + +class TestHelpers(TestCase): + """Test the helpers methods.""" + + def test_controller_mock(self): + """Test controller mock.""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + controller = get_controller_mock(loop) + + self.assertEqual(type(controller), Controller) + + def test_interface_mock(self): + """Test interface mock.""" + switch = MagicMock() + switch.dpid = "00:00:00:00:00:00:00:01" + interface_mock = get_interface_mock('name', 123, switch) + + self.assertEqual(interface_mock.id, '00:00:00:00:00:00:00:01:123') + self.assertEqual(interface_mock.name, 'name') + self.assertEqual(interface_mock.port_number, 123) + self.assertEqual(interface_mock.switch, switch) + self.assertEqual(interface_mock.address, '00:00:00:00:00:00') + self.assertTrue(interface_mock.lldp) + + def test_link_mock(self): + """Test link mock.""" + endpoint_a = MagicMock() + endpoint_b = MagicMock() + link_mock = get_link_mock(endpoint_a, endpoint_b) + + self.assertEqual(link_mock.endpoint_a, endpoint_a) + self.assertEqual(link_mock.endpoint_b, endpoint_b) + self.assertEqual(link_mock.metadata, {"A": 0, "BB": 0.0, + "CCC": "test"}) + + def test_switch_mock(self): + """Test switch mock.""" + dpid = "00:00:00:00:00:00:00:01" + switch_mock = get_switch_mock(dpid, 0x04) + + self.assertEqual(switch_mock.dpid, dpid) + self.assertEqual(switch_mock.ofp_version, '0x04') + self.assertEqual(switch_mock.connection.protocol.version, 0x04) + self.assertEqual(switch_mock.connection.switch, switch_mock) + self.assertEqual(switch_mock.connection.address, '00:00:00:00:00:00') + self.assertEqual(switch_mock.connection.state.value, 0) + + def test_connection_mock(self): + """Test connection mock.""" + switch = MagicMock() + connection_mock = get_connection_mock(0x04, switch, 'addr', 123) + + self.assertEqual(connection_mock.protocol.version, 0x04) + self.assertEqual(connection_mock.switch, switch) + self.assertEqual(connection_mock.address, 'addr') + self.assertEqual(connection_mock.state, 123) + + def test_kytos_event_mock(self): + """Test kytos_event mock.""" + kytos_event_mock = get_kytos_event_mock(name='event', + content={'message': 'msg', + 'destination': 'dest', + 'source': 'src'}) + self.assertEqual(kytos_event_mock.name, 'event') + self.assertEqual(kytos_event_mock.message, 'msg') + self.assertEqual(kytos_event_mock.destination, 'dest') + self.assertEqual(kytos_event_mock.source, 'src') + + def test_get_test_client(self): + """Test get_test_client method.""" + napp = MagicMock() + + api_server = MagicMock() + api_server.app.test_client.return_value = 'client' + + loop = asyncio.new_event_loop() + asyncio.set_event_loop(None) + controller = get_controller_mock(loop) + controller.api_server = api_server + + test_client = get_test_client(controller, napp) + + api_server.register_napp_endpoints.assert_called_with(napp) + self.assertEqual(test_client, 'client') diff --git a/tox.ini b/tox.ini index 4e0bd4a0c..ac61fd299 100644 --- a/tox.ini +++ b/tox.ini @@ -1,15 +1,18 @@ [tox] -#envlist = py36,py37 -envlist = py36 +#envlist = py36,py38 [testenv] whitelist_externals= rm + make commands= ; Force packaging even if setup.{py,cfg} haven't changed rm -rf ./kytos.egg-info/ python setup.py ci + # The build of the documentation is running outside the tox env, so it is + # necessary to clear the build after running ci. + make clean -C docs deps= -rrequirements/dev.txt