Repository: learningequality/kolibri

Issue:
Sanity checks are not passed when running kolibri in a development env
### Observed behavior
When running kolibri in develop mode (and in the develop git branch), using npm or yarn commands, sanity checks are not applied when starting the webserver.
If a fresh installation is made , the `content`folder is not created inside the KOLIBRI_HOME directory


### Expected behavior
Using `yarn devserver` or `npm run django-devserver` should provision correctly the KOLIBRI_HOME folder.

### User-facing consequences
None


### Errors and logs
```
ERROR    Internal Server Error: /api/device/freespace/
Traceback (most recent call last):
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/django/views/decorators/csrf.py", line 58, in wrapped_view
    return view_func(*args, **kwargs)
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/rest_framework/viewsets.py", line 116, in view
    return self.dispatch(request, *args, **kwargs)
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/rest_framework/views.py", line 495, in dispatch
    response = self.handle_exception(exc)
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/rest_framework/views.py", line 455, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/datos/le/mio/kolibri/venv/local/lib/python2.7/site-packages/rest_framework/views.py", line 492, in dispatch
    response = handler(request, *args, **kwargs)
  File "/datos/le/mio/kolibri/kolibri/core/device/api.py", line 55, in list
    free = get_free_space(OPTIONS["Paths"]["CONTENT_DIR"])
  File "/datos/le/mio/kolibri/kolibri/utils/system.py", line 187, in get_free_space
    st = os.statvfs(os.path.realpath(path))
OSError: [Errno 2] No such file or directory: '/home/jose/.kolibri/content'
```

### Steps to reproduce
* Using kolibri sources,
* remove the KOLIBRI_HOME directory
* execute
```
git checkout develop
yarn devserver
```
### Context
Linux
Running from kolibri code sources




Discussion:
Hi @jredrejo ,

Does this issue still apply to you? I deleted my kolibri home directory, ran the command `yarn devserver` under the latest develop branch, and it created a new kolibri home directory for me. 

Do you mind checking if the issue still exists on your side when you have some free time? Thank you! 
@lyw07 I have just checked in develop the problem has disappeared, but in release0-13.x it persists, 
i.e.
```
mkdir /tmp/kk
KOLIBRI_HOME=/tmp/kk yarn devserver
```
gives even more problems, not only the content is not provisioned, the db is not correctly created either. 


```
ERROR    ENGINE Error in 'start' listener <bound method ServicesPlugin.start of <kolibri.utils.server.ServicesPlugin object at 0x7f25822d9a90>>
Traceback (most recent call last):
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
    return Database.Cursor.execute(self, query, params)
sqlite3.OperationalError: no such table: morango_databaseidmodel

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/cherrypy/process/wspbus.py", line 230, in publish
    output.append(listener(*args, **kwargs))
  File "/datos/le/mio/kolibri/kolibri/utils/server.py", line 106, in start
    register_zeroconf_service(port=self.port)
  File "/datos/le/mio/kolibri/kolibri/core/discovery/utils/network/search.py", line 176, in register_zeroconf_service
    device_info = get_device_info()
  File "/datos/le/mio/kolibri/kolibri/core/public/utils.py", line 11, in get_device_info
    instance_model = InstanceIDModel.get_or_create_current_instance()  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/morango/models/core.py", line 132, in get_or_create_current_instance
    "database": DatabaseIDModel.get_or_create_current_database_id(),
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/morango/models/core.py", line 85, in get_or_create_current_database_id
    return cls.objects.get(current=True)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/models/manager.py", line 85, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/models/query.py", line 374, in get
    num = len(clone)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/models/query.py", line 232, in __len__
    self._fetch_all()
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/models/query.py", line 1121, in _fetch_all
    self._result_cache = list(self._iterable_class(self))
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/models/query.py", line 53, in __iter__
    results = compiler.execute_sql(chunked_fetch=self.chunked_fetch)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/models/sql/compiler.py", line 899, in execute_sql
    raise original_exception
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/models/sql/compiler.py", line 889, in execute_sql
    cursor.execute(sql, params)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/utils.py", line 94, in __exit__
    six.reraise(dj_exc_type, dj_exc_value, traceback)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/utils/six.py", line 685, in reraise
    raise value.with_traceback(tb)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/backends/utils.py", line 64, in execute
    return self.cursor.execute(sql, params)
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/django/db/backends/sqlite3/base.py", line 328, in execute
    return Database.Cursor.execute(self, query, params)
django.db.utils.OperationalError: no such table: morango_databaseidmodel

ERROR    ENGINE Shutting down due to error in start listener:
Traceback (most recent call last):
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/cherrypy/process/wspbus.py", line 268, in start
    self.publish('start')
  File "/datos/le/mio/kolibri/venv/lib/python3.6/site-packages/cherrypy/process/wspbus.py", line 248, in publish
    raise exc
cherrypy.process.wspbus.ChannelFailures: OperationalError('no such table: morango_databaseidmodel',)
```


`kolibri start `works fine in release-0.13 though
thank you so much for checking!
actually this happens to me in develop branch too.
for 0.13.x branch, the issue is here: https://github.com/learningequality/kolibri/blob/release-v0.13.x/kolibri/utils/server.py#L106
for develop branch, the issue is here: https://github.com/learningequality/kolibri/blob/develop/kolibri/utils/server.py#L126

both are actually the same issue - in peer to peer import, we are trying to access the database to get the device info to register our device to the network before migrations have been run.

@rtibbles Is it okay to register zeroconf service after the migrations run? Thank you!
By the time it gets to this point, the migrations should have been run - so the issue is rather that we are sometimes not running the migrations. We should perhaps have an extra sanity check after Django initialization that the database has been migrated, and to run migration in case that it hasn't.

This would also deal with the issue that occurs when someone switches to Postgres and errors out with an unmigrated db.
ah I see. Thank you for the explanation! Then I think it's also an issue with developer doc: https://kolibri-dev.readthedocs.io/en/develop/getting_started.html#development-server

Resolving patch:
diff --git a/kolibri/utils/cli.py b/kolibri/utils/cli.py
--- a/kolibri/utils/cli.py
+++ b/kolibri/utils/cli.py
@@ -28,8 +28,10 @@
 from . import server
 from .debian_check import check_debian_user
 from .sanity_checks import check_content_directory_exists_and_writable
+from .sanity_checks import check_database_is_migrated
 from .sanity_checks import check_log_file_location
 from .sanity_checks import check_other_kolibri_running
+from .sanity_checks import migrate_databases
 from .system import become_daemon
 from kolibri.core.deviceadmin.utils import IncompatibleDatabase
 from kolibri.core.upgrade import matches_version
@@ -232,6 +234,9 @@ def invoke(self, ctx):
         check_debian_user(ctx.params.get("no_input"))
         setup_logging(debug=get_debug_param())
         initialize()
+        check_content_directory_exists_and_writable()
+        if not ctx.params["skip_update"]:
+            check_database_is_migrated()
         for param in initialize_params:
             ctx.params.pop(param.name)
         return super(KolibriDjangoCommand, self).invoke(ctx)
@@ -328,20 +333,6 @@ def initialize(skip_update=False):
         run_plugin_updates()
 
 
-def _migrate_databases():
-    """
-    Try to migrate all active databases. This should not be called unless Django has
-    been initialized.
-    """
-    from django.conf import settings
-
-    for database in settings.DATABASES:
-        call_command("migrate", interactive=False, database=database)
-
-    # load morango fixtures needed for certificate related operations
-    call_command("loaddata", "scopedefinitions")
-
-
 def update(old_version, new_version):
     """
     Called whenever a version change in kolibri is detected
@@ -367,7 +358,7 @@ def update(old_version, new_version):
 
     call_command("collectstatic", interactive=False, verbosity=0)
 
-    _migrate_databases()
+    migrate_databases()
 
     run_upgrades(old_version, new_version)
 
@@ -440,9 +431,6 @@ def start(port, background):
 
     create_startup_lock(port)
 
-    # Check if the content directory exists when Kolibri runs after the first time.
-    check_content_directory_exists_and_writable()
-
     # Clear old sessions up
     call_command("clearsessions")
 
diff --git a/kolibri/utils/sanity_checks.py b/kolibri/utils/sanity_checks.py
--- a/kolibri/utils/sanity_checks.py
+++ b/kolibri/utils/sanity_checks.py
@@ -4,6 +4,9 @@
 import sys
 
 import portend
+from django.apps import apps
+from django.core.management import call_command
+from django.db.utils import OperationalError
 
 from .conf import OPTIONS
 from .server import get_status
@@ -103,3 +106,45 @@ def check_log_file_location():
         if os.path.exists(old_log_path):
             new_log_path = os.path.join(home, "logs", log_location_update[log])
             shutil.move(old_log_path, new_log_path)
+
+
+def migrate_databases():
+    """
+    Try to migrate all active databases. This should not be called unless Django has
+    been initialized.
+    """
+    from django.conf import settings
+
+    for database in settings.DATABASES:
+        call_command("migrate", interactive=False, database=database)
+
+    # load morango fixtures needed for certificate related operations
+    call_command("loaddata", "scopedefinitions")
+
+
+def check_database_is_migrated():
+    """
+    Use a check that the database instance id model is initialized to check if the database
+    is in a proper state to be used. This must only be run after django initialization.
+    """
+    apps.check_apps_ready()
+    from morango.models import InstanceIDModel
+
+    try:
+        InstanceIDModel.get_or_create_current_instance()[0]
+        return
+    except OperationalError:
+        try:
+            migrate_databases()
+            return
+        except Exception as e:
+            logging.error(
+                "Tried to migrate the database but another error occurred: {}".format(e)
+            )
+    except Exception as e:
+        logging.error(
+            "Tried to check that the database was accessible and an error occurred: {}".format(
+                e
+            )
+        )
+    sys.exit(1)


Test patch: