Added JWT auth, fixed processing lock issue wiof removal of tasks from nodes that are offline

pull/95/head
Piero Toffanin 2017-02-08 12:30:11 -05:00
rodzic eae691cf73
commit 199ffbbfdf
7 zmienionych plików z 55 dodań i 34 usunięć

Wyświetl plik

@ -76,6 +76,10 @@ Have you had other issues? Please [report them](https://github.com/OpenDroneMap/
WebODM can be linked to one or more processing nodes running [node-OpenDroneMap](https://github.com/pierotofy/node-OpenDroneMap). The default configuration already includes a "node-odm-1" processing node which runs on the same machine as WebODM, just to help you get started. As you become more familiar with WebODM, you might want to install processing nodes on separate machines. WebODM can be linked to one or more processing nodes running [node-OpenDroneMap](https://github.com/pierotofy/node-OpenDroneMap). The default configuration already includes a "node-odm-1" processing node which runs on the same machine as WebODM, just to help you get started. As you become more familiar with WebODM, you might want to install processing nodes on separate machines.
### Security
If you want to run WebODM in production, make sure to change the `SECRET_KEY` variable in `webodm/settings.py`, as well as any other relevant setting as indicated in the [Django Deployment Checklist](https://docs.djangoproject.com/en/1.10/howto/deployment/checklist/).
## Run it natively ## Run it natively
If you want to run WebODM natively, you will need to install: If you want to run WebODM natively, you will need to install:

Wyświetl plik

@ -20,7 +20,7 @@ from guardian.shortcuts import get_perms_for_model, assign_perm
from app import pending_actions from app import pending_actions
from app.postgis import OffDbRasterField from app.postgis import OffDbRasterField
from nodeodm import status_codes from nodeodm import status_codes
from nodeodm.exceptions import ProcessingException from nodeodm.exceptions import ProcessingError, ProcessingTimeout, ProcessingException
from nodeodm.models import ProcessingNode from nodeodm.models import ProcessingNode
from webodm import settings from webodm import settings
@ -149,7 +149,7 @@ class Task(models.Model):
def __str__(self): def __str__(self):
name = self.name if self.name is not None else "unnamed" name = self.name if self.name is not None else "unnamed"
return 'Task {} ({})'.format(name, self.id) return 'Task [{}] ({})'.format(name, self.id)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Autovalidate on save # Autovalidate on save
@ -209,21 +209,21 @@ class Task(models.Model):
# TODO: log process has started processing # TODO: log process has started processing
except ProcessingException as e: except ProcessingError as e:
self.set_failure(str(e)) self.set_failure(str(e))
if self.pending_action is not None: if self.pending_action is not None:
try: try:
if self.pending_action == pending_actions.CANCEL: if self.pending_action == pending_actions.CANCEL:
# Do we need to cancel the task on the processing node? # Do we need to cancel the task on the processing node?
logger.info("Canceling task {}".format(self)) logger.info("Canceling {}".format(self))
if self.processing_node and self.uuid: if self.processing_node and self.uuid:
self.processing_node.cancel_task(self.uuid) self.processing_node.cancel_task(self.uuid)
self.pending_action = None self.pending_action = None
self.status = None self.status = None
self.save() self.save()
else: else:
raise ProcessingException("Cannot cancel a task that has no processing node or UUID") raise ProcessingError("Cannot cancel a task that has no processing node or UUID")
elif self.pending_action == pending_actions.RESTART: elif self.pending_action == pending_actions.RESTART:
logger.info("Restarting {}".format(self)) logger.info("Restarting {}".format(self))
@ -237,7 +237,7 @@ class Task(models.Model):
try: try:
info = self.processing_node.get_task_info(self.uuid) info = self.processing_node.get_task_info(self.uuid)
uuid_still_exists = info['uuid'] == self.uuid uuid_still_exists = info['uuid'] == self.uuid
except ProcessingException: except ProcessingError:
pass pass
if uuid_still_exists: if uuid_still_exists:
@ -257,10 +257,10 @@ class Task(models.Model):
self.pending_action = None self.pending_action = None
self.save() self.save()
else: else:
raise ProcessingException("Cannot restart a task that has no processing node or UUID") raise ProcessingError("Cannot restart a task that has no processing node or UUID")
elif self.pending_action == pending_actions.REMOVE: elif self.pending_action == pending_actions.REMOVE:
logger.info("Removing task {}".format(self)) logger.info("Removing {}".format(self))
if self.processing_node and self.uuid: if self.processing_node and self.uuid:
# Attempt to delete the resources on the processing node # Attempt to delete the resources on the processing node
# We don't care if this fails, as resources on processing nodes # We don't care if this fails, as resources on processing nodes
@ -276,7 +276,7 @@ class Task(models.Model):
# Stop right here! # Stop right here!
return return
except ProcessingException as e: except ProcessingError as e:
self.last_error = str(e) self.last_error = str(e)
self.save() self.save()
@ -336,7 +336,7 @@ class Task(models.Model):
logger.info("Imported orthophoto {} for {}".format(orthophoto_4326_path, self)) logger.info("Imported orthophoto {} for {}".format(orthophoto_4326_path, self))
self.save() self.save()
except ProcessingException as e: except ProcessingError as e:
self.set_failure(str(e)) self.set_failure(str(e))
else: else:
# FAILED, CANCELED # FAILED, CANCELED
@ -344,11 +344,11 @@ class Task(models.Model):
else: else:
# Still waiting... # Still waiting...
self.save() self.save()
except ProcessingException as e: except ProcessingError as e:
self.set_failure(str(e)) self.set_failure(str(e))
except (ConnectionRefusedError, ConnectionError) as e: except (ConnectionRefusedError, ConnectionError) as e:
logger.warning("{} cannot communicate with processing node: {}".format(self, str(e))) logger.warning("{} cannot communicate with processing node: {}".format(self, str(e)))
except requests.exceptions.ConnectTimeout as e: except ProcessingTimeout as e:
logger.warning("{} timed out with error: {}. We'll try reprocessing at the next tick.".format(self, str(e))) logger.warning("{} timed out with error: {}. We'll try reprocessing at the next tick.".format(self, str(e)))

Wyświetl plik

@ -1,2 +1,10 @@
class ProcessingException(Exception): class ProcessingException(Exception):
pass pass
class ProcessingError(ProcessingException):
pass
class ProcessingTimeout(ProcessingException):
pass

Wyświetl plik

@ -12,7 +12,7 @@ from .api_client import ApiClient
import json import json
from django.db.models import signals from django.db.models import signals
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from .exceptions import ProcessingException from .exceptions import ProcessingError, ProcessingTimeout
import simplejson import simplejson
def api(func): def api(func):
""" """
@ -23,7 +23,9 @@ def api(func):
try: try:
return func(*args, **kwargs) return func(*args, **kwargs)
except (json.decoder.JSONDecodeError, simplejson.JSONDecodeError) as e: except (json.decoder.JSONDecodeError, simplejson.JSONDecodeError) as e:
raise ProcessingException(str(e)) raise ProcessingError(str(e))
except (requests.exceptions.ConnectTimeout, requests.exceptions.ConnectionError) as e:
raise ProcessingTimeout(str(e))
return wrapper return wrapper
@ -83,20 +85,20 @@ class ProcessingNode(models.Model):
:returns UUID of the newly created task :returns UUID of the newly created task
""" """
if len(images) < 2: raise ProcessingException("Need at least 2 images") if len(images) < 2: raise ProcessingError("Need at least 2 images")
api_client = self.api_client() api_client = self.api_client()
try: try:
result = api_client.new_task(images, name, options) result = api_client.new_task(images, name, options)
except requests.exceptions.ConnectionError as e: except requests.exceptions.ConnectionError as e:
raise ProcessingException(e) raise ProcessingError(e)
if isinstance(result, dict) and 'uuid' in result: if isinstance(result, dict) and 'uuid' in result:
return result['uuid'] return result['uuid']
elif isinstance(result, dict) and 'error' in result: elif isinstance(result, dict) and 'error' in result:
raise ProcessingException(result['error']) raise ProcessingError(result['error'])
else: else:
raise ProcessingException("Unexpected answer from server: {}".format(result)) raise ProcessingError("Unexpected answer from server: {}".format(result))
@api @api
def get_task_info(self, uuid): def get_task_info(self, uuid):
@ -110,9 +112,9 @@ class ProcessingNode(models.Model):
if isinstance(result, dict) and 'uuid' in result: if isinstance(result, dict) and 'uuid' in result:
return result return result
elif isinstance(result, dict) and 'error' in result: elif isinstance(result, dict) and 'error' in result:
raise ProcessingException(result['error']) raise ProcessingError(result['error'])
else: else:
raise ProcessingException("Unknown result from task info: {}".format(result)) raise ProcessingError("Unknown result from task info: {}".format(result))
@api @api
def get_task_console_output(self, uuid, line): def get_task_console_output(self, uuid, line):
@ -123,11 +125,11 @@ class ProcessingNode(models.Model):
api_client = self.api_client() api_client = self.api_client()
result = api_client.task_output(uuid, line) result = api_client.task_output(uuid, line)
if isinstance(result, dict) and 'error' in result: if isinstance(result, dict) and 'error' in result:
raise ProcessingException(result['error']) raise ProcessingError(result['error'])
elif isinstance(result, list): elif isinstance(result, list):
return "".join(result) return "".join(result)
else: else:
raise ProcessingException("Unknown response for console output: {}".format(result)) raise ProcessingError("Unknown response for console output: {}".format(result))
@api @api
def cancel_task(self, uuid): def cancel_task(self, uuid):
@ -153,7 +155,7 @@ class ProcessingNode(models.Model):
api_client = self.api_client() api_client = self.api_client()
res = api_client.task_download(uuid, asset) res = api_client.task_download(uuid, asset)
if isinstance(res, dict) and 'error' in res: if isinstance(res, dict) and 'error' in res:
raise ProcessingException(res['error']) raise ProcessingError(res['error'])
else: else:
return res return res
@ -174,11 +176,11 @@ class ProcessingNode(models.Model):
:return: True on success, raises ProcessingException otherwise :return: True on success, raises ProcessingException otherwise
""" """
if isinstance(result, dict) and 'error' in result: if isinstance(result, dict) and 'error' in result:
raise ProcessingException(result['error']) raise ProcessingError(result['error'])
elif isinstance(result, dict) and 'success' in result: elif isinstance(result, dict) and 'success' in result:
return True return True
else: else:
raise ProcessingException("Unknown response: {}".format(result)) raise ProcessingError("Unknown response: {}".format(result))
class Meta: class Meta:
permissions = ( permissions = (
@ -192,7 +194,7 @@ def auto_update_node_info(sender, instance, created, **kwargs):
if created: if created:
try: try:
instance.update_node_info() instance.update_node_info()
except ProcessingException: except ProcessingError:
pass pass
class ProcessingNodeUserObjectPermission(UserObjectPermissionBase): class ProcessingNodeUserObjectPermission(UserObjectPermissionBase):

Wyświetl plik

@ -6,7 +6,7 @@ from os import path
from .models import ProcessingNode from .models import ProcessingNode
from .api_client import ApiClient from .api_client import ApiClient
from requests.exceptions import ConnectionError from requests.exceptions import ConnectionError
from .exceptions import ProcessingException from .exceptions import ProcessingError
from . import status_codes from . import status_codes
current_dir = path.dirname(path.realpath(__file__)) current_dir = path.dirname(path.realpath(__file__))
@ -80,7 +80,7 @@ class TestClientApi(TestCase):
task_info = api.task_info(uuid) task_info = api.task_info(uuid)
if task_info['status']['code'] == status: if task_info['status']['code'] == status:
return True return True
except ProcessingException: except ProcessingError:
pass pass
time.sleep(0.5) time.sleep(0.5)
@ -120,25 +120,25 @@ class TestClientApi(TestCase):
self.assertTrue(isinstance(api.task_output(uuid, 0), list)) self.assertTrue(isinstance(api.task_output(uuid, 0), list))
self.assertTrue(isinstance(online_node.get_task_console_output(uuid, 0), str)) self.assertTrue(isinstance(online_node.get_task_console_output(uuid, 0), str))
self.assertRaises(ProcessingException, online_node.get_task_console_output, "wrong-uuid", 0) self.assertRaises(ProcessingError, online_node.get_task_console_output, "wrong-uuid", 0)
# Can restart task # Can restart task
self.assertTrue(online_node.restart_task(uuid)) self.assertTrue(online_node.restart_task(uuid))
self.assertRaises(ProcessingException, online_node.restart_task, "wrong-uuid") self.assertRaises(ProcessingError, online_node.restart_task, "wrong-uuid")
wait_for_status(api, uuid, status_codes.COMPLETED, 10, "Could not restart task") wait_for_status(api, uuid, status_codes.COMPLETED, 10, "Could not restart task")
# Can cancel task (should work even if we completed the task) # Can cancel task (should work even if we completed the task)
self.assertTrue(online_node.cancel_task(uuid)) self.assertTrue(online_node.cancel_task(uuid))
self.assertRaises(ProcessingException, online_node.cancel_task, "wrong-uuid") self.assertRaises(ProcessingError, online_node.cancel_task, "wrong-uuid")
# Wait for task to be canceled # Wait for task to be canceled
wait_for_status(api, uuid, status_codes.CANCELED, 5, "Could not remove task") wait_for_status(api, uuid, status_codes.CANCELED, 5, "Could not remove task")
self.assertTrue(online_node.remove_task(uuid)) self.assertTrue(online_node.remove_task(uuid))
self.assertRaises(ProcessingException, online_node.remove_task, "wrong-uuid") self.assertRaises(ProcessingError, online_node.remove_task, "wrong-uuid")
# Cannot delete task again # Cannot delete task again
self.assertRaises(ProcessingException, online_node.remove_task, uuid) self.assertRaises(ProcessingError, online_node.remove_task, uuid)
# Task has been deleted # Task has been deleted
self.assertRaises(ProcessingException, online_node.get_task_info, uuid) self.assertRaises(ProcessingError, online_node.get_task_info, uuid)

Wyświetl plik

@ -9,6 +9,7 @@ django-guardian==1.4.6
django-rest-swagger==2.1.0 django-rest-swagger==2.1.0
django-webpack-loader==0.3.3 django-webpack-loader==0.3.3
djangorestframework==3.5.1 djangorestframework==3.5.1
djangorestframework-jwt==1.9.0
drf-nested-routers==0.11.1 drf-nested-routers==0.11.1
funcsigs==1.0.2 funcsigs==1.0.2
futures==3.0.5 futures==3.0.5
@ -19,6 +20,7 @@ packaging==16.8
Pillow==3.3.1 Pillow==3.3.1
pip-autoremove==0.9.0 pip-autoremove==0.9.0
psycopg2==2.6.2 psycopg2==2.6.2
PyJWT==1.4.2
pyparsing==2.1.10 pyparsing==2.1.10
pytz==2016.6.1 pytz==2016.6.1
requests==2.11.1 requests==2.11.1

Wyświetl plik

@ -220,6 +220,11 @@ REST_FRAMEWORK = {
'rest_framework.filters.DjangoFilterBackend', 'rest_framework.filters.DjangoFilterBackend',
'rest_framework.filters.OrderingFilter', 'rest_framework.filters.OrderingFilter',
], ],
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
),
'PAGE_SIZE': 10, 'PAGE_SIZE': 10,
} }