Repository: kytos/kytos

Issue:
Remove hard dependency on github for UI
# The Issue

In order to execute `kytosd`, the kytos needs to be able to download the UI from github. This download may be inaccessible, either from github being unavailable, firewall blocking github, or DNS unable to find github.

# Proposed Solution

- Allow for executing kytos without running the web server for the UI, while still running the web server for the api.
- Provide a different mechanism to couple the Web server for the UI to the controller.

Discussion:
josemauro: I am trying to remove the ui folder to test the changes with:
`rm -rf ./lib/python3.6/site-packages/kytos-2020.1-py3.6.egg/kytos/web-ui/`
After do that, kytos finish as follows:
`kytos $> quit                                                                                                                                           
Stopping Kytos daemon... Bye, see you!
2020-09-29 15:38:10,062 - INFO [kytos.core.controller] (MainThread) Stopping Kytos controller...
2020-09-29 15:38:10,062 - INFO [kytos.core.controller] (MainThread) Stopping Kytos
2020-09-29 15:38:10,063 - INFO [kytos.core.buffers] (MainThread) Stop signal received by Kytos buffers.
2020-09-29 15:38:10,063 - INFO [kytos.core.buffers] (MainThread) Sending KytosShutdownEvent to all apps.
2020-09-29 15:38:10,064 - INFO [kytos.core.buffers] (MainThread) [buffer: raw_event] Stop mode enabled. Rejecting new events.
2020-09-29 15:38:10,064 - INFO [kytos.core.buffers] (MainThread) [buffer: msg_in_event] Stop mode enabled. Rejecting new events.
2020-09-29 15:38:10,065 - INFO [kytos.core.buffers] (MainThread) [buffer: msg_out_event] Stop mode enabled. Rejecting new events.
2020-09-29 15:38:10,066 - INFO [kytos.core.buffers] (MainThread) [buffer: app_event] Stop mode enabled. Rejecting new events.
2020-09-29 15:38:10,070 - INFO [kytos.core.napps.napp_dir_listener] (MainThread) NAppDirListener Stopped...
2020-09-29 15:38:10,071 - INFO [kytos.core.controller] (MainThread) Stopping threadpool: <concurrent.futures.thread.ThreadPoolExecutor object at 0x7f05d1319a90>
2020-09-29 15:38:10,504 - ERROR [concurrent.futures] (ThreadPoolExecutor-2_1) exception calling callback for <Future at 0x7f05d1324400 state=finished returned NoneType>
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/concurrent/futures/_base.py", line 324, in _invoke_callbacks
    callback(self)
  File "/usr/local/lib/python3.6/asyncio/futures.py", line 417, in _call_set_state
    dest_loop.call_soon_threadsafe(_set_state, destination, source)
  File "/usr/local/lib/python3.6/asyncio/base_events.py", line 641, in call_soon_threadsafe
    self._check_closed()
  File "/usr/local/lib/python3.6/asyncio/base_events.py", line 381, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed`

Patch:
diff --git a/kytos/core/api_server.py b/kytos/core/api_server.py
index f10c3b0..ccc0c80 100644
--- a/kytos/core/api_server.py
+++ b/kytos/core/api_server.py
@@ -200,7 +200,10 @@ class APIServer:
 
     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.
@@ -227,6 +230,9 @@ class APIServer:
                 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')


Test patch:
diff --git a/tests/unit/test_core/test_api_server.py b/tests/unit/test_core/test_api_server.py
index 1d98e71..20ff2bd 100644
--- a/tests/unit/test_core/test_api_server.py
+++ b/tests/unit/test_core/test_api_server.py
@@ -113,13 +113,23 @@ class TestAPIServer(unittest.TestCase):
             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(self, mock_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')