Change delete execution

This commit is contained in:
Patrick Jentsch 2019-11-14 09:48:30 +01:00
parent 1152417419
commit bab479db20
9 changed files with 111 additions and 148 deletions

View File

@ -2,7 +2,7 @@ from app import db
from app.decorators import admin_required from app.decorators import admin_required
from app.models import Role, User from app.models import Role, User
from app.tables import AdminUserItem, AdminUserTable from app.tables import AdminUserItem, AdminUserTable
from app.utils import background_delete_user from app.background_functions import delete_user_
from flask import current_app, flash, redirect, render_template, url_for from flask import current_app, flash, redirect, render_template, url_for
from flask_login import login_required from flask_login import login_required
from . import admin from . import admin
@ -50,10 +50,9 @@ def admin_user_page(user_id):
@login_required @login_required
@admin_required @admin_required
def admin_delete_user(user_id): def admin_delete_user(user_id):
delete_thread = threading.Thread( delete_thread = threading.Thread(target=delete_user_,
target=background_delete_user, args=(current_app._get_current_object(),
args=(current_app._get_current_object(), user_id) user_id))
)
delete_thread.start() delete_thread.start()
flash('User {} has been deleted!'.format(user_id)) flash('User {} has been deleted!'.format(user_id))
return redirect(url_for('admin.for_admins_only')) return redirect(url_for('admin.for_admins_only'))

View File

@ -0,0 +1,25 @@
from .models import Corpus, Job, User
def delete_corpus_(app, corpus_id):
with app.app_context():
corpus = Corpus.query.filter_by(id=corpus_id).first()
if corpus is None:
raise Exception('Corpus {} not found!'.format(corpus_id))
corpus.delete()
def delete_job_(app, job_id):
with app.app_context():
job = Job.query.filter_by(id=job_id).first()
if job is None:
raise Exception('Job {} not found!'.format(job_id))
job.delete()
def delete_user_(app, user_id):
with app.app_context():
user = User.query.filter_by(id=user_id).first()
if user is None:
raise Exception('User {} not found!'.format(user_id))
user.delete()

View File

@ -5,7 +5,7 @@ from flask import (abort, current_app, flash, redirect, request,
from flask_login import current_user, login_required from flask_login import current_user, login_required
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from . import corpora from . import corpora
from .background_tasks import (delete_corpus_, delete_corpus_file_, from .background_functions import (delete_corpus_, delete_corpus_file_,
edit_corpus_file_) edit_corpus_file_)
from .forms import (AddCorpusFileForm, AddCorpusForm, EditCorpusFileForm, from .forms import (AddCorpusFileForm, AddCorpusForm, EditCorpusFileForm,
QueryDownloadForm, QueryForm) QueryDownloadForm, QueryForm)
@ -28,11 +28,11 @@ def add_corpus():
try: try:
os.makedirs(dir) os.makedirs(dir)
except OSError: except OSError:
flash('OSError!') flash('[ERROR]: Could not add corpus!')
db.session.remove(corpus) corpus.delete()
db.session.commit() else:
flash('Corpus added!') flash('Corpus added!')
return redirect(url_for('corpora.corpus', corpus_id=corpus.id)) return redirect(url_for('corpora.corpus', corpus_id=corpus.id))
return render_template('corpora/add_corpus.html.j2', return render_template('corpora/add_corpus.html.j2',
add_corpus_form=add_corpus_form, add_corpus_form=add_corpus_form,
title='Add corpus') title='Add corpus')

View File

@ -1,5 +1,5 @@
from app.models import Job, JobInput, JobResult from app.models import Job, JobInput, JobResult
from app.utils import background_delete_job from app.background_functions import delete_job_
from flask import (abort, current_app, flash, redirect, render_template, from flask import (abort, current_app, flash, redirect, render_template,
send_from_directory, url_for) send_from_directory, url_for)
from flask_login import current_user, login_required from flask_login import current_user, login_required
@ -23,7 +23,7 @@ def delete_job(job_id):
job = Job.query.get_or_404(job_id) job = Job.query.get_or_404(job_id)
if not (job.creator == current_user or current_user.is_administrator()): if not (job.creator == current_user or current_user.is_administrator()):
abort(403) abort(403)
delete_thread = threading.Thread(target=background_delete_job, delete_thread = threading.Thread(target=delete_job_,
args=(current_app._get_current_object(), args=(current_app._get_current_object(),
job_id)) job_id))
delete_thread.start() delete_thread.start()

View File

@ -3,8 +3,7 @@ from flask import current_app
from flask_login import UserMixin, AnonymousUserMixin from flask_login import UserMixin, AnonymousUserMixin
from itsdangerous import BadSignature, TimedJSONWebSignatureSerializer from itsdangerous import BadSignature, TimedJSONWebSignatureSerializer
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
from . import db from . import db, logger, login_manager
from . import login_manager
import os import os
import shutil import shutil
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@ -83,12 +82,9 @@ class Role(db.Model):
to them. Order of the roles dictionary determines the ID of each role. to them. Order of the roles dictionary determines the ID of each role.
User hast the ID 1 and Administrator has the ID 2. User hast the ID 1 and Administrator has the ID 2.
""" """
roles = { roles = {'User': [Permission.CREATE_JOB],
'User': [Permission.CREATE_JOB], 'Administrator': [Permission.ADMIN, Permission.CREATE_JOB,
'Administrator': [Permission.ADMIN, Permission.DELETE_JOB]}
Permission.CREATE_JOB,
Permission.DELETE_JOB]
}
default_role = 'User' default_role = 'User'
for r in roles: for r in roles:
role = Role.query.filter_by(name=r).first() role = Role.query.filter_by(name=r).first()
@ -208,17 +204,21 @@ class User(UserMixin, db.Model):
""" """
return self.can(Permission.ADMIN) return self.can(Permission.ADMIN)
def delete_user(self): def delete(self):
""" """
Delete user from database. Also delete all associated jobs and corpora Delete the user and its corpora and jobs from database and filesystem.
files.
""" """
delete_path = os.path.join('/mnt/opaque/', str(self.id)) for job in self.jobs:
while os.path.exists(delete_path): job.delete()
try: for corpus in self.corpora:
shutil.rmtree(delete_path, ignore_errors=True) corpus.delete()
except OSError: path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
pass str(self.id))
try:
shutil.rmtree(path)
except Exception as e:
logger.warning(e)
pass
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()
@ -246,9 +246,7 @@ class JobInput(db.Model):
dir = db.Column(db.String(255)) dir = db.Column(db.String(255))
job_id = db.Column(db.Integer, db.ForeignKey('jobs.id')) job_id = db.Column(db.Integer, db.ForeignKey('jobs.id'))
# Relationships # Relationships
results = db.relationship('JobResult', results = db.relationship('JobResult', backref='job_input', lazy='dynamic',
backref='job_input',
lazy='dynamic',
cascade='save-update, merge, delete') cascade='save-update, merge, delete')
def __repr__(self): def __repr__(self):
@ -314,24 +312,44 @@ class Job(db.Model):
title = db.Column(db.String(32)) title = db.Column(db.String(32))
user_id = db.Column(db.Integer, db.ForeignKey('users.id')) user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
# Relationships # Relationships
inputs = db.relationship('JobInput', inputs = db.relationship('JobInput', backref='job', lazy='dynamic',
backref='job',
lazy='dynamic',
cascade='save-update, merge, delete') cascade='save-update, merge, delete')
results = db.relationship('JobResult', results = db.relationship('JobResult', backref='job', lazy='dynamic',
backref='job',
lazy='dynamic',
cascade='save-update, merge, delete') cascade='save-update, merge, delete')
def __init__(self, **kwargs):
super(Job, self).__init__(**kwargs)
def __repr__(self): def __repr__(self):
""" """
String representation of the Job. For human readability. String representation of the Job. For human readability.
""" """
return '<Job %r>' % self.title return '<Job %r>' % self.title
def delete(self):
"""
Delete the job and its inputs and outputs from database and filesystem.
"""
self.status = 'stopping'
db.session.commit()
while self.status != 'deleted':
''' TODO: wait a second '''
db.session.refresh(self)
path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
str(self.user_id), 'jobs', str(self.id))
'''
' TODO: Remove this workaround by executing the following command
' before service removal.
'
' docker service update --mount-rm <service>
'''
while os.path.exists(path):
try:
shutil.rmtree(path)
except Exception as e:
''' TODO: Proper exception handling '''
logger.warning(e)
pass
db.session.delete(self)
db.session.commit()
def to_dict(self): def to_dict(self):
return {'id': self.id, return {'id': self.id,
'creation_date': self.creation_date.timestamp(), 'creation_date': self.creation_date.timestamp(),
@ -349,34 +367,6 @@ class Job(db.Model):
'title': self.title, 'title': self.title,
'user_id': self.user_id} 'user_id': self.user_id}
def flag_for_stop(self):
"""
Flag running or failed job (anything that is not completed) with
stopping. Opaque daemon will end services flaged with 'stopping'.
"""
self.status = 'stopping'
db.session.commit()
def delete_job(self):
"""
Delete job with given job id from database. Also delete associated job
files. Contianers are still running for a few seconds after
the associated service has been removed. This is the reason for the
while loop. The loop checks if the file path to all the job files still
exists and removes it again and again till the container did shutdown
for good.
See: https://docs.docker.com/engine/swarm/swarm-tutorial/delete-service/
"""
delete_path = os.path.join('/mnt/opaque/', str(self.user_id), 'jobs',
str(self.id))
while os.path.exists(delete_path):
try:
shutil.rmtree(delete_path, ignore_errors=True)
except OSError:
pass
db.session.delete(self)
db.session.commit()
class CorpusFile(db.Model): class CorpusFile(db.Model):
""" """
@ -394,20 +384,20 @@ class CorpusFile(db.Model):
def delete(self): def delete(self):
path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
self.dir, self.dir, self.filename)
self.filename)
try: try:
os.remove(path) os.remove(path)
except: except Exception as e:
return ''' TODO: Proper exception handling '''
logger.warning(e)
pass
self.corpus.status = 'unprepared' self.corpus.status = 'unprepared'
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()
def insert_metadata(self): def insert_metadata(self):
file = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], file = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
self.dir, self.dir, self.filename)
self.filename)
element_tree = ET.parse(file) element_tree = ET.parse(file)
text_node = element_tree.find('text') text_node = element_tree.find('text')
text_node.set('author', self.author) text_node.set('author', self.author)
@ -433,9 +423,7 @@ class Corpus(db.Model):
analysis_container_ip = db.Column(db.String(16)) analysis_container_ip = db.Column(db.String(16))
analysis_container_name = db.Column(db.String(32)) analysis_container_name = db.Column(db.String(32))
# Relationships # Relationships
files = db.relationship('CorpusFile', files = db.relationship('CorpusFile', backref='corpus', lazy='dynamic',
backref='corpus',
lazy='dynamic',
cascade='save-update, merge, delete') cascade='save-update, merge, delete')
def __repr__(self): def __repr__(self):
@ -456,13 +444,20 @@ class Corpus(db.Model):
for corpus_file in self.files: for corpus_file in self.files:
corpus_file.delete() corpus_file.delete()
path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'], path = os.path.join(current_app.config['OPAQUE_STORAGE_DIRECTORY'],
str(self.user_id), str(self.user_id), 'corpora', str(self.id))
'corpora', '''
str(self.id)) ' TODO: Remove this workaround by executing the following command
try: ' before service removal.
shutil.rmtree(path) '
except: ' docker service update --mount-rm <service>
return '''
while os.path.exists(path):
try:
shutil.rmtree(path)
except Exception as e:
''' TODO: Proper exception handling '''
logger.warning(e)
pass
db.session.delete(self) db.session.delete(self)
db.session.commit() db.session.commit()

View File

@ -1,5 +1,5 @@
from app import db, logger from app import db, logger
from app.utils import background_delete_user from app.background_functions import delete_user_
from flask import abort, current_app, flash, redirect, render_template, url_for from flask import abort, current_app, flash, redirect, render_template, url_for
from flask_login import current_user, login_required, logout_user from flask_login import current_user, login_required, logout_user
from . import profile from . import profile
@ -94,10 +94,9 @@ def delete_self():
""" """
View to delete yourslef and all associated data. View to delete yourslef and all associated data.
""" """
delete_thread = threading.Thread( delete_thread = threading.Thread(target=delete_user_,
target=background_delete_user, args=(current_app._get_current_object(),
args=(current_app._get_current_object(), current_user.id) current_user.id))
)
delete_thread.start() delete_thread.start()
logout_user() logout_user()
flash('Your account has been deleted!') flash('Your account has been deleted!')

View File

@ -46,9 +46,8 @@ def service(service):
try: try:
os.makedirs(absolut_dir) os.makedirs(absolut_dir)
except OSError: except OSError:
flash('[OSError] Could not add job!') flash('[ERROR]: Could not add job!')
db.session.delete(job) job.delete()
db.session.commit()
else: else:
for file in add_job_form.files.data: for file in add_job_form.files.data:
filename = secure_filename(file.filename) filename = secure_filename(file.filename)

View File

@ -1,54 +0,0 @@
from . import db, logger
from .models import Job, User, Corpus, CorpusFile
'''
' A list of background process functions. Functions should be called using the
Thread class from the module threading.
'''
def background_delete_user(app, current_user_id):
with app.app_context():
logger.warning('Called by delete_thread.')
logger.warning('User id is: {}.'.format(current_user_id))
jobs = Job.query.filter_by(user_id=current_user_id).all()
corpora = Corpus.query.filter_by(user_id=current_user_id).all()
logger.warning('Jobs to delete are: {}'.format(jobs))
user = User.query.get_or_404(current_user_id)
for job in jobs:
job.flag_for_stop()
logger.warning('Job status: {}'.format(job.status))
deleted = False
while deleted is False:
logger.warning('Refreshing')
db.session.refresh(job)
logger.warning('Refreshed')
if job.status == 'deleted':
logger.warning('Job status is deleted.')
job.delete_job()
deleted = True
logger.warning('Job deletion loop has ended.')
for corpus in corpora:
corpus.delete_corpus()
logger.warning('Corpus deletion loop has ended.')
user.delete_user()
def background_delete_job(app, job_id):
with app.app_context():
logger.warning('Called by delete_thread.')
logger.warning('Job id is: {}.'.format(job_id))
job = Job.query.filter_by(id=job_id).first()
logger.warning('Job object is: {}'.format(job))
logger.warning('Job status: {}'.format(job.status))
job.flag_for_stop()
logger.warning('Job status: {}'.format(job.status))
deleted = False
while deleted is False:
db.session.refresh(job)
if job.status == 'deleted':
logger.warning('Job status is deleted.')
job.delete_job()
deleted = True
logger.warning('Loop has ended.')