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.
### 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
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.postgis import OffDbRasterField
from nodeodm import status_codes
from nodeodm.exceptions import ProcessingException
from nodeodm.exceptions import ProcessingError, ProcessingTimeout, ProcessingException
from nodeodm.models import ProcessingNode
from webodm import settings
@ -149,7 +149,7 @@ class Task(models.Model):
def __str__(self):
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):
# Autovalidate on save
@ -209,21 +209,21 @@ class Task(models.Model):
# TODO: log process has started processing
except ProcessingException as e:
except ProcessingError as e:
self.set_failure(str(e))
if self.pending_action is not None:
try:
if self.pending_action == pending_actions.CANCEL:
# 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:
self.processing_node.cancel_task(self.uuid)
self.pending_action = None
self.status = None
self.save()
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:
logger.info("Restarting {}".format(self))
@ -237,7 +237,7 @@ class Task(models.Model):
try:
info = self.processing_node.get_task_info(self.uuid)
uuid_still_exists = info['uuid'] == self.uuid
except ProcessingException:
except ProcessingError:
pass
if uuid_still_exists:
@ -257,10 +257,10 @@ class Task(models.Model):
self.pending_action = None
self.save()
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:
logger.info("Removing task {}".format(self))
logger.info("Removing {}".format(self))
if self.processing_node and self.uuid:
# Attempt to delete the resources on the processing node
# We don't care if this fails, as resources on processing nodes
@ -276,7 +276,7 @@ class Task(models.Model):
# Stop right here!
return
except ProcessingException as e:
except ProcessingError as e:
self.last_error = str(e)
self.save()
@ -336,7 +336,7 @@ class Task(models.Model):
logger.info("Imported orthophoto {} for {}".format(orthophoto_4326_path, self))
self.save()
except ProcessingException as e:
except ProcessingError as e:
self.set_failure(str(e))
else:
# FAILED, CANCELED
@ -344,11 +344,11 @@ class Task(models.Model):
else:
# Still waiting...
self.save()
except ProcessingException as e:
except ProcessingError as e:
self.set_failure(str(e))
except (ConnectionRefusedError, ConnectionError) as 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)))

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -6,7 +6,7 @@ from os import path
from .models import ProcessingNode
from .api_client import ApiClient
from requests.exceptions import ConnectionError
from .exceptions import ProcessingException
from .exceptions import ProcessingError
from . import status_codes
current_dir = path.dirname(path.realpath(__file__))
@ -80,7 +80,7 @@ class TestClientApi(TestCase):
task_info = api.task_info(uuid)
if task_info['status']['code'] == status:
return True
except ProcessingException:
except ProcessingError:
pass
time.sleep(0.5)
@ -120,25 +120,25 @@ class TestClientApi(TestCase):
self.assertTrue(isinstance(api.task_output(uuid, 0), list))
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
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")
# Can cancel task (should work even if we completed the task)
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_status(api, uuid, status_codes.CANCELED, 5, "Could not remove task")
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
self.assertRaises(ProcessingException, online_node.remove_task, uuid)
self.assertRaises(ProcessingError, online_node.remove_task, uuid)
# 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-webpack-loader==0.3.3
djangorestframework==3.5.1
djangorestframework-jwt==1.9.0
drf-nested-routers==0.11.1
funcsigs==1.0.2
futures==3.0.5
@ -19,6 +20,7 @@ packaging==16.8
Pillow==3.3.1
pip-autoremove==0.9.0
psycopg2==2.6.2
PyJWT==1.4.2
pyparsing==2.1.10
pytz==2016.6.1
requests==2.11.1

Wyświetl plik

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