|
- {{ e.time|date:"Y-m-d" }}
+ {{ e.time|date:"Y-m-d" }}
|
{% if e.by %}
From 93f5a5fa247c79de9aaf20d05616edd374568705 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 26 May 2022 16:23:32 -0300
Subject: [PATCH 41/99] fix: remove 'manual' as next state for 'validating'
submission state
---
ietf/name/fixtures/names.json | 1 -
.../name/migrations/0044_validating_draftsubmissionstatename.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/ietf/name/fixtures/names.json b/ietf/name/fixtures/names.json
index b0e0f62003..0d09e04a4f 100644
--- a/ietf/name/fixtures/names.json
+++ b/ietf/name/fixtures/names.json
@@ -10411,7 +10411,6 @@
"name": "Validating Submitted Draft",
"next_states": [
"uploaded",
- "manual",
"cancel"
],
"order": 10,
diff --git a/ietf/name/migrations/0044_validating_draftsubmissionstatename.py b/ietf/name/migrations/0044_validating_draftsubmissionstatename.py
index 8109c9ddff..de82bbeef9 100644
--- a/ietf/name/migrations/0044_validating_draftsubmissionstatename.py
+++ b/ietf/name/migrations/0044_validating_draftsubmissionstatename.py
@@ -14,7 +14,7 @@ def forward(apps, schema_editor):
)
new_state.next_states.set(
DraftSubmissionStateName.objects.filter(
- slug__in=['cancel', 'manual', 'uploaded'],
+ slug__in=['cancel', 'uploaded'],
)
)
From 993606fbfdbfb24ceec39cbf9d9fe310f28f3d27 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 26 May 2022 16:36:40 -0300
Subject: [PATCH 42/99] refactor: share SubmissionBaseUploadForm code with
Deprecated version
---
ietf/submit/forms.py | 508 +++++++++++++++----------------------------
1 file changed, 181 insertions(+), 327 deletions(-)
diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py
index e1011aff26..7581cad18f 100644
--- a/ietf/submit/forms.py
+++ b/ietf/submit/forms.py
@@ -43,11 +43,12 @@
from ietf.utils.text import normalize_text
from ietf.utils.xmldraft import XMLDraft, XMLParseError
-class DeprecatedSubmissionBaseUploadForm(forms.Form):
+
+class SubmissionBaseUploadForm(forms.Form):
xml = forms.FileField(label='.xml format', required=True)
def __init__(self, request, *args, **kwargs):
- super().__init__(*args, **kwargs)
+ super(SubmissionBaseUploadForm, self).__init__(*args, **kwargs)
self.remote_ip = remote_ip(request)
@@ -131,25 +132,22 @@ def clean_file(self, field_name, parser_class):
self.file_info[field_name] = parser_class(f).critical_parse()
if self.file_info[field_name].errors:
raise forms.ValidationError(self.file_info[field_name].errors)
-
return f
def clean_xml(self):
return self.clean_file("xml", XMLParser)
def clean(self):
- def format_messages(where, e, log):
- out = log.write_out.getvalue().splitlines()
- err = log.write_err.getvalue().splitlines()
+ def format_messages(where, e, log_msgs):
m = str(e)
if m:
- m = [ m ]
+ m = [m]
else:
import traceback
typ, val, tb = sys.exc_info()
m = traceback.format_exception(typ, val, tb)
m = [ l.replace('\n ', ':\n ') for l in m ]
- msgs = [s for s in (["Error from xml2rfc (%s):" % (where,)] + m + out + err) if s]
+ msgs = [s for s in (["Error from xml2rfc (%s):" % (where,)] + m + log_msgs) if s]
return msgs
if self.shutdown and not has_role(self.request.user, "Secretariat"):
@@ -164,13 +162,9 @@ def format_messages(where, e, log):
if not self.errors:
raise forms.ValidationError('Unexpected submission file types; found %s, but %s is required' % (', '.join(self.file_types), ' or '.join(self.base_formats)))
- #debug.show('self.cleaned_data["xml"]')
+ # Determine the draft name and revision. Try XML first.
if self.cleaned_data.get('xml'):
- #if not self.cleaned_data.get('txt'):
xml_file = self.cleaned_data.get('xml')
- file_name = {}
- xml2rfc.log.write_out = io.StringIO() # open(os.devnull, "w")
- xml2rfc.log.write_err = io.StringIO() # open(os.devnull, "w")
tfn = None
with ExitStack() as stack:
@stack.callback
@@ -190,139 +184,30 @@ def cleanup(): # called when context exited, even in case of exception
tfn = tf.name
for chunk in xml_file.chunks():
tf.write(chunk)
- os.environ["XML_LIBRARY"] = settings.XML_LIBRARY
- parser = xml2rfc.XmlRfcParser(str(tfn), quiet=True)
- # --- Parse the xml ---
try:
- self.xmltree = parser.parse(remove_comments=False)
- # If we have v2, run it through v2v3. Keep track of the submitted version, though.
- self.xmlroot = self.xmltree.getroot()
- self.xml_version = self.xmlroot.get('version', '2')
- if self.xml_version == '2':
- v2v3 = xml2rfc.V2v3XmlWriter(self.xmltree)
- self.xmltree.tree = v2v3.convert2to3()
- self.xmlroot = self.xmltree.getroot() # update to the new root
-
- draftname = self.xmlroot.attrib.get('docName')
- if draftname is None:
- self.add_error('xml', "No docName attribute found in the xml root element")
- name_error = validate_submission_name(draftname)
- if name_error:
- self.add_error('xml', name_error) # This is a critical and immediate failure - do not proceed with other validation.
- else:
- revmatch = re.search("-[0-9][0-9]$", draftname)
- if revmatch:
- self.revision = draftname[-2:]
- self.filename = draftname[:-3]
- else:
- self.revision = None
- self.filename = draftname
- self.title = self.xmlroot.findtext('front/title').strip()
- if type(self.title) is str:
- self.title = unidecode(self.title)
- self.title = normalize_text(self.title)
- self.abstract = (self.xmlroot.findtext('front/abstract') or '').strip()
- if type(self.abstract) is str:
- self.abstract = unidecode(self.abstract)
- author_info = self.xmlroot.findall('front/author')
- for author in author_info:
- info = {
- "name": author.attrib.get('fullname'),
- "email": author.findtext('address/email'),
- "affiliation": author.findtext('organization'),
- }
- elem = author.find('address/postal/country')
- if elem != None:
- ascii_country = elem.get('ascii', None)
- info['country'] = ascii_country if ascii_country else elem.text
-
- for item in info:
- if info[item]:
- info[item] = info[item].strip()
- self.authors.append(info)
-
- # --- Prep the xml ---
- file_name['xml'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (self.filename, self.revision, ext))
- try:
- prep = xml2rfc.PrepToolWriter(self.xmltree, quiet=True, liberal=True, keep_pis=[xml2rfc.V3_PI_TARGET])
- prep.options.accept_prepped = True
- self.xmltree.tree = prep.prep()
- if self.xmltree.tree == None:
- self.add_error('xml', "Error from xml2rfc (prep): %s" % prep.errors)
- except Exception as e:
- msgs = format_messages('prep', e, xml2rfc.log)
- self.add_error('xml', msgs)
-
- # --- Convert to txt ---
- if not ('txt' in self.cleaned_data and self.cleaned_data['txt']):
- file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (self.filename, self.revision))
- try:
- writer = xml2rfc.TextWriter(self.xmltree, quiet=True)
- writer.options.accept_prepped = True
- writer.write(file_name['txt'])
- log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
- ( os.path.dirname(file_name['xml']),
- xml2rfc.__version__,
- os.path.basename(file_name['txt']),
- os.path.basename(file_name['xml']),
- self.xml_version))
- except Exception as e:
- msgs = format_messages('txt', e, xml2rfc.log)
- log.log('\n'.join(msgs))
- self.add_error('xml', msgs)
-
- # --- Convert to html ---
- try:
- file_name['html'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.html' % (self.filename, self.revision))
- writer = xml2rfc.HtmlWriter(self.xmltree, quiet=True)
- writer.write(file_name['html'])
- self.file_types.append('.html')
- log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
- ( os.path.dirname(file_name['xml']),
- xml2rfc.__version__,
- os.path.basename(file_name['html']),
- os.path.basename(file_name['xml']),
- self.xml_version))
- except Exception as e:
- msgs = format_messages('html', e, xml2rfc.log)
- self.add_error('xml', msgs)
-
+ xml_draft = XMLDraft(tfn)
+ except XMLParseError as e:
+ msgs = format_messages('xml', e, e.parser_msgs())
+ self.add_error('xml', msgs)
+ return
except Exception as e:
- try:
- msgs = format_messages('txt', e, xml2rfc.log)
- log.log('\n'.join(msgs))
- self.add_error('xml', msgs)
- except Exception:
- self.add_error('xml', "An exception occurred when trying to process the XML file: %s" % e)
-
- # The following errors are likely noise if we have previous field
- # errors:
- if self.errors:
- raise forms.ValidationError('')
+ self.add_error('xml', f'Error parsing XML draft: {e}')
+ return
- if self.cleaned_data.get('txt'):
- # try to parse it
+ self.filename = xml_draft.filename
+ self.revision = xml_draft.revision
+ elif self.cleaned_data.get('txt'):
+ # no XML available, extract from the text if we have it
+ # n.b., this code path is unused until a subclass with a 'txt' field is created.
txt_file = self.cleaned_data['txt']
txt_file.seek(0)
bytes = txt_file.read()
- txt_file.seek(0)
try:
text = bytes.decode(self.file_info['txt'].charset)
- #
self.parsed_draft = PlaintextDraft(text, txt_file.name)
- if self.filename == None:
- self.filename = self.parsed_draft.filename
- elif self.filename != self.parsed_draft.filename:
- self.add_error('txt', "Inconsistent name information: xml:%s, txt:%s" % (self.filename, self.parsed_draft.filename))
- if self.revision == None:
- self.revision = self.parsed_draft.revision
- elif self.revision != self.parsed_draft.revision:
- self.add_error('txt', "Inconsistent revision information: xml:%s, txt:%s" % (self.revision, self.parsed_draft.revision))
- if self.title == None:
- self.title = self.parsed_draft.get_title()
- elif self.title != self.parsed_draft.get_title():
- self.add_error('txt', "Inconsistent title information: xml:%s, txt:%s" % (self.title, self.parsed_draft.get_title()))
+ self.filename = self.parsed_draft.filename
+ self.revision = self.parsed_draft.revision
except (UnicodeDecodeError, LookupError) as e:
self.add_error('txt', 'Failed decoding the uploaded file: "%s"' % str(e))
@@ -351,50 +236,52 @@ def cleanup(): # called when context exited, even in case of exception
"element has a docName attribute which provides the full draft name including "
"revision number.")
- if not self.title:
- raise forms.ValidationError("Could not extract a valid title from the upload")
-
if self.cleaned_data.get('txt') or self.cleaned_data.get('xml'):
# check group
- self.group = self.deduce_group()
-
+ self.group = self.deduce_group(self.filename)
# check existing
existing = Submission.objects.filter(name=self.filename, rev=self.revision).exclude(state__in=("posted", "cancel", "waiting-for-draft"))
if existing:
- raise forms.ValidationError(mark_safe('A submission with same name and revision is currently being processed. Check the status here.' % urlreverse("ietf.submit.views.submission_status", kwargs={ 'submission_id': existing[0].pk })))
+ raise forms.ValidationError(
+ format_html(
+ 'A submission with same name and revision is currently being processed. Check the status here.',
+ urljoin(
+ settings.IDTRACKER_BASE_URL,
+ urlreverse("ietf.submit.views.submission_status", kwargs={'submission_id': existing[0].pk}),
+ )
+ )
+ )
# cut-off
if self.revision == '00' and self.in_first_cut_off:
raise forms.ValidationError(mark_safe(self.cutoff_warning))
-
# check thresholds
today = datetime.date.today()
- self.check_submissions_tresholds(
+ self.check_submissions_thresholds(
"for the draft %s" % self.filename,
dict(name=self.filename, rev=self.revision, submission_date=today),
settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME, settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME_SIZE,
)
- self.check_submissions_tresholds(
+ self.check_submissions_thresholds(
"for the same submitter",
dict(remote_ip=self.remote_ip, submission_date=today),
settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER, settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER_SIZE,
)
if self.group:
- self.check_submissions_tresholds(
+ self.check_submissions_thresholds(
"for the group \"%s\"" % (self.group.acronym),
dict(group=self.group, submission_date=today),
settings.IDSUBMIT_MAX_DAILY_SAME_GROUP, settings.IDSUBMIT_MAX_DAILY_SAME_GROUP_SIZE,
)
- self.check_submissions_tresholds(
+ self.check_submissions_thresholds(
"across all submitters",
dict(submission_date=today),
settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE,
)
-
return super().clean()
- def check_submissions_tresholds(self, which, filter_kwargs, max_amount, max_size):
+ def check_submissions_thresholds(self, which, filter_kwargs, max_amount, max_size):
submissions = Submission.objects.filter(**filter_kwargs)
if len(submissions) > max_amount:
@@ -402,9 +289,9 @@ def check_submissions_tresholds(self, which, filter_kwargs, max_amount, max_size
if sum(s.file_size for s in submissions if s.file_size) > max_size * 1024 * 1024:
raise forms.ValidationError("Max uploaded amount %s has been reached for today (maximum is %s MB)." % (which, max_size))
- def deduce_group(self):
+ @staticmethod
+ def deduce_group(name):
"""Figure out group from name or previously submitted draft, returns None if individual."""
- name = self.filename
existing_draft = Document.objects.filter(name=name, type="draft")
if existing_draft:
group = existing_draft[0].group
@@ -418,11 +305,12 @@ def deduce_group(self):
raise forms.ValidationError("The draft name \"%s\" is missing a third part, please rename it" % name)
if name.startswith('draft-ietf-') or name.startswith("draft-irtf-"):
-
if name_parts[1] == "ietf":
group_type = "wg"
elif name_parts[1] == "irtf":
group_type = "rg"
+ else:
+ group_type = None
# first check groups with dashes
for g in Group.objects.filter(acronym__contains="-", type=group_type):
@@ -449,110 +337,21 @@ def deduce_group(self):
raise forms.ValidationError('Draft names starting with draft-%s- are restricted, please pick a differen name' % ntype)
return None
-class SubmissionBaseUploadForm(forms.Form):
- xml = forms.FileField(label='.xml format', required=True)
-
- def __init__(self, request, *args, **kwargs):
- super(SubmissionBaseUploadForm, self).__init__(*args, **kwargs)
-
- self.remote_ip = remote_ip(request)
-
- self.request = request
- self.in_first_cut_off = False
- self.cutoff_warning = ""
- self.shutdown = False
- self.set_cutoff_warnings()
-
- self.group = None
- self.filename = None
- self.revision = None
- self.title = None
- self.abstract = None
- self.authors = []
- self.parsed_draft = None
- self.file_types = []
- self.file_info = {} # indexed by file field name, e.g., 'txt', 'xml', ...
- self.xml_version = None
- # No code currently (14 Sep 2017) uses this class directly; it is
- # only used through its subclasses. The two assignments below are
- # set to trigger an exception if it is used directly only to make
- # sure that adequate consideration is made if it is decided to use it
- # directly in the future. Feel free to set these appropriately to
- # avoid the exceptions in that case:
- self.formats = None # None will raise an exception in clean() if this isn't changed in a subclass
- self.base_formats = None # None will raise an exception in clean() if this isn't changed in a subclass
-
- def set_cutoff_warnings(self):
- now = datetime.datetime.now(pytz.utc)
- meeting = Meeting.get_current_meeting()
- if not meeting:
- return
- #
- cutoff_00 = meeting.get_00_cutoff()
- cutoff_01 = meeting.get_01_cutoff()
- reopen = meeting.get_reopen_time()
- #
- cutoff_00_str = cutoff_00.strftime("%Y-%m-%d %H:%M %Z")
- cutoff_01_str = cutoff_01.strftime("%Y-%m-%d %H:%M %Z")
- reopen_str = reopen.strftime("%Y-%m-%d %H:%M %Z")
-
- # Workaround for IETF107. This would be better handled by a refactor that allowed meetings to have no cutoff period.
- if cutoff_01 >= reopen:
- return
-
- if cutoff_00 == cutoff_01:
- if now.date() >= (cutoff_00.date() - meeting.idsubmit_cutoff_warning_days) and now.date() < cutoff_00.date():
- self.cutoff_warning = ( 'The last submission time for Internet-Drafts before %s is %s.
' % (meeting, cutoff_00_str))
- elif now <= cutoff_00:
- self.cutoff_warning = (
- 'The last submission time for new Internet-Drafts before the meeting is %s. '
- 'After that, you will not be able to submit drafts until after %s (IETF-meeting local time)' % (cutoff_00_str, reopen_str, ))
- else:
- if now.date() >= (cutoff_00.date() - meeting.idsubmit_cutoff_warning_days) and now.date() < cutoff_00.date():
- self.cutoff_warning = ( 'The last submission time for new documents (i.e., version -00 Internet-Drafts) before %s is %s.
' % (meeting, cutoff_00_str) +
- 'The last submission time for revisions to existing documents before %s is %s. ' % (meeting, cutoff_01_str) )
- elif now.date() >= cutoff_00.date() and now <= cutoff_01:
- # We are in the first_cut_off
- if now < cutoff_00:
- self.cutoff_warning = (
- 'The last submission time for new documents (i.e., version -00 Internet-Drafts) before the meeting is %s. '
- 'After that, you will not be able to submit a new document until after %s (IETF-meeting local time)' % (cutoff_00_str, reopen_str, ))
- else: # No 00 version allowed
- self.cutoff_warning = (
- 'The last submission time for new documents (i.e., version -00 Internet-Drafts) was %s. '
- 'You will not be able to submit a new document until after %s (IETF-meeting local time).
'
- 'You can still submit a version -01 or higher Internet-Draft until %s' % (cutoff_00_str, reopen_str, cutoff_01_str, ))
- self.in_first_cut_off = True
- if now > cutoff_01 and now < reopen:
- self.cutoff_warning = (
- 'The last submission time for the I-D submission was %s.
'
- 'The I-D submission tool will be reopened after %s (IETF-meeting local time).' % (cutoff_01_str, reopen_str))
- self.shutdown = True
-
- def clean_file(self, field_name, parser_class):
- f = self.cleaned_data[field_name]
- if not f:
- return f
-
- self.file_info[field_name] = parser_class(f).critical_parse()
- if self.file_info[field_name].errors:
- raise forms.ValidationError(self.file_info[field_name].errors)
- return f
-
- def clean_xml(self):
- return self.clean_file("xml", XMLParser)
+class DeprecatedSubmissionBaseUploadForm(SubmissionBaseUploadForm):
def clean(self):
- def format_messages(where, e, log_msgs):
+ def format_messages(where, e, log):
+ out = log.write_out.getvalue().splitlines()
+ err = log.write_err.getvalue().splitlines()
m = str(e)
if m:
- m = [m]
+ m = [ m ]
else:
import traceback
typ, val, tb = sys.exc_info()
m = traceback.format_exception(typ, val, tb)
m = [ l.replace('\n ', ':\n ') for l in m ]
- msgs = [s for s in (["Error from xml2rfc (%s):" % (where,)] + m + log_msgs) if s]
+ msgs = [s for s in (["Error from xml2rfc (%s):" % (where,)] + m + out + err) if s]
return msgs
if self.shutdown and not has_role(self.request.user, "Secretariat"):
@@ -567,9 +366,13 @@ def format_messages(where, e, log_msgs):
if not self.errors:
raise forms.ValidationError('Unexpected submission file types; found %s, but %s is required' % (', '.join(self.file_types), ' or '.join(self.base_formats)))
- # Determine the draft name and revision. Try XML first.
+ #debug.show('self.cleaned_data["xml"]')
if self.cleaned_data.get('xml'):
+ #if not self.cleaned_data.get('txt'):
xml_file = self.cleaned_data.get('xml')
+ file_name = {}
+ xml2rfc.log.write_out = io.StringIO() # open(os.devnull, "w")
+ xml2rfc.log.write_err = io.StringIO() # open(os.devnull, "w")
tfn = None
with ExitStack() as stack:
@stack.callback
@@ -589,30 +392,139 @@ def cleanup(): # called when context exited, even in case of exception
tfn = tf.name
for chunk in xml_file.chunks():
tf.write(chunk)
+ os.environ["XML_LIBRARY"] = settings.XML_LIBRARY
+ parser = xml2rfc.XmlRfcParser(str(tfn), quiet=True)
+ # --- Parse the xml ---
try:
- xml_draft = XMLDraft(tfn)
- except XMLParseError as e:
- msgs = format_messages('xml', e, e.parser_msgs())
- self.add_error('xml', msgs)
- return
+ self.xmltree = parser.parse(remove_comments=False)
+ # If we have v2, run it through v2v3. Keep track of the submitted version, though.
+ self.xmlroot = self.xmltree.getroot()
+ self.xml_version = self.xmlroot.get('version', '2')
+ if self.xml_version == '2':
+ v2v3 = xml2rfc.V2v3XmlWriter(self.xmltree)
+ self.xmltree.tree = v2v3.convert2to3()
+ self.xmlroot = self.xmltree.getroot() # update to the new root
+
+ draftname = self.xmlroot.attrib.get('docName')
+ if draftname is None:
+ self.add_error('xml', "No docName attribute found in the xml root element")
+ name_error = validate_submission_name(draftname)
+ if name_error:
+ self.add_error('xml', name_error) # This is a critical and immediate failure - do not proceed with other validation.
+ else:
+ revmatch = re.search("-[0-9][0-9]$", draftname)
+ if revmatch:
+ self.revision = draftname[-2:]
+ self.filename = draftname[:-3]
+ else:
+ self.revision = None
+ self.filename = draftname
+ self.title = self.xmlroot.findtext('front/title').strip()
+ if type(self.title) is str:
+ self.title = unidecode(self.title)
+ self.title = normalize_text(self.title)
+ self.abstract = (self.xmlroot.findtext('front/abstract') or '').strip()
+ if type(self.abstract) is str:
+ self.abstract = unidecode(self.abstract)
+ author_info = self.xmlroot.findall('front/author')
+ for author in author_info:
+ info = {
+ "name": author.attrib.get('fullname'),
+ "email": author.findtext('address/email'),
+ "affiliation": author.findtext('organization'),
+ }
+ elem = author.find('address/postal/country')
+ if elem != None:
+ ascii_country = elem.get('ascii', None)
+ info['country'] = ascii_country if ascii_country else elem.text
+
+ for item in info:
+ if info[item]:
+ info[item] = info[item].strip()
+ self.authors.append(info)
+
+ # --- Prep the xml ---
+ file_name['xml'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s%s' % (self.filename, self.revision, ext))
+ try:
+ prep = xml2rfc.PrepToolWriter(self.xmltree, quiet=True, liberal=True, keep_pis=[xml2rfc.V3_PI_TARGET])
+ prep.options.accept_prepped = True
+ self.xmltree.tree = prep.prep()
+ if self.xmltree.tree == None:
+ self.add_error('xml', "Error from xml2rfc (prep): %s" % prep.errors)
+ except Exception as e:
+ msgs = format_messages('prep', e, xml2rfc.log)
+ self.add_error('xml', msgs)
+
+ # --- Convert to txt ---
+ if not ('txt' in self.cleaned_data and self.cleaned_data['txt']):
+ file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (self.filename, self.revision))
+ try:
+ writer = xml2rfc.TextWriter(self.xmltree, quiet=True)
+ writer.options.accept_prepped = True
+ writer.write(file_name['txt'])
+ log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
+ ( os.path.dirname(file_name['xml']),
+ xml2rfc.__version__,
+ os.path.basename(file_name['txt']),
+ os.path.basename(file_name['xml']),
+ self.xml_version))
+ except Exception as e:
+ msgs = format_messages('txt', e, xml2rfc.log)
+ log.log('\n'.join(msgs))
+ self.add_error('xml', msgs)
+
+ # --- Convert to html ---
+ try:
+ file_name['html'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.html' % (self.filename, self.revision))
+ writer = xml2rfc.HtmlWriter(self.xmltree, quiet=True)
+ writer.write(file_name['html'])
+ self.file_types.append('.html')
+ log.log("In %s: xml2rfc %s generated %s from %s (version %s)" %
+ ( os.path.dirname(file_name['xml']),
+ xml2rfc.__version__,
+ os.path.basename(file_name['html']),
+ os.path.basename(file_name['xml']),
+ self.xml_version))
+ except Exception as e:
+ msgs = format_messages('html', e, xml2rfc.log)
+ self.add_error('xml', msgs)
+
except Exception as e:
- self.add_error('xml', f'Error parsing XML draft: {e}')
- return
+ try:
+ msgs = format_messages('txt', e, xml2rfc.log)
+ log.log('\n'.join(msgs))
+ self.add_error('xml', msgs)
+ except Exception:
+ self.add_error('xml', "An exception occurred when trying to process the XML file: %s" % e)
- self.filename = xml_draft.filename
- self.revision = xml_draft.revision
- elif self.cleaned_data.get('txt'):
- # no XML available, extract from the text if we have it
+ # The following errors are likely noise if we have previous field
+ # errors:
+ if self.errors:
+ raise forms.ValidationError('')
+
+ if self.cleaned_data.get('txt'):
# try to parse it
txt_file = self.cleaned_data['txt']
txt_file.seek(0)
bytes = txt_file.read()
+ txt_file.seek(0)
try:
text = bytes.decode(self.file_info['txt'].charset)
+ #
self.parsed_draft = PlaintextDraft(text, txt_file.name)
- self.filename = self.parsed_draft.filename
- self.revision = self.parsed_draft.revision
+ if self.filename == None:
+ self.filename = self.parsed_draft.filename
+ elif self.filename != self.parsed_draft.filename:
+ self.add_error('txt', "Inconsistent name information: xml:%s, txt:%s" % (self.filename, self.parsed_draft.filename))
+ if self.revision == None:
+ self.revision = self.parsed_draft.revision
+ elif self.revision != self.parsed_draft.revision:
+ self.add_error('txt', "Inconsistent revision information: xml:%s, txt:%s" % (self.revision, self.parsed_draft.revision))
+ if self.title == None:
+ self.title = self.parsed_draft.get_title()
+ elif self.title != self.parsed_draft.get_title():
+ self.add_error('txt', "Inconsistent title information: xml:%s, txt:%s" % (self.title, self.parsed_draft.get_title()))
except (UnicodeDecodeError, LookupError) as e:
self.add_error('txt', 'Failed decoding the uploaded file: "%s"' % str(e))
@@ -641,25 +553,22 @@ def cleanup(): # called when context exited, even in case of exception
"element has a docName attribute which provides the full draft name including "
"revision number.")
+ if not self.title:
+ raise forms.ValidationError("Could not extract a valid title from the upload")
+
if self.cleaned_data.get('txt') or self.cleaned_data.get('xml'):
# check group
- self.group = self.deduce_group(self.filename)
+ self.group = self.deduce_group()
+
# check existing
existing = Submission.objects.filter(name=self.filename, rev=self.revision).exclude(state__in=("posted", "cancel", "waiting-for-draft"))
if existing:
- raise forms.ValidationError(
- format_html(
- 'A submission with same name and revision is currently being processed. Check the status here.',
- urljoin(
- settings.IDTRACKER_BASE_URL,
- urlreverse("ietf.submit.views.submission_status", kwargs={'submission_id': existing[0].pk}),
- )
- )
- )
+ raise forms.ValidationError(mark_safe('A submission with same name and revision is currently being processed. Check the status here.' % urlreverse("ietf.submit.views.submission_status", kwargs={ 'submission_id': existing[0].pk })))
# cut-off
if self.revision == '00' and self.in_first_cut_off:
raise forms.ValidationError(mark_safe(self.cutoff_warning))
+
# check thresholds
today = datetime.date.today()
@@ -684,63 +593,8 @@ def cleanup(): # called when context exited, even in case of exception
dict(submission_date=today),
settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE,
)
- return super().clean()
-
- def check_submissions_thresholds(self, which, filter_kwargs, max_amount, max_size):
- submissions = Submission.objects.filter(**filter_kwargs)
-
- if len(submissions) > max_amount:
- raise forms.ValidationError("Max submissions %s has been reached for today (maximum is %s submissions)." % (which, max_amount))
- if sum(s.file_size for s in submissions if s.file_size) > max_size * 1024 * 1024:
- raise forms.ValidationError("Max uploaded amount %s has been reached for today (maximum is %s MB)." % (which, max_size))
-
- @staticmethod
- def deduce_group(name):
- """Figure out group from name or previously submitted draft, returns None if individual."""
- existing_draft = Document.objects.filter(name=name, type="draft")
- if existing_draft:
- group = existing_draft[0].group
- if group and group.type_id not in ("individ", "area"):
- return group
- else:
- return None
- else:
- name_parts = name.split("-")
- if len(name_parts) < 3:
- raise forms.ValidationError("The draft name \"%s\" is missing a third part, please rename it" % name)
-
- if name.startswith('draft-ietf-') or name.startswith("draft-irtf-"):
- if name_parts[1] == "ietf":
- group_type = "wg"
- elif name_parts[1] == "irtf":
- group_type = "rg"
- else:
- group_type = None
-
- # first check groups with dashes
- for g in Group.objects.filter(acronym__contains="-", type=group_type):
- if name.startswith('draft-%s-%s-' % (name_parts[1], g.acronym)):
- return g
-
- try:
- return Group.objects.get(acronym=name_parts[2], type=group_type)
- except Group.DoesNotExist:
- raise forms.ValidationError('There is no active group with acronym \'%s\', please rename your draft' % name_parts[2])
- elif name.startswith("draft-rfc-"):
- return Group.objects.get(acronym="iesg")
- elif name.startswith("draft-rfc-editor-") or name.startswith("draft-rfced-") or name.startswith("draft-rfceditor-"):
- return Group.objects.get(acronym="rfceditor")
- else:
- ntype = name_parts[1].lower()
- # This covers group types iesg, iana, iab, ise, and others:
- if GroupTypeName.objects.filter(slug=ntype).exists():
- group = Group.objects.filter(acronym=ntype).first()
- if group:
- return group
- else:
- raise forms.ValidationError('Draft names starting with draft-%s- are restricted, please pick a differen name' % ntype)
- return None
+ return super().clean()
class SubmissionManualUploadForm(DeprecatedSubmissionBaseUploadForm):
From 4a44bdbc2c9f4190baedd08426e4a773fc08417d Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 27 May 2022 11:13:57 -0300
Subject: [PATCH 43/99] fix: validate text submission title, update a couple
comments
---
ietf/settings.py | 4 ++--
ietf/submit/utils.py | 13 ++++++++++---
ietf/utils/xmldraft.py | 3 +--
3 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/ietf/settings.py b/ietf/settings.py
index 9d12df6cd6..76bf4b6285 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -1172,8 +1172,8 @@ def skip_unreadable_post(record):
# Celery configuration
CELERY_TIMEZONE = 'UTC'
CELERY_BROKER_URL = 'amqp://mq/'
-CELERY_ACKS_LATE = True # failed tasks will be retried; keep tasks idempotent or disable per-task
-
+CELERY_ACKS_LATE = True # tasks aborted due to worker failure will retry; keep tasks idempotent or disable per-task
+# (CELERY_ACKS_LATE does not retry a task that fails, including due to a clean worker shutdown)
# Meetecho API setup: Uncomment this and provide real credentials to enable
# Meetecho conference creation for interim session requests
diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py
index fc10c5c9b1..f40f760443 100644
--- a/ietf/submit/utils.py
+++ b/ietf/submit/utils.py
@@ -1184,11 +1184,18 @@ def process_submission_text(submission):
text_draft = PlaintextDraft.from_file(text_path)
if submission.name != text_draft.filename:
- raise SubmissionError('Text draft filename disagrees with submission filename')
+ raise SubmissionError(
+ f'Text draft filename ({text_draft.filename}) disagrees with submission filename ({submission.name})'
+ )
if submission.rev != text_draft.revision:
- raise SubmissionError('Text draft revision disagrees with submission revision')
- if not _normalize_title(text_draft.get_title()):
+ raise SubmissionError(
+ f'Text draft revision ({text_draft.revision}) disagrees with submission revision ({submission.rev})')
+ text_title = _normalize_title(text_draft.get_title())
+ if not text_title:
raise SubmissionError('Could not extract a valid title from the text')
+ if text_title != submission.title():
+ raise SubmissionError(
+ f'Text draft title ({text_title}) disagrees with submission title ({submission.title})')
submission.abstract = text_draft.get_abstract()
submission.document_date = text_draft.get_creation_date()
diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py
index b71c591865..67902f9733 100644
--- a/ietf/utils/xmldraft.py
+++ b/ietf/utils/xmldraft.py
@@ -17,8 +17,7 @@
class XMLDraft(Draft):
"""Draft from XML source
- Currently just a holding place for get_refs() for an XML file. Can eventually expand
- to implement the other public methods of Draft as need arises.
+ Not all methods from the superclass are implemented yet.
"""
def __init__(self, xml_file):
"""Initialize XMLDraft instance
From 4c0ed30e31a1bd25e894961038954083d48e575f Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 27 May 2022 13:55:38 -0300
Subject: [PATCH 44/99] chore: disable requirements updating when celery dev
container starts
---
docker-compose.yml | 1 -
1 file changed, 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 0cefd20963..3ee690140e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -66,7 +66,6 @@ services:
image: ghcr.io/painless-security/datatracker-celery:latest
environment:
CELERY_APP: ietf
- UPDATE_REQUIREMENTS: 1
command:
- '--loglevel=INFO'
restart: unless-stopped
From 4fa4287c6102aa36d98ba839f8f4a2332d076ffe Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 27 May 2022 14:14:29 -0300
Subject: [PATCH 45/99] feat: log traceback on unexpected error during
submission processing
---
ietf/submit/utils.py | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py
index f40f760443..93f83e88fe 100644
--- a/ietf/submit/utils.py
+++ b/ietf/submit/utils.py
@@ -8,6 +8,7 @@
import pathlib
import re
import time
+import traceback
import xml2rfc
from typing import Optional # pyflakes:ignore
@@ -1193,7 +1194,7 @@ def process_submission_text(submission):
text_title = _normalize_title(text_draft.get_title())
if not text_title:
raise SubmissionError('Could not extract a valid title from the text')
- if text_title != submission.title():
+ if text_title != submission.title:
raise SubmissionError(
f'Text draft title ({text_title}) disagrees with submission title ({submission.title})')
@@ -1213,9 +1214,9 @@ def process_submission_text(submission):
def process_uploaded_submission(submission):
- def abort_submission(error_message):
+ def abort_submission(error):
cancel_submission(submission)
- create_submission_event(None, submission, f'Submission rejected: {error_message}')
+ create_submission_event(None, submission, f'Submission rejected: {error}')
if submission.state_id != 'validating':
log.log(f'Submission {submission.pk} is not in "validating" state, skipping.')
@@ -1244,8 +1245,12 @@ def abort_submission(error_message):
errors = [c.message for c in submission.checks.filter(passed__isnull=False) if not c.passed]
if len(errors) > 0:
raise SubmissionError('Checks failed: ' + ' / '.join(errors))
+ except SubmissionError as err:
+ abort_submission(err)
except Exception as err:
- abort_submission(str(err))
+ log.log(f'Unexpected exception while processing submission {submission.pk}.')
+ log.log(traceback.format_exc())
+ abort_submission('A system error occurred while processing the submission.')
# if we get here and are still "validating", accept the draft
if submission.state_id == 'validating':
From 9836b3cfd63bf54fe81813cadccc4a9df0431ef3 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 27 May 2022 14:15:19 -0300
Subject: [PATCH 46/99] feat: allow secretariat to cancel "validating"
submission
---
ietf/submit/views.py | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/ietf/submit/views.py b/ietf/submit/views.py
index 5ed543d4e7..d79c810b80 100644
--- a/ietf/submit/views.py
+++ b/ietf/submit/views.py
@@ -346,10 +346,11 @@ def submission_status(request, submission_id, access_token=None):
is_ad = area and area.has_role(request.user, "ad")
can_edit = can_edit_submission(request.user, submission, access_token) and submission.state_id == "uploaded"
- # disallow cancellation of 'validating' submissions until the async validation process is abortable
- can_cancel = ((key_matched or is_secretariat)
- and submission.state_id != 'validating'
- and submission.state.next_states.filter(slug="cancel"))
+ # disallow cancellation of 'validating' submissions except by secretariat until async process is safely abortable
+ can_cancel = (
+ (is_secretariat or (key_matched and submission.state_id != 'validating'))
+ and submission.state.next_states.filter(slug="cancel")
+ )
can_group_approve = (is_secretariat or is_ad or is_chair) and submission.state_id == "grp-appr"
can_ad_approve = (is_secretariat or is_ad) and submission.state_id == "ad-appr"
From 8d4358ed98d0d6141fdd16a26e80d8bf6322f518 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 27 May 2022 14:15:40 -0300
Subject: [PATCH 47/99] feat: indicate time since submission on the status page
---
ietf/templates/submit/submission_status.html | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/ietf/templates/submit/submission_status.html b/ietf/templates/submit/submission_status.html
index 0a83f0eba7..2650b28d95 100644
--- a/ietf/templates/submit/submission_status.html
+++ b/ietf/templates/submit/submission_status.html
@@ -135,7 +135,13 @@ Submission checks
{% elif submission.state_id == 'validating' %}
This submission is still being processed and validated. This normally takes a few minutes after
- submission. Please contact the secretariat if it has been more than an hour.
+ submission.
+ {% with earliest_event=submission.submissionevent_set.last %}
+ {% if earliest_event %}
+ It has been {{ earliest_event.time|timesince }} since submission.
+ {% endif %}
+ {% endwith %}
+ Please contact the secretariat for assistance if it has been more than an hour.
{% else %}
Meta-data from the submission
From 2ef3ec224bf54386b98a0f07daec99893f05d0e5 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 27 May 2022 17:30:44 -0300
Subject: [PATCH 48/99] perf: check submission rate thresholds earlier when
possible
No sense parsing details of a draft that is going to be dropped regardless
of those details!
---
ietf/submit/forms.py | 27 ++++++++++++++-------------
1 file changed, 14 insertions(+), 13 deletions(-)
diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py
index 7581cad18f..ac1fa48bf4 100644
--- a/ietf/submit/forms.py
+++ b/ietf/submit/forms.py
@@ -153,6 +153,19 @@ def format_messages(where, e, log_msgs):
if self.shutdown and not has_role(self.request.user, "Secretariat"):
raise forms.ValidationError('The submission tool is currently shut down')
+ # check general submission rate thresholds before doing any more work
+ today = datetime.date.today()
+ self.check_submissions_thresholds(
+ "for the same submitter",
+ dict(remote_ip=self.remote_ip, submission_date=today),
+ settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER, settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER_SIZE,
+ )
+ self.check_submissions_thresholds(
+ "across all submitters",
+ dict(submission_date=today),
+ settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE,
+ )
+
for ext in self.formats:
f = self.cleaned_data.get(ext, None)
if not f:
@@ -255,30 +268,18 @@ def cleanup(): # called when context exited, even in case of exception
# cut-off
if self.revision == '00' and self.in_first_cut_off:
raise forms.ValidationError(mark_safe(self.cutoff_warning))
- # check thresholds
- today = datetime.date.today()
-
+ # check thresholds that depend on the draft / group
self.check_submissions_thresholds(
"for the draft %s" % self.filename,
dict(name=self.filename, rev=self.revision, submission_date=today),
settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME, settings.IDSUBMIT_MAX_DAILY_SAME_DRAFT_NAME_SIZE,
)
- self.check_submissions_thresholds(
- "for the same submitter",
- dict(remote_ip=self.remote_ip, submission_date=today),
- settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER, settings.IDSUBMIT_MAX_DAILY_SAME_SUBMITTER_SIZE,
- )
if self.group:
self.check_submissions_thresholds(
"for the group \"%s\"" % (self.group.acronym),
dict(group=self.group, submission_date=today),
settings.IDSUBMIT_MAX_DAILY_SAME_GROUP, settings.IDSUBMIT_MAX_DAILY_SAME_GROUP_SIZE,
)
- self.check_submissions_thresholds(
- "across all submitters",
- dict(submission_date=today),
- settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS, settings.IDSUBMIT_MAX_DAILY_SUBMISSIONS_SIZE,
- )
return super().clean()
def check_submissions_thresholds(self, which, filter_kwargs, max_amount, max_size):
From e3428f03d289b6555ada27128ab47a0b9aeb5836 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 30 May 2022 11:56:57 -0300
Subject: [PATCH 49/99] fix: create Submission before saving to reduce race
condition window
---
ietf/submit/views.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/ietf/submit/views.py b/ietf/submit/views.py
index d79c810b80..a8fa653dbf 100644
--- a/ietf/submit/views.py
+++ b/ietf/submit/views.py
@@ -151,9 +151,12 @@ def err(code, error, messages=None):
if not hasattr(user, 'person'):
return err(400, "No person with username %s" % username)
- clear_existing_files(form)
- save_files(form)
-
+ # There is a race condition here: creating the Submission with the name/rev
+ # of this draft is meant to prevent another submission from occurring. However,
+ # if two submissions occur at the same time, both may decide that they are the
+ # only submission in progress. This may result in a Submission being posted with
+ # the wrong files. The window for this is short, though, so it's probably
+ # tolerable risk.
submission = get_submission(form)
submission.state = DraftSubmissionStateName.objects.get(slug="validating")
submission.remote_ip = form.remote_ip
@@ -161,6 +164,8 @@ def err(code, error, messages=None):
submission.submission_date = datetime.date.today()
submission.submitter = user.person.formatted_email()
submission.save()
+ clear_existing_files(form)
+ save_files(form)
create_submission_event(request, submission, desc="Uploaded submission through API")
# Wrap in on_commit so the delayed task cannot start until the view is done with the DB
From 3ad6dc5092800ca98d8bd39d90c8a56a6581783f Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 30 May 2022 13:02:08 -0300
Subject: [PATCH 50/99] fix: call deduce_group() with filename
---
ietf/submit/forms.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py
index ac1fa48bf4..578dd63f66 100644
--- a/ietf/submit/forms.py
+++ b/ietf/submit/forms.py
@@ -559,7 +559,7 @@ def cleanup(): # called when context exited, even in case of exception
if self.cleaned_data.get('txt') or self.cleaned_data.get('xml'):
# check group
- self.group = self.deduce_group()
+ self.group = self.deduce_group(self.filename)
# check existing
existing = Submission.objects.filter(name=self.filename, rev=self.revision).exclude(state__in=("posted", "cancel", "waiting-for-draft"))
From 83166c9f6e1985189476ec5cddf707eaa6fbdf09 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 30 May 2022 15:31:00 -0300
Subject: [PATCH 51/99] refactor: remove code lint
---
ietf/submit/utils.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py
index 93f83e88fe..828cb29f69 100644
--- a/ietf/submit/utils.py
+++ b/ietf/submit/utils.py
@@ -1247,7 +1247,7 @@ def abort_submission(error):
raise SubmissionError('Checks failed: ' + ' / '.join(errors))
except SubmissionError as err:
abort_submission(err)
- except Exception as err:
+ except Exception:
log.log(f'Unexpected exception while processing submission {submission.pk}.')
log.log(traceback.format_exc())
abort_submission('A system error occurred while processing the submission.')
From a941bc3f0412b71b8bae65e61203e4110113e91f Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 31 May 2022 12:23:44 -0300
Subject: [PATCH 52/99] refactor: change the api_upload URL to api/submission
---
ietf/api/urls.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/api/urls.py b/ietf/api/urls.py
index 25942b8c55..cfe80fdcd1 100644
--- a/ietf/api/urls.py
+++ b/ietf/api/urls.py
@@ -39,7 +39,7 @@
# Draft submission API
url(r'^submit/?$', submit_views.api_submit),
# Draft upload API
- url(r'^upload/?$', submit_views.api_upload),
+ url(r'^submission/?$', submit_views.api_upload),
# Draft submission state API
url(r'^submission/(?P[0-9]+)/status/?', submit_views.api_submission_status),
# Datatracker version
From 6be72502fceb6b54f5f5d937aaad5f9595c7ef32 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 31 May 2022 12:26:30 -0300
Subject: [PATCH 53/99] docs: update submission API documentation
---
ietf/submit/views.py | 2 +-
.../templates/submit/api_submission_info.html | 104 ++++++++++++++++++
ietf/templates/submit/api_submit_info.html | 14 ++-
3 files changed, 115 insertions(+), 5 deletions(-)
create mode 100644 ietf/templates/submit/api_submission_info.html
diff --git a/ietf/submit/views.py b/ietf/submit/views.py
index a8fa653dbf..2c61848040 100644
--- a/ietf/submit/views.py
+++ b/ietf/submit/views.py
@@ -118,7 +118,7 @@ def err(code, error, messages=None):
return JsonResponse(data, status=code)
if request.method == 'GET':
- return render(request, 'submit/api_submit_info.html')
+ return render(request, 'submit/api_submission_info.html')
elif request.method == 'POST':
exception = None
submission = None
diff --git a/ietf/templates/submit/api_submission_info.html b/ietf/templates/submit/api_submission_info.html
new file mode 100644
index 0000000000..8a1de33d2f
--- /dev/null
+++ b/ietf/templates/submit/api_submission_info.html
@@ -0,0 +1,104 @@
+{% extends "base.html" %}
+{# Copyright The IETF Trust 2015-2022, All Rights Reserved #}
+{% load origin ietf_filters %}
+{% block title %}Draft submission API instructions{% endblock %}
+{% block content %}
+ {% origin %}
+ Draft submission API instructions
+
+ A simplified draft submission interface, intended for automation,
+ is available at {% url 'ietf.submit.views.api_upload' %}.
+
+
+ The interface accepts only XML uploads that can be processed on the server, and
+ requires the user to have a datatracker account. A successful submit still requires
+ the same email confirmation round-trip as submissions done through the regular
+ submission tool.
+
+
+ This interface does not provide all the options which the regular submission tool does.
+ Some limitations:
+
+
+ - Only XML-only uploads are supported, not text or combined.
+ - Document replacement information cannot be supplied.
+ -
+ The server expects
multipart/form-data, supported by curl but not by wget.
+
+
+
+ It takes two parameters:
+
+
+ -
+
user which is the user login
+
+ -
+
xml, which is the submitted file
+
+
+
+ When a draft is submitted, basic checks are performed immediately and an HTTP response
+ is sent including an appropriate http result code and JSON data describing the outcome.
+
+
+ On success, the JSON data format is
+
+
+{
+ "id": "123",
+ "name": "draft-just-submitted",
+ "rev": "00",
+ "status_url": "{% absurl 'ietf.submit.views.api_submission_status' submission_id='123' %}"
+}
+
+ On error, the JSON data format is
+
+
+{
+ "error": "Description of the error"
+}
+
+ If the basic checks passed and a successful response is sent, the draft is queued for further
+ processing. Its status can be monitored by issuing GET requests to the status_url
+ indicated in the JSON response. This URL will respond with JSON data in the format
+
+
+{
+ "id": "123",
+ "state": "validating"
+}
+
+ The state validating indicates that the draft is being or waiting to be processed.
+ Any other state indicates that the draft completed validation. If the validation failed or if the
+ draft was canceled after validation, the state will be cancel.
+
+
+ Human-readable details of the draft's status and history can be found at
+ {% absurl 'ietf.submit.views.submission_status' submission_id='123' %}
+ (replacing 123 with the id for the submission).)
+
+
+ Here is an example of submitting a draft and polling its status through the API:
+
+
+$ curl -s -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_upload' %} | jq
+{
+ "id": "126375",
+ "name": "draft-user-example",
+ "rev": "00",
+ "status_url": "{% absurl 'ietf.submit.views.api_submission_status' submission_id='126375' %}"
+}
+
+$ curl -s {% absurl 'ietf.submit.views.api_submission_status' submission_id='126375' %} | jq
+{
+ "id": "126375",
+ "state": "validating"
+}
+
+$ curl -s {% absurl 'ietf.submit.views.api_submission_status' submission_id='126375' %} | jq
+{
+ "id": "126375",
+ "state": "auth"
+}
+{% endblock %}
\ No newline at end of file
diff --git a/ietf/templates/submit/api_submit_info.html b/ietf/templates/submit/api_submit_info.html
index e873e26afb..66bc488652 100644
--- a/ietf/templates/submit/api_submit_info.html
+++ b/ietf/templates/submit/api_submit_info.html
@@ -1,13 +1,19 @@
{% extends "base.html" %}
-{# Copyright The IETF Trust 2015, All Rights Reserved #}
-{% load origin %}
+{# Copyright The IETF Trust 2015-2022, All Rights Reserved #}
+{% load origin ietf_filters %}
{% block title %}Draft submission API instructions{% endblock %}
{% block content %}
{% origin %}
Draft submission API instructions
+
+ Note: API endpoint described here is known to have a slow response time or to fail
+ due to timeout for some draft submissions, particularly those with large file sizes.
+ It is recommended to use the new API endpoint
+ instead for increased reliability.
+
A simplified draft submission interface, intended for automation,
- is available at {% url 'ietf.submit.views.api_submit' %}.
+ is available at {% absurl 'ietf.submit.views.api_submit' %}.
The interface accepts only XML uploads that can be processed on the server, and
@@ -44,7 +50,7 @@ Draft submission API instructions
Here is an example:
-$ curl -S -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" https://datatracker.ietf.org/api/submit
+$ curl -S -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_submit' %}
Upload of draft-user-example OK, confirmation requests sent to:
User Name <user.name@example.com>
{% endblock %}
\ No newline at end of file
From f7792d92feee823b0195f620a1d1b8ff54f30d35 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 31 May 2022 12:27:39 -0300
Subject: [PATCH 54/99] test: add tests of api_submission's text draft
consistency checks
---
ietf/submit/tests.py | 47 +++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 46 insertions(+), 1 deletion(-)
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index 01191add1f..c3ebe4b979 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -26,7 +26,7 @@
from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames,
post_submission, validate_submission_name, validate_submission_rev,
- process_uploaded_submission)
+ process_uploaded_submission, SubmissionError, process_submission_text)
from ietf.doc.factories import DocumentFactory, WgDraftFactory, IndividualDraftFactory, IndividualRfcFactory
from ietf.doc.models import ( Document, DocAlias, DocEvent, State,
BallotPositionDocEvent, DocumentAuthor, SubmissionDocEvent )
@@ -3065,6 +3065,51 @@ def test_process_uploaded_submission_task_ignores_invalid_id(self, mock_method):
process_uploaded_submission_task(bad_pk)
self.assertEqual(mock_method.call_count, 0)
+ def test_process_submission_text_consistency_checks(self):
+ """process_submission_text should check draft metadata against submission"""
+ submission = SubmissionFactory(
+ name='draft-somebody-test',
+ rev='00',
+ title='Correct Draft Title',
+ )
+ txt_path = Path(settings.IDSUBMIT_STAGING_PATH) / 'draft-somebody-test-00.txt'
+
+ # name mismatch
+ txt, _ = submission_file(
+ 'draft-somebody-wrong-name-00', # name that appears in the file
+ 'draft-somebody-test-00.xml',
+ None,
+ 'test_submission.txt',
+ title='Correct Draft Title',
+ )
+ txt_path.open('w').write(txt.read())
+ with self.assertRaisesMessage(SubmissionError, 'disagrees with submission filename'):
+ process_submission_text(submission)
+
+ # rev mismatch
+ txt, _ = submission_file(
+ 'draft-somebody-test-01', # name that appears in the file
+ 'draft-somebody-test-00.xml',
+ None,
+ 'test_submission.txt',
+ title='Correct Draft Title',
+ )
+ txt_path.open('w').write(txt.read())
+ with self.assertRaisesMessage(SubmissionError, 'disagrees with submission revision'):
+ process_submission_text(submission)
+
+ # title mismatch
+ txt, _ = submission_file(
+ 'draft-somebody-test-00', # name that appears in the file
+ 'draft-somebody-test-00.xml',
+ None,
+ 'test_submission.txt',
+ title='Not Correct Draft Title',
+ )
+ txt_path.open('w').write(txt.read())
+ with self.assertRaisesMessage(SubmissionError, 'disagrees with submission title'):
+ process_submission_text(submission)
+
def test_status_of_validating_submission(self):
s = SubmissionFactory(state_id='validating')
url = urlreverse('ietf.submit.views.submission_status', kwargs={'submission_id': s.pk})
From 5b2760b0783d1e84ac0746306f535ad00b7741ca Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 31 May 2022 12:29:37 -0300
Subject: [PATCH 55/99] refactor: rename api_upload to api_submission to agree
with new URL
---
ietf/api/urls.py | 2 +-
ietf/submit/tests.py | 10 +++++-----
ietf/submit/views.py | 2 +-
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/ietf/api/urls.py b/ietf/api/urls.py
index cfe80fdcd1..0bdc179327 100644
--- a/ietf/api/urls.py
+++ b/ietf/api/urls.py
@@ -39,7 +39,7 @@
# Draft submission API
url(r'^submit/?$', submit_views.api_submit),
# Draft upload API
- url(r'^submission/?$', submit_views.api_upload),
+ url(r'^submission/?$', submit_views.api_submission),
# Draft submission state API
url(r'^submission/(?P[0-9]+)/status/?', submit_views.api_submission_status),
# Datatracker version
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index c3ebe4b979..8cbb51384d 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -2731,10 +2731,10 @@ def supply_extra_metadata(self, name, status_url, submitter_name, submitter_emai
# with a no-op for testing purposes.
@mock.patch.object(transaction, 'on_commit', lambda x: x())
@override_settings(IDTRACKER_BASE_URL='https://datatracker.example.com')
-class ApiUploadTests(BaseSubmitTestCase):
+class ApiSubmissionTests(BaseSubmitTestCase):
def test_upload_draft(self):
- """api_upload accepts a submission and queues it for processing"""
- url = urlreverse('ietf.submit.views.api_upload')
+ """api_submission accepts a submission and queues it for processing"""
+ url = urlreverse('ietf.submit.views.api_submission')
xml, author = submission_file('draft-somebody-test-00', 'draft-somebody-test-00.xml', None, 'test_submission.xml')
data = {
'xml': xml,
@@ -2768,9 +2768,9 @@ def test_upload_draft(self):
self.assertIn('Uploaded submission through API', submission.submissionevent_set.last().desc)
def test_rejects_broken_upload(self):
- """api_upload immediately rejects a submission with serious problems"""
+ """api_submission immediately rejects a submission with serious problems"""
orig_submission_count = Submission.objects.count()
- url = urlreverse('ietf.submit.views.api_upload')
+ url = urlreverse('ietf.submit.views.api_submission')
# invalid submitter
xml, author = submission_file('draft-somebody-test-00', 'draft-somebody-test-00.xml', None, 'test_submission.xml')
diff --git a/ietf/submit/views.py b/ietf/submit/views.py
index 2c61848040..a44ae03cda 100644
--- a/ietf/submit/views.py
+++ b/ietf/submit/views.py
@@ -110,7 +110,7 @@ def upload_submission(request):
'form': form})
@csrf_exempt
-def api_upload(request):
+def api_submission(request):
def err(code, error, messages=None):
data = {'error': error}
if messages is not None:
From ad6a96f8e01d7172a919cfe2b90a3091e8528d19 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 31 May 2022 14:17:37 -0300
Subject: [PATCH 56/99] test: test API documentation and submission thresholds
---
ietf/submit/forms.py | 3 +-
ietf/submit/tests.py | 119 +++++++++++++++++++++++++++++++++++++++++++
2 files changed, 121 insertions(+), 1 deletion(-)
diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py
index 578dd63f66..86e55bfe55 100644
--- a/ietf/submit/forms.py
+++ b/ietf/submit/forms.py
@@ -282,7 +282,8 @@ def cleanup(): # called when context exited, even in case of exception
)
return super().clean()
- def check_submissions_thresholds(self, which, filter_kwargs, max_amount, max_size):
+ @staticmethod
+ def check_submissions_thresholds(which, filter_kwargs, max_amount, max_size):
submissions = Submission.objects.filter(**filter_kwargs)
if len(submissions) > max_amount:
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index 8cbb51384d..f3cc5f97b8 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -17,6 +17,7 @@
from django.conf import settings
from django.db import transaction
+from django.forms import ValidationError
from django.test import override_settings
from django.test.client import RequestFactory
from django.urls import reverse as urlreverse
@@ -41,6 +42,7 @@
from ietf.person.models import Person
from ietf.person.factories import UserFactory, PersonFactory, EmailFactory
from ietf.submit.factories import SubmissionFactory, SubmissionExtResourceFactory
+from ietf.submit.forms import SubmissionBaseUploadForm
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
from ietf.submit.mail import add_submission_email, process_response_email
from ietf.submit.tasks import process_uploaded_submission_task
@@ -2830,6 +2832,13 @@ def test_rejects_broken_upload(self):
self.assertFalse(mock_task.delay.called)
self.assertEqual(Submission.objects.count(), orig_submission_count)
+ @override_settings(IDTRACKER_BASE_URL='http://baseurl.example.com')
+ def test_get_documentation(self):
+ """A GET to the submission endpoint retrieves documentation"""
+ r = self.client.get(urlreverse('ietf.submit.views.api_submission'))
+ self.assertTemplateUsed(r, 'submit/api_submission_info.html')
+ self.assertContains(r, 'http://baseurl.example.com', status_code=200)
+
def test_submission_status(self):
s = SubmissionFactory(state_id='validating')
url = urlreverse('ietf.submit.views.api_submission_status', kwargs={'submission_id': s.pk})
@@ -2854,6 +2863,116 @@ def test_submission_status(self):
self.assertEqual(r.status_code, 404)
+class SubmissionUploadFormTests(BaseSubmitTestCase):
+ def test_check_submission_thresholds(self):
+ today = datetime.date.today()
+ yesterday = today - datetime.timedelta(days=1)
+ (this_group, that_group) = GroupFactory.create_batch(2, type_id='wg')
+ this_ip = '10.0.0.1'
+ that_ip = '192.168.42.42'
+ one_mb = 1024 * 1024
+ this_draft = 'draft-this-draft'
+ that_draft = 'draft-different-draft'
+ SubmissionFactory(group=this_group, name=this_draft, rev='00', submission_date=yesterday, remote_ip=this_ip, file_size=one_mb)
+ SubmissionFactory(group=this_group, name=that_draft, rev='00', submission_date=yesterday, remote_ip=this_ip, file_size=one_mb)
+ SubmissionFactory(group=this_group, name=this_draft, rev='00', submission_date=today, remote_ip=this_ip, file_size=one_mb)
+ SubmissionFactory(group=this_group, name=that_draft, rev='00', submission_date=today, remote_ip=this_ip, file_size=one_mb)
+ SubmissionFactory(group=that_group, name=this_draft, rev='00', submission_date=yesterday, remote_ip=that_ip, file_size=one_mb)
+ SubmissionFactory(group=that_group, name=that_draft, rev='00', submission_date=yesterday, remote_ip=that_ip, file_size=one_mb)
+ SubmissionFactory(group=that_group, name=this_draft, rev='00', submission_date=today, remote_ip=that_ip, file_size=one_mb)
+ SubmissionFactory(group=that_group, name=that_draft, rev='00', submission_date=today, remote_ip=that_ip, file_size=one_mb)
+ SubmissionFactory(group=that_group, name=that_draft, rev='01', submission_date=today, remote_ip=that_ip, file_size=one_mb)
+
+ # Tests aim to cover the permutations of DB filters that are used by the clean() method
+ # - all IP addresses, today
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'valid today, all submitters',
+ dict(submission_date=today),
+ max_amount=5,
+ max_size=5, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max submissions'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too many today, all submitters',
+ dict(submission_date=today),
+ max_amount=4,
+ max_size=5, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max uploaded amount'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too much today, all submitters',
+ dict(submission_date=today),
+ max_amount=5,
+ max_size=4, # megabytes
+ )
+
+ # - one IP address, today
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'valid today, one submitter',
+ dict(remote_ip=this_ip, submission_date=today),
+ max_amount=2,
+ max_size=2, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max submissions'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too many today, one submitter',
+ dict(remote_ip=this_ip, submission_date=today),
+ max_amount=1,
+ max_size=2, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max uploaded amount'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too much today, one submitter',
+ dict(remote_ip=this_ip, submission_date=today),
+ max_amount=2,
+ max_size=1, # megabytes
+ )
+
+ # - single draft/rev, today
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'valid today, one draft',
+ dict(name=this_draft, rev='00', submission_date=today),
+ max_amount=2,
+ max_size=2, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max submissions'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too many today, one draft',
+ dict(name=this_draft, rev='00', submission_date=today),
+ max_amount=1,
+ max_size=2, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max uploaded amount'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too much today, one draft',
+ dict(name=this_draft, rev='00', submission_date=today),
+ max_amount=2,
+ max_size=1, # megabytes
+ )
+
+ # - one group, today
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'valid today, one group',
+ dict(group=this_group, submission_date=today),
+ max_amount=2,
+ max_size=2, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max submissions'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too many today, one group',
+ dict(group=this_group, submission_date=today),
+ max_amount=1,
+ max_size=2, # megabytes
+ )
+ with self.assertRaisesMessage(ValidationError, 'Max uploaded amount'):
+ SubmissionBaseUploadForm.check_submissions_thresholds(
+ 'too much today, one group',
+ dict(group=this_group, submission_date=today),
+ max_amount=2,
+ max_size=1, # megabytes
+ )
+
+
class AsyncSubmissionTests(BaseSubmitTestCase):
"""Tests of async submission-related tasks"""
def test_process_uploaded_submission(self):
From fb132ee43867c25cc90a2047498f8697c427272e Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 31 May 2022 14:53:03 -0300
Subject: [PATCH 57/99] fix: fix a couple api_submission view renames missed in
templates
---
ietf/templates/submit/api_submission_info.html | 4 ++--
ietf/templates/submit/api_submit_info.html | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/ietf/templates/submit/api_submission_info.html b/ietf/templates/submit/api_submission_info.html
index 8a1de33d2f..ac6ee26321 100644
--- a/ietf/templates/submit/api_submission_info.html
+++ b/ietf/templates/submit/api_submission_info.html
@@ -7,7 +7,7 @@
Draft submission API instructions
A simplified draft submission interface, intended for automation,
- is available at {% url 'ietf.submit.views.api_upload' %}.
+ is available at {% url 'ietf.submit.views.api_submission' %}.
The interface accepts only XML uploads that can be processed on the server, and
@@ -82,7 +82,7 @@ Draft submission API instructions
Here is an example of submitting a draft and polling its status through the API:
-$ curl -s -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_upload' %} | jq
+$ curl -s -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_submission' %} | jq
{
"id": "126375",
"name": "draft-user-example",
diff --git a/ietf/templates/submit/api_submit_info.html b/ietf/templates/submit/api_submit_info.html
index 66bc488652..114b979622 100644
--- a/ietf/templates/submit/api_submit_info.html
+++ b/ietf/templates/submit/api_submit_info.html
@@ -8,7 +8,7 @@ Draft submission API instructions
Note: API endpoint described here is known to have a slow response time or to fail
due to timeout for some draft submissions, particularly those with large file sizes.
- It is recommended to use the new API endpoint
+ It is recommended to use the new API endpoint
instead for increased reliability.
From 41149b5279d4ac7d2b5e5fce1dbbde1e9469cf55 Mon Sep 17 00:00:00 2001
From: Nicolas Giard
Date: Tue, 31 May 2022 16:07:41 -0400
Subject: [PATCH 58/99] chore: use base image + add arm64 support
---
.github/workflows/build-celery-worker.yml | 29 +++++---
dev/celery/Dockerfile | 86 +----------------------
docker-compose.yml | 2 +-
docker/docker-compose.celery.yml | 2 +-
4 files changed, 22 insertions(+), 97 deletions(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index 32c0119d8c..71240f893c 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -11,10 +11,6 @@ on:
workflow_dispatch:
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: datatracker-celery
-
jobs:
publish:
runs-on: ubuntu-latest
@@ -25,12 +21,25 @@ jobs:
steps:
- uses: actions/checkout@v2
- - name: Docker Build & Push Action
- uses: mr-smithers-excellent/docker-build-push@v5.6
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
with:
- image: ${{ env.IMAGE_NAME }}
- tags: latest
- registry: ${{ env.REGISTRY }}
- dockerfile: dev/celery/Dockerfile
+ registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Docker Build & Push
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: dev/celery/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+ tags: ghcr.io/ietf-tools/datatracker-celery:latest
+
diff --git a/dev/celery/Dockerfile b/dev/celery/Dockerfile
index 4d084ccee2..b923e42c03 100644
--- a/dev/celery/Dockerfile
+++ b/dev/celery/Dockerfile
@@ -1,93 +1,9 @@
# Dockerfile for celery worker
#
-FROM python:3.9-bullseye
+FROM ghcr.io/ietf-tools/datatracker-app-base:latest
LABEL maintainer="IETF Tools Team "
ENV DEBIAN_FRONTEND=noninteractive
-RUN apt-get update
-
-# Install the packages we need
-RUN apt-get update --fix-missing
-RUN apt-get install -qy \
- apache2-utils \
- apt-file \
- apt-utils \
- bash \
- build-essential \
- curl \
- enscript \
- gawk \
- gcc \
- ghostscript \
- git \
- gnupg \
- graphviz \
- jq \
- less \
- libcairo2-dev \
- libgtk2.0-0 \
- libgtk-3-0 \
- libnotify-dev \
- libgconf-2-4 \
- libgbm-dev \
- libnss3 \
- libxss1 \
- libasound2 \
- libxtst6 \
- libmagic-dev \
- libmariadb-dev \
- locales \
- mariadb-client \
- netcat \
- nodejs \
- pigz \
- pv \
- python3-ipython \
- ripgrep \
- rsync \
- rsyslog \
- ruby \
- ruby-rubygems \
- unzip \
- wget \
- xauth \
- xvfb \
- yang-tools
-
-# Install kramdown-rfc2629 (ruby)
-RUN gem install kramdown-rfc2629
-
-# Get rid of installation files we don't need in the image, to reduce size
-RUN apt-get clean && rm -rf /var/lib/apt/lists/*
-
-# Set locale to en_US.UTF-8
-RUN echo "LC_ALL=en_US.UTF-8" >> /etc/environment && \
- echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen && \
- echo "LANG=en_US.UTF-8" > /etc/locale.conf && \
- dpkg-reconfigure locales && \
- locale-gen en_US.UTF-8 && \
- update-locale LC_ALL en_US.UTF-8
-ENV LC_ALL en_US.UTF-8
-
-# Install idnits
-ADD https://raw.githubusercontent.com/ietf-tools/idnits-mirror/main/idnits /usr/local/bin/
-RUN chmod +rx /usr/local/bin/idnits
-
-# Install current datatracker python dependencies
-COPY requirements.txt /tmp/pip-tmp/
-RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
- && rm -rf /tmp/pip-tmp
-
-# Turn off rsyslog kernel logging (doesn't work in Docker)
-RUN sed -i '/imklog/s/^/#/' /etc/rsyslog.conf
-
-# Fetch wait-for utility
-ADD https://raw.githubusercontent.com/eficode/wait-for/v2.1.3/wait-for /usr/local/bin/
-RUN chmod +rx /usr/local/bin/wait-for
-
-# Create workspace
-RUN mkdir -p /workspace
-WORKDIR /workspace
# Install endpoint script
COPY dev/celery/docker-init.sh /docker-init.sh
diff --git a/docker-compose.yml b/docker-compose.yml
index 3ee690140e..4af018c7ae 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -63,7 +63,7 @@ services:
restart: unless-stopped
celery:
- image: ghcr.io/painless-security/datatracker-celery:latest
+ image: ghcr.io/ietf-tools/datatracker-celery:latest
environment:
CELERY_APP: ietf
command:
diff --git a/docker/docker-compose.celery.yml b/docker/docker-compose.celery.yml
index dac674079e..dedae2d004 100644
--- a/docker/docker-compose.celery.yml
+++ b/docker/docker-compose.celery.yml
@@ -29,7 +29,7 @@ services:
# syslog-address: "tcp://ietfa.amsl.com:514"
celery:
- image: ghcr.io/painless-security/datatracker-celery:latest
+ image: ghcr.io/ietf-tools/datatracker-celery:latest
environment:
CELERY_APP: ietf
# UPDATE_REQUIREMENTS: 1 # uncomment to update Python requirements on startup
From 46949ca3b0d4151691c68c634cc94376747d74da Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 1 Jun 2022 13:33:14 -0300
Subject: [PATCH 59/99] ci: try to fix workflow_dispatch for celery worker
---
.github/workflows/build-celery-worker.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index 32c0119d8c..ec958fb3a1 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -9,7 +9,7 @@ on:
- 'requirements.txt'
- 'dev/celery/Dockerfile'
- workflow_dispatch:
+ workflow_dispatch: {}
env:
REGISTRY: ghcr.io
From 930038292ce3bc11fee72c7bcf1f3d4b8d35b32b Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 1 Jun 2022 13:37:46 -0300
Subject: [PATCH 60/99] ci: another attempt to fix workflow_dispatch
---
.github/workflows/build-celery-worker.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index ec958fb3a1..c9378b8b7b 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -9,7 +9,7 @@ on:
- 'requirements.txt'
- 'dev/celery/Dockerfile'
- workflow_dispatch: {}
+ workflow_dispatch:
env:
REGISTRY: ghcr.io
From 234b7d4c21fd1d68537826dc53a62788c5befcac Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 1 Jun 2022 13:38:57 -0300
Subject: [PATCH 61/99] ci: build celery image for submit-async branch
---
.github/workflows/build-celery-worker.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index c9378b8b7b..bc23d48d89 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -5,6 +5,7 @@ on:
branches:
- 'main'
- 'jennifer/celery'
+ - 'jennifer/subimt-async'
paths:
- 'requirements.txt'
- 'dev/celery/Dockerfile'
From ea2174371c385219bfc8ba87f2c83cc542cea1d6 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 1 Jun 2022 13:40:27 -0300
Subject: [PATCH 62/99] ci: fix typo
---
.github/workflows/build-celery-worker.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index bc23d48d89..57020c822b 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -5,7 +5,7 @@ on:
branches:
- 'main'
- 'jennifer/celery'
- - 'jennifer/subimt-async'
+ - 'jennifer/submit-async'
paths:
- 'requirements.txt'
- 'dev/celery/Dockerfile'
From 86f039e69edfb7978152b71842e248b2894e7dea Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 1 Jun 2022 13:54:38 -0300
Subject: [PATCH 63/99] ci: publish celery worker to ghcr.io/painless-security
---
.github/workflows/build-celery-worker.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index 3e20a8ffe8..24e83f323a 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -42,5 +42,5 @@ jobs:
file: dev/celery/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
- tags: ghcr.io/ietf-tools/datatracker-celery:latest
+ tags: ghcr.io/painless-security/datatracker-celery:latest
From 86d4772e5dc0d54049f103de9a0b581cc2e0e6fb Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 2 Jun 2022 13:57:28 -0300
Subject: [PATCH 64/99] ci: install python requirements in celery image
---
dev/celery/Dockerfile | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/dev/celery/Dockerfile b/dev/celery/Dockerfile
index b923e42c03..793666653c 100644
--- a/dev/celery/Dockerfile
+++ b/dev/celery/Dockerfile
@@ -5,8 +5,17 @@ LABEL maintainer="IETF Tools Team "
ENV DEBIAN_FRONTEND=noninteractive
-# Install endpoint script
+RUN apt-get purge -y imagemagick imagemagick-6-common
+
+# Copy the startup file
COPY dev/celery/docker-init.sh /docker-init.sh
-RUN chmod +rx /docker-init.sh
+RUN sed -i 's/\r$//' /docker-init.sh && \
+ chmod +x /docker-init.sh
+
+# Install current datatracker python dependencies
+COPY requirements.txt /tmp/pip-tmp/
+RUN pip3 --disable-pip-version-check --no-cache-dir install --user --no-warn-script-location -r /tmp/pip-tmp/requirements.txt
+RUN pip3 --disable-pip-version-check --no-cache-dir install --user --no-warn-script-location pylint pylint-common pylint-django
+RUN sudo rm -rf /tmp/pip-tmp
ENTRYPOINT [ "/docker-init.sh" ]
\ No newline at end of file
From 2cfcae2eacacbf44ffb66cf1c7a760e13125c0df Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 2 Jun 2022 15:26:22 -0300
Subject: [PATCH 65/99] ci: fix up requirements install on celery image
---
dev/celery/Dockerfile | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/dev/celery/Dockerfile b/dev/celery/Dockerfile
index 793666653c..91f2949a6b 100644
--- a/dev/celery/Dockerfile
+++ b/dev/celery/Dockerfile
@@ -14,8 +14,7 @@ RUN sed -i 's/\r$//' /docker-init.sh && \
# Install current datatracker python dependencies
COPY requirements.txt /tmp/pip-tmp/
-RUN pip3 --disable-pip-version-check --no-cache-dir install --user --no-warn-script-location -r /tmp/pip-tmp/requirements.txt
-RUN pip3 --disable-pip-version-check --no-cache-dir install --user --no-warn-script-location pylint pylint-common pylint-django
-RUN sudo rm -rf /tmp/pip-tmp
+RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt
+RUN rm -rf /tmp/pip-tmp
ENTRYPOINT [ "/docker-init.sh" ]
\ No newline at end of file
From ae826ef7fc790ae92c55757b7cff21874a4a2843 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 30 Jun 2022 15:06:45 -0300
Subject: [PATCH 66/99] chore: remove XML_LIBRARY references that crept back in
---
ietf/submit/utils.py | 1 -
ietf/utils/xmldraft.py | 7 -------
2 files changed, 8 deletions(-)
diff --git a/ietf/submit/utils.py b/ietf/submit/utils.py
index 828cb29f69..f181f31a75 100644
--- a/ietf/submit/utils.py
+++ b/ietf/submit/utils.py
@@ -935,7 +935,6 @@ def render_missing_formats(submission):
"""
xml2rfc.log.write_out = io.StringIO() # open(os.devnull, "w")
xml2rfc.log.write_err = io.StringIO() # open(os.devnull, "w")
- os.environ["XML_LIBRARY"] = settings.XML_LIBRARY
xml_path = staging_path(submission.name, submission.rev, '.xml')
parser = xml2rfc.XmlRfcParser(str(xml_path), quiet=True)
# --- Parse the xml ---
diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py
index 1a83cf1470..3f3111c3dc 100644
--- a/ietf/utils/xmldraft.py
+++ b/ietf/utils/xmldraft.py
@@ -9,8 +9,6 @@
from contextlib import ExitStack
-from django.conf import settings
-
from .draft import Draft
@@ -39,7 +37,6 @@ def parse_xml(filename):
"""
orig_write_out = xml2rfc.log.write_out
orig_write_err = xml2rfc.log.write_err
- orig_xml_library = os.environ.get('XML_LIBRARY', None)
parser_out = io.StringIO()
parser_err = io.StringIO()
@@ -48,13 +45,9 @@ def parse_xml(filename):
def cleanup(): # called when context exited, even if there's an exception
xml2rfc.log.write_out = orig_write_out
xml2rfc.log.write_err = orig_write_err
- os.environ.pop('XML_LIBRARY')
- if orig_xml_library is not None:
- os.environ['XML_LIBRARY'] = orig_xml_library
xml2rfc.log.write_out = parser_out
xml2rfc.log.write_err = parser_err
- os.environ['XML_LIBRARY'] = settings.XML_LIBRARY
parser = xml2rfc.XmlRfcParser(filename, quiet=True)
try:
From 5d95b1fa5aad229690872735a1518f50697e6669 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 30 Jun 2022 18:20:45 -0300
Subject: [PATCH 67/99] feat: accept 'replaces' field in api_submission
---
ietf/submit/forms.py | 45 ++++++++++++++++++++++++++++++++++++++++++++
ietf/submit/views.py | 1 +
2 files changed, 46 insertions(+)
diff --git a/ietf/submit/forms.py b/ietf/submit/forms.py
index ff3c03dbcd..34f568d2ed 100644
--- a/ietf/submit/forms.py
+++ b/ietf/submit/forms.py
@@ -29,6 +29,7 @@
from ietf.group.models import Group
from ietf.ietfauth.utils import has_role
from ietf.doc.fields import SearchableDocAliasesField
+from ietf.doc.models import DocAlias
from ietf.ipr.mail import utc_from_string
from ietf.meeting.models import Meeting
from ietf.message.models import Message
@@ -629,12 +630,56 @@ def __init__(self, request, *args, **kwargs):
class SubmissionAutoUploadForm(SubmissionBaseUploadForm):
user = forms.EmailField(required=True)
+ replaces = forms.CharField(required=False, max_length=1000, strip=True)
def __init__(self, request, *args, **kwargs):
super().__init__(request, *args, **kwargs)
self.formats = ['xml', ]
self.base_formats = ['xml', ]
+ def clean(self):
+ super().clean()
+
+ # Clean the replaces field after the rest of the cleaning so we know the name of the
+ # uploaded draft via self.filename
+ if self.cleaned_data['replaces']:
+ names_replaced = [s.strip() for s in self.cleaned_data['replaces'].split(',')]
+ self.cleaned_data['replaces'] = ','.join(names_replaced)
+ aliases_replaced = DocAlias.objects.filter(name__in=names_replaced)
+ if len(names_replaced) != len(aliases_replaced):
+ known_names = aliases_replaced.values_list('name', flat=True)
+ unknown_names = [n for n in names_replaced if n not in known_names]
+ self.add_error(
+ 'replaces',
+ forms.ValidationError(
+ 'Unknown draft name(s): ' + ', '.join(unknown_names)
+ ),
+ )
+ for alias in aliases_replaced:
+ if alias.document.name == self.filename:
+ self.add_error(
+ 'replaces',
+ forms.ValidationError("A draft cannot replace itself"),
+ )
+ elif alias.document.type_id != "draft":
+ self.add_error(
+ 'replaces',
+ forms.ValidationError("A draft can only replace another draft"),
+ )
+ elif alias.document.get_state_slug() == "rfc":
+ self.add_error(
+ 'replaces',
+ forms.ValidationError("A draft cannot replace an RFC"),
+ )
+ elif alias.document.get_state_slug('draft-iesg') in ('approved', 'ann', 'rfcqueue'):
+ self.add_error(
+ 'replaces',
+ forms.ValidationError(
+ alias.name + " is approved by the IESG and cannot be replaced"
+ ),
+ )
+
+
class NameEmailForm(forms.Form):
name = forms.CharField(required=True)
email = forms.EmailField(label='Email address', required=True)
diff --git a/ietf/submit/views.py b/ietf/submit/views.py
index a44ae03cda..bab8597021 100644
--- a/ietf/submit/views.py
+++ b/ietf/submit/views.py
@@ -163,6 +163,7 @@ def err(code, error, messages=None):
submission.file_types = ','.join(form.file_types)
submission.submission_date = datetime.date.today()
submission.submitter = user.person.formatted_email()
+ submission.replaces = form.cleaned_data['replaces']
submission.save()
clear_existing_files(form)
save_files(form)
From de46d1e24e52a34c8d2be79e07deae7356266ea1 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 30 Jun 2022 18:37:54 -0300
Subject: [PATCH 68/99] docs: update api_submission documentation
---
ietf/templates/submit/api_submission_info.html | 11 +++++++----
1 file changed, 7 insertions(+), 4 deletions(-)
diff --git a/ietf/templates/submit/api_submission_info.html b/ietf/templates/submit/api_submission_info.html
index ac6ee26321..da2b21d523 100644
--- a/ietf/templates/submit/api_submission_info.html
+++ b/ietf/templates/submit/api_submission_info.html
@@ -27,14 +27,17 @@ Draft submission API instructions
- It takes two parameters:
+ It takes the following parameters:
-
-
user which is the user login
+ user which is the user login (required)
-
-
xml, which is the submitted file
+ xml, which is the submitted file (required)
+
+ -
+
replaces, a comma-separated list of draft names replaced by this submission (optional)
@@ -82,7 +85,7 @@ Draft submission API instructions
Here is an example of submitting a draft and polling its status through the API:
-$ curl -s -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" {% absurl 'ietf.submit.views.api_submission' %} | jq
+$ curl -s -F "user=user.name@example.com" -F "xml=@~/draft-user-example.xml" -F "replaces=draft-user-replaced-draft" {% absurl 'ietf.submit.views.api_submission' %} | jq
{
"id": "126375",
"name": "draft-user-example",
From 783fa119a0df466de69c0275ff9d226141cebfc7 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 30 Jun 2022 18:57:12 -0300
Subject: [PATCH 69/99] fix: remove unused import
---
ietf/utils/xmldraft.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/ietf/utils/xmldraft.py b/ietf/utils/xmldraft.py
index 3f3111c3dc..15bf745cc5 100644
--- a/ietf/utils/xmldraft.py
+++ b/ietf/utils/xmldraft.py
@@ -1,7 +1,6 @@
# Copyright The IETF Trust 2022, All Rights Reserved
# -*- coding: utf-8 -*-
import io
-import os
import re
import xml2rfc
From b682d7148593949d2c48abddea47d0b9f0b7ae2b Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 30 Jun 2022 19:45:08 -0300
Subject: [PATCH 70/99] test: test "replaces" validation for submission API
---
ietf/submit/tests.py | 110 ++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 108 insertions(+), 2 deletions(-)
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index f3cc5f97b8..bf6d1ae17d 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -16,6 +16,7 @@
from pathlib import Path
from django.conf import settings
+from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import transaction
from django.forms import ValidationError
from django.test import override_settings
@@ -28,7 +29,8 @@
from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames,
post_submission, validate_submission_name, validate_submission_rev,
process_uploaded_submission, SubmissionError, process_submission_text)
-from ietf.doc.factories import DocumentFactory, WgDraftFactory, IndividualDraftFactory, IndividualRfcFactory
+from ietf.doc.factories import (DocumentFactory, WgDraftFactory, IndividualDraftFactory, IndividualRfcFactory,
+ ReviewFactory, WgRfcFactory)
from ietf.doc.models import ( Document, DocAlias, DocEvent, State,
BallotPositionDocEvent, DocumentAuthor, SubmissionDocEvent )
from ietf.doc.utils import create_ballot_if_not_open, can_edit_docextresources, update_action_holders
@@ -42,7 +44,7 @@
from ietf.person.models import Person
from ietf.person.factories import UserFactory, PersonFactory, EmailFactory
from ietf.submit.factories import SubmissionFactory, SubmissionExtResourceFactory
-from ietf.submit.forms import SubmissionBaseUploadForm
+from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
from ietf.submit.mail import add_submission_email, process_response_email
from ietf.submit.tasks import process_uploaded_submission_task
@@ -2972,6 +2974,110 @@ def test_check_submission_thresholds(self):
max_size=1, # megabytes
)
+ def test_replaces_field(self):
+ """test SubmissionAutoUploadForm replaces field"""
+ request_factory = RequestFactory()
+ WgDraftFactory(name='draft-somebody-test')
+ existing_drafts = WgDraftFactory.create_batch(2)
+ xml, auth = submission_file('draft-somebody-test-01', 'draft-somebody-test-01.xml', None, 'test_submission.xml')
+ files_dict = {
+ 'xml': SimpleUploadedFile('draft-somebody-test-01.xml', xml.read().encode('utf8'),
+ content_type='application/xml'),
+ }
+
+ # no replaces
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': ''},
+ files=files_dict,
+ )
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data['replaces'], '')
+
+ # whitespace
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': ' '},
+ files=files_dict,
+ )
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data['replaces'], '')
+
+ # one replaces
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': existing_drafts[0].name},
+ files=files_dict,
+ )
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data['replaces'], existing_drafts[0].name)
+
+ # two replaces
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': f'{existing_drafts[0].name},{existing_drafts[1].name}'},
+ files=files_dict,
+ )
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data['replaces'], f'{existing_drafts[0].name},{existing_drafts[1].name}')
+
+ # two replaces, extra whitespace
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': f' {existing_drafts[0].name} , {existing_drafts[1].name}'},
+ files=files_dict,
+ )
+ self.assertTrue(form.is_valid())
+ self.assertEqual(form.cleaned_data['replaces'], f'{existing_drafts[0].name},{existing_drafts[1].name}')
+
+ # can't replace self
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': 'draft-somebody-test'},
+ files=files_dict,
+ )
+ self.assertFalse(form.is_valid())
+ self.assertIn('A draft cannot replace itself', form.errors['replaces'])
+
+ # can't replace non-draft
+ review = ReviewFactory()
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': review.name},
+ files=files_dict,
+ )
+ self.assertFalse(form.is_valid())
+ self.assertIn('A draft can only replace another draft', form.errors['replaces'])
+
+ # can't replace RFC
+ rfc = WgRfcFactory()
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': rfc.name},
+ files=files_dict,
+ )
+ self.assertFalse(form.is_valid())
+ self.assertIn('A draft cannot replace an RFC', form.errors['replaces'])
+
+ # can't replace draft approved by iesg
+ existing_drafts[0].set_state(State.objects.get(type='draft-iesg', slug='approved'))
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': existing_drafts[0].name},
+ files=files_dict,
+ )
+ self.assertFalse(form.is_valid())
+ self.assertIn(f'{existing_drafts[0].name} is approved by the IESG and cannot be replaced',
+ form.errors['replaces'])
+
+ # unknown draft
+ form = SubmissionAutoUploadForm(
+ request_factory.get('/some/url'),
+ data={'user': auth.user.username, 'replaces': 'fake-name'},
+ files=files_dict,
+ )
+ self.assertFalse(form.is_valid())
+
class AsyncSubmissionTests(BaseSubmitTestCase):
"""Tests of async submission-related tasks"""
From 60a6ef6250f8b370a06de920b353fc9f35015c8c Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 30 Jun 2022 19:58:19 -0300
Subject: [PATCH 71/99] test: test that "replaces" is set by api_submission
---
ietf/submit/tests.py | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index bf6d1ae17d..df6bd04594 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -2771,6 +2771,24 @@ def test_upload_draft(self):
self.assertEqual(submission.state_id, 'validating')
self.assertIn('Uploaded submission through API', submission.submissionevent_set.last().desc)
+ def test_upload_draft_with_replaces(self):
+ """api_submission accepts a submission and queues it for processing"""
+ existing_draft = WgDraftFactory()
+ url = urlreverse('ietf.submit.views.api_submission')
+ xml, author = submission_file('draft-somebody-test-00', 'draft-somebody-test-00.xml', None, 'test_submission.xml')
+ data = {
+ 'xml': xml,
+ 'user': author.user.username,
+ 'replaces': existing_draft.name,
+ }
+ # mock out the task so we don't call to celery during testing!
+ with mock.patch('ietf.submit.views.process_uploaded_submission_task'):
+ r = self.client.post(url, data)
+ self.assertEqual(r.status_code, 200)
+ submission = Submission.objects.last()
+ self.assertEqual(submission.name, 'draft-somebody-test')
+ self.assertEqual(submission.replaces, existing_draft.name)
+
def test_rejects_broken_upload(self):
"""api_submission immediately rejects a submission with serious problems"""
orig_submission_count = Submission.objects.count()
From 621941217be52c22ab6affbbf56900a464837c24 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 5 Jul 2022 17:22:44 -0300
Subject: [PATCH 72/99] feat: trap TERM to gracefully stop celery container
---
dev/celery/docker-init.sh | 16 +++++++++++++++-
1 file changed, 15 insertions(+), 1 deletion(-)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index 4245cff814..6c613e9358 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -9,4 +9,18 @@ if [[ -n "${UPDATE_REQUIREMENTS}" && -r requirements.txt ]]; then
pip install --upgrade -r requirements.txt
fi
-celery --app="${CELERY_APP:-ietf}" worker "$@"
+celery_pid=0
+cleanup () {
+ # Cleanly terminate the celery app by sending it a TERM, then waiting for it to exit.
+ if [[ "${celery_pid}" != 0 ]]; then
+ echo "Gracefully terminating celery worker. This may take a few minutes if tasks are in progress..."
+ kill -TERM "${celery_pid}"
+ wait "${celery_pid}"
+ fi
+}
+
+trap cleanup TERM
+# start celery in the background so we can trap the TERM signal
+celery --app="${CELERY_APP:-ietf}" worker "$@" &
+celery_pid=$!
+wait "${celery_pid}"
From a31767b44b2c526cae15bdad18e4f2ebdbf353a6 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 7 Jul 2022 11:19:08 -0300
Subject: [PATCH 73/99] chore: tweak celery/mq settings
---
dev/celery/docker-init.sh | 5 ++---
docker-compose.yml | 4 ++++
ietf/settings.py | 2 --
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index 6c613e9358..c5d845e6b5 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -9,17 +9,16 @@ if [[ -n "${UPDATE_REQUIREMENTS}" && -r requirements.txt ]]; then
pip install --upgrade -r requirements.txt
fi
-celery_pid=0
cleanup () {
# Cleanly terminate the celery app by sending it a TERM, then waiting for it to exit.
- if [[ "${celery_pid}" != 0 ]]; then
+ if [[ -n "${celery_pid}" ]]; then
echo "Gracefully terminating celery worker. This may take a few minutes if tasks are in progress..."
kill -TERM "${celery_pid}"
wait "${celery_pid}"
fi
}
-trap cleanup TERM
+trap 'trap "" TERM; cleanup' TERM
# start celery in the background so we can trap the TERM signal
celery --app="${CELERY_APP:-ietf}" worker "$@" &
celery_pid=$!
diff --git a/docker-compose.yml b/docker-compose.yml
index 848bbf2072..cce7b9969f 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,6 +16,7 @@ services:
depends_on:
- db
+ - mq
ipc: host
@@ -68,7 +69,10 @@ services:
CELERY_APP: ietf
command:
- '--loglevel=INFO'
+ depends_on:
+ - db
restart: unless-stopped
+ stop_grace_period: 1m
volumes:
mariadb-data:
diff --git a/ietf/settings.py b/ietf/settings.py
index 81800e0a5d..9338b81728 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -1169,8 +1169,6 @@ def skip_unreadable_post(record):
# Celery configuration
CELERY_TIMEZONE = 'UTC'
CELERY_BROKER_URL = 'amqp://mq/'
-CELERY_ACKS_LATE = True # tasks aborted due to worker failure will retry; keep tasks idempotent or disable per-task
-# (CELERY_ACKS_LATE does not retry a task that fails, including due to a clean worker shutdown)
# Meetecho API setup: Uncomment this and provide real credentials to enable
# Meetecho conference creation for interim session requests
From d2e3acd615d37c74c99a219ed6e86355d2ef5437 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 7 Jul 2022 12:51:34 -0300
Subject: [PATCH 74/99] docs: update installation instructions
---
dev/INSTALL | 41 +++++++++++++++++++++++++++++++++--------
1 file changed, 33 insertions(+), 8 deletions(-)
diff --git a/dev/INSTALL b/dev/INSTALL
index 132e607f52..6da6cb53de 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -36,31 +36,56 @@ General Instructions for Deployment of a New Release
6. Run system checks (which patches the just installed modules)::
- ietf/manage.py check
+ ietf/manage.py check
- 7. Run migrations:
+ 7. Switch to the docker directory and update async task docker image:
+
+ cd /a/docker/datatracker-cel
+ docker image tag ghcr.io/ietf-tools/datatracker-celery:latest datatracker-celery-fallback
+ docker-compose pull celery
+
+ 8. Stop async task worker (this may take a few minutes if tasks are in progress):
+
+ docker-compose stop celery
+ docker-compose rm celery
+ cd -
+
+ 9. Run migrations:
ietf/manage.py migrate
Take note if any migrations were executed.
- 8. Back out one directory level, then re-point the 'web' symlink::
+ 10. Back out one directory level, then re-point the 'web' symlink::
cd ..
rm ./web; ln -s ${releasenumber} web
- 9. Reload the datatracker service (it is no longer necessary to restart apache) ::
+ 11. Reload the datatracker service (it is no longer necessary to restart apache) ::
exit # or CTRL-D, back to root level shell
systemctl restart datatracker
- 10. Verify operation:
+ 12. Start async task worker:
+
+ cd /a/docker/datatracker-cel && bash startcommand && cd -
+
+ 13. Verify operation:
http://datatracker.ietf.org/
- 11. If install failed and there were no migrations at step 7, revert web symlink and repeat the restart in step 9.
- If there were migrations at step 7, they will need to be reversed before the restart at step 9. If it's not obvious
- what to do to reverse the migrations, contact the dev team.
+ 14. If install failed and there were no migrations at step 9, revert web symlink and docker update and repeat the
+ restart in steps 11 and 12. To revert the docker update:
+
+ cd /a/docker/datatracker-cel
+ docker-compose stop celery
+ docker-compose rm celery
+ docker image rm ghcr.io/ietf-tools/datatracker-celery:latest
+ docker image tag datatracker-celery-fallback ghcr.io/ietf-tools/datatracker-celery:latest
+ cd -
+
+ If there were migrations at step 7, they will need to be reversed before the restart at step 11.
+ If it's not obvious what to do to reverse the migrations, contact the dev team.
Patching a Production Release
From df10bc07629c33ce92082553358ca6f1149a8b8b Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 7 Jul 2022 13:03:11 -0300
Subject: [PATCH 75/99] ci: adjust paths that trigger celery worker image
build
---
.github/workflows/build-celery-worker.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index 24e83f323a..2ce078f218 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -8,7 +8,7 @@ on:
- 'jennifer/submit-async'
paths:
- 'requirements.txt'
- - 'dev/celery/Dockerfile'
+ - 'dev/celery/**'
workflow_dispatch:
From 7a7a575f623b2d307028f8176f9164c6c72ec5f2 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 13 Jul 2022 14:04:38 -0300
Subject: [PATCH 76/99] ci: fix branches/repo names left over from dev
---
.github/workflows/build-celery-worker.yml | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index 2ce078f218..86ed8ec87d 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -4,8 +4,6 @@ on:
push:
branches:
- 'main'
- - 'jennifer/celery'
- - 'jennifer/submit-async'
paths:
- 'requirements.txt'
- 'dev/celery/**'
@@ -42,5 +40,5 @@ jobs:
file: dev/celery/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
- tags: ghcr.io/painless-security/datatracker-celery:latest
+ tags: ghcr.io/ietf-tools/datatracker-celery:latest
From e99f2c321c3f2d2b52490726f364548371e377e0 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 13 Jul 2022 15:46:37 -0300
Subject: [PATCH 77/99] ci: run manage.py check when initializing celery
container
Driver here is applying the patches. Starting the celery workers
also invokes the check task, but this should cause a clearer failure
if something fails.
---
dev/celery/docker-init.sh | 3 +++
1 file changed, 3 insertions(+)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index c5d845e6b5..3728c9b633 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -9,6 +9,9 @@ if [[ -n "${UPDATE_REQUIREMENTS}" && -r requirements.txt ]]; then
pip install --upgrade -r requirements.txt
fi
+echo "Running initial checks..."
+/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check --settings=settings_local
+
cleanup () {
# Cleanly terminate the celery app by sending it a TERM, then waiting for it to exit.
if [[ -n "${celery_pid}" ]]; then
From dbdf092f7d043f94339bcee58d6e142bd53d2cc6 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 13 Jul 2022 18:39:27 -0300
Subject: [PATCH 78/99] docs: revise INSTALL instructions
---
dev/INSTALL | 20 +++++++++++++++-----
1 file changed, 15 insertions(+), 5 deletions(-)
diff --git a/dev/INSTALL b/dev/INSTALL
index 6da6cb53de..b90c23e9ff 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -44,14 +44,14 @@ General Instructions for Deployment of a New Release
docker image tag ghcr.io/ietf-tools/datatracker-celery:latest datatracker-celery-fallback
docker-compose pull celery
- 8. Stop async task worker (this may take a few minutes if tasks are in progress):
+ 8. Stop and remove the async task container (this may take a few minutes if tasks are in progress):
docker-compose stop celery
docker-compose rm celery
- cd -
- 9. Run migrations:
+ 9. Return to the release directory and run migrations:
+ cd /a/www/ietf-datatracker/${releasenumber}
ietf/manage.py migrate
Take note if any migrations were executed.
@@ -68,7 +68,8 @@ General Instructions for Deployment of a New Release
12. Start async task worker:
- cd /a/docker/datatracker-cel && bash startcommand && cd -
+ cd /a/docker/datatracker-cel
+ bash startcommand
13. Verify operation:
@@ -120,8 +121,17 @@ The following process should be used:
6. Edit ``.../ietf/__init__.py`` in the new patched release to indicate the patch
version in the ``__patch__`` string.
- 7. Change the 'web' symlink, reload etc. as described in
+ 7. Stop the async task container (this may take a few minutes if tasks are in progress):
+
+ cd /a/docker/datatracker-cel
+ docker-compose stop celery
+
+ 8. Change the 'web' symlink, reload etc. as described in
`General Instructions for Deployment of a New Release`_.
+ 9. Start async task worker:
+
+ cd /a/docker/datatracker-cel
+ bash startcommand
From a048fff4099b1347c7c449803bd7976df0028134 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 14 Jul 2022 18:04:14 -0300
Subject: [PATCH 79/99] ci: pass filename to pip update in celery container
---
dev/celery/docker-init.sh | 16 ++++++++++++----
docker-compose.yml | 1 +
2 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index 3728c9b633..552e179cea 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -1,12 +1,20 @@
#!/bin/bash
-
+#
+# Environment parameters:
+#
+# CELERY_APP - name of application to pass to celery (defaults to ietf)
+#
+# UPDATES_REQUIREMENTS_FROM - path, relative to /workspace mount, to a pip requirements
+# file that should be installed at container startup. Default is no package install/update.
+#
WORKSPACEDIR="/workspace"
cd "$WORKSPACEDIR" || exit 255
-if [[ -n "${UPDATE_REQUIREMENTS}" && -r requirements.txt ]]; then
- echo "Updating requirements..."
- pip install --upgrade -r requirements.txt
+if [[ -n "${UPDATE_REQUIREMENTS_FROM}" ]]; then
+ reqs_file="${WORKSPACEDIR}/${UPDATE_REQUIREMENTS_FROM}"
+ echo "Updating requirements from ${reqs_file}..."
+ pip install --upgrade -r "${reqs_file}"
fi
echo "Running initial checks..."
diff --git a/docker-compose.yml b/docker-compose.yml
index cce7b9969f..e73d72301d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -67,6 +67,7 @@ services:
image: ghcr.io/ietf-tools/datatracker-celery:latest
environment:
CELERY_APP: ietf
+ UPDATE_REQUIREMENTS_FROM: requirements.txt
command:
- '--loglevel=INFO'
depends_on:
From 22e82801421e7adf14801277fced3fa5b54301dc Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 14 Jul 2022 18:05:09 -0300
Subject: [PATCH 80/99] docs: update INSTALL to include freezing pip versions
Will be used to coordinate package versions with the celery
container in production.
---
dev/INSTALL | 1 +
1 file changed, 1 insertion(+)
diff --git a/dev/INSTALL b/dev/INSTALL
index b90c23e9ff..1c90f079bf 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -29,6 +29,7 @@ General Instructions for Deployment of a New Release
python3.9 -mvenv env
source env/bin/activate
pip install -r requirements.txt
+ pip freeze > frozen-requirements.txt
5. Move static files into place for CDN (/a/www/www6s/lib/dt):
From 18f234132929bb808d18a98f96cd0c2b6a503a5c Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Tue, 2 Aug 2022 18:30:34 -0300
Subject: [PATCH 81/99] docs: add explanation of frozen-requirements.txt
---
dev/INSTALL | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/dev/INSTALL b/dev/INSTALL
index 1c90f079bf..ed660125ed 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -31,6 +31,10 @@ General Instructions for Deployment of a New Release
pip install -r requirements.txt
pip freeze > frozen-requirements.txt
+ (The pip freeze command records the exact versions of the Python libraries that pip installed.
+ This is used by the celery docker container to ensure it uses the same library versions as
+ the datatracker service.)
+
5. Move static files into place for CDN (/a/www/www6s/lib/dt):
ietf/manage.py collectstatic
From a48f582858c9258a568fb335ce9ff2056330f8df Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 3 Aug 2022 14:38:22 -0300
Subject: [PATCH 82/99] ci: build image for sandbox deployment
---
.github/workflows/build-celery-worker.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index 86ed8ec87d..9fa5245fa1 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- 'main'
+ - 'jennifer/submit-async'
paths:
- 'requirements.txt'
- 'dev/celery/**'
@@ -40,5 +41,6 @@ jobs:
file: dev/celery/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
- tags: ghcr.io/ietf-tools/datatracker-celery:latest
+# tags: ghcr.io/ietf-tools/datatracker-celery:latest
+ tags: ghcr.io/painless-security/datatracker-celery:latest
From 2d43316c04897dde92d8c548a69a63e8c97878a6 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 3 Aug 2022 14:41:00 -0300
Subject: [PATCH 83/99] ci: add additional build trigger path
---
.github/workflows/build-celery-worker.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/build-celery-worker.yml b/.github/workflows/build-celery-worker.yml
index 9fa5245fa1..93f8c2a43d 100644
--- a/.github/workflows/build-celery-worker.yml
+++ b/.github/workflows/build-celery-worker.yml
@@ -8,6 +8,7 @@ on:
paths:
- 'requirements.txt'
- 'dev/celery/**'
+ - '.github/workflows/build-celery-worker.yml'
workflow_dispatch:
From 40f01a3d83db4893e0d0d9c1cfcb8e9797747215 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Wed, 3 Aug 2022 15:16:17 -0300
Subject: [PATCH 84/99] docs: tweak INSTALL
---
dev/INSTALL | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/dev/INSTALL b/dev/INSTALL
index ed660125ed..f376e54c41 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -49,7 +49,9 @@ General Instructions for Deployment of a New Release
docker image tag ghcr.io/ietf-tools/datatracker-celery:latest datatracker-celery-fallback
docker-compose pull celery
- 8. Stop and remove the async task container (this may take a few minutes if tasks are in progress):
+ 8. Stop and remove the async task container:
+ (Wait for these to finish cleanly - it may take up to about 10 minutes for the 'stop' command to
+ complete if a long-running task is in progress.)
docker-compose stop celery
docker-compose rm celery
From bec798c26f3cdb89d6887f60afcd4045ff2a099c Mon Sep 17 00:00:00 2001
From: Robert Sparks
Date: Wed, 3 Aug 2022 13:22:12 -0500
Subject: [PATCH 85/99] fix: change INSTALL process to stop datatracker before
running migrations
---
dev/INSTALL | 21 ++++++++++++++-------
1 file changed, 14 insertions(+), 7 deletions(-)
diff --git a/dev/INSTALL b/dev/INSTALL
index f376e54c41..86a9502326 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -56,33 +56,40 @@ General Instructions for Deployment of a New Release
docker-compose stop celery
docker-compose rm celery
- 9. Return to the release directory and run migrations:
+ 9. Stop the datatracker
+ (consider doing this with a second shell at ietfa to avoid the exit and shift back to wwwrun)
+
+ exit
+ sudo systemctl stop datatracker.socket datatracker.service
+ sudo su - -s /bin/bash wwwrun
+
+ 10. Return to the release directory and run migrations:
cd /a/www/ietf-datatracker/${releasenumber}
ietf/manage.py migrate
Take note if any migrations were executed.
- 10. Back out one directory level, then re-point the 'web' symlink::
+ 11. Back out one directory level, then re-point the 'web' symlink::
cd ..
rm ./web; ln -s ${releasenumber} web
- 11. Reload the datatracker service (it is no longer necessary to restart apache) ::
+ 12. Start the datatracker service (it is no longer necessary to restart apache) ::
exit # or CTRL-D, back to root level shell
- systemctl restart datatracker
+ sudo systemctl start datatracker.service datatracker.socket
- 12. Start async task worker:
+ 13. Start async task worker:
cd /a/docker/datatracker-cel
bash startcommand
- 13. Verify operation:
+ 14. Verify operation:
http://datatracker.ietf.org/
- 14. If install failed and there were no migrations at step 9, revert web symlink and docker update and repeat the
+ 15. If install failed and there were no migrations at step 9, revert web symlink and docker update and repeat the
restart in steps 11 and 12. To revert the docker update:
cd /a/docker/datatracker-cel
From ce947852500100409f231508c1d0233244de0795 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 4 Aug 2022 13:06:01 -0300
Subject: [PATCH 86/99] chore: use ietf.settings for manage.py check in celery
container
---
dev/celery/docker-init.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index 552e179cea..63188a8eb9 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -18,7 +18,7 @@ if [[ -n "${UPDATE_REQUIREMENTS_FROM}" ]]; then
fi
echo "Running initial checks..."
-/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check --settings=settings_local
+/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check
cleanup () {
# Cleanly terminate the celery app by sending it a TERM, then waiting for it to exit.
From e74cbc5ab651b05c69f3173dfc69a8976f9021d4 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 4 Aug 2022 13:45:24 -0300
Subject: [PATCH 87/99] chore: set uid/gid for celery worker
---
dev/celery/docker-init.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index 63188a8eb9..889b075d9b 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -31,6 +31,6 @@ cleanup () {
trap 'trap "" TERM; cleanup' TERM
# start celery in the background so we can trap the TERM signal
-celery --app="${CELERY_APP:-ietf}" worker "$@" &
+celery --app="${CELERY_APP:-ietf}" --uid="${CELERY_UID:-0}" --gid="${CELERY_GID:-0}" worker "$@" &
celery_pid=$!
wait "${celery_pid}"
From cdac495ebece9bfc51641343586f08c241c050d3 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 4 Aug 2022 14:28:31 -0300
Subject: [PATCH 88/99] chore: create user/group in celery container if needed
---
dev/INSTALL | 6 ++++--
dev/celery/docker-init.sh | 27 ++++++++++++++++++++++++++-
2 files changed, 30 insertions(+), 3 deletions(-)
diff --git a/dev/INSTALL b/dev/INSTALL
index 86a9502326..bbfc1791a2 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -50,12 +50,14 @@ General Instructions for Deployment of a New Release
docker-compose pull celery
8. Stop and remove the async task container:
- (Wait for these to finish cleanly - it may take up to about 10 minutes for the 'stop' command to
- complete if a long-running task is in progress.)
+ Wait for these to finish cleanly. It may take up to about 10 minutes for the 'stop' command to
+ complete if a long-running task is in progress.
docker-compose stop celery
docker-compose rm celery
+ (Answer 'y' when prompted to remove the container.)
+
9. Stop the datatracker
(consider doing this with a second shell at ietfa to avoid the exit and shift back to wwwrun)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index 889b075d9b..fb959581c2 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -4,6 +4,10 @@
#
# CELERY_APP - name of application to pass to celery (defaults to ietf)
#
+# CELERY_UID - numeric uid for the celery worker process
+#
+# CELERY_GID - numeric gid for the celery worker process
+#
# UPDATES_REQUIREMENTS_FROM - path, relative to /workspace mount, to a pip requirements
# file that should be installed at container startup. Default is no package install/update.
#
@@ -20,6 +24,27 @@ fi
echo "Running initial checks..."
/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check
+if [[ -n "${CELERY_UID}" ]]; then
+ # ensure that some group with the necessary GID exists in container
+ if ! id "${CELERY_UID}" ; then
+ adduser --system --uid "${CELERY_UID}" --no-create-home --disabled-login "celery-user-${CELERY_UID}"
+ fi
+ UID_OPT="--uid=${CELERY_UID}"
+else
+ UID_OPT=
+fi
+
+if [[ -n "${CELERY_GID}" ]]; then
+ # ensure that some group with the necessary GID exists in container
+ if ! getent group "${CELERY_GID}" ; then
+ addgroup --gid "${CELERY_GID}" "celery-group-${CELERY_GID}"
+ fi
+ GID_OPT="--gid=${CELERY_GID}"
+else
+ GID_OPT=
+fi
+
+
cleanup () {
# Cleanly terminate the celery app by sending it a TERM, then waiting for it to exit.
if [[ -n "${celery_pid}" ]]; then
@@ -31,6 +56,6 @@ cleanup () {
trap 'trap "" TERM; cleanup' TERM
# start celery in the background so we can trap the TERM signal
-celery --app="${CELERY_APP:-ietf}" --uid="${CELERY_UID:-0}" --gid="${CELERY_GID:-0}" worker "$@" &
+celery --app="${CELERY_APP:-ietf}" worker "${UID_OPT}" ${GID_OPT} "$@" &
celery_pid=$!
wait "${celery_pid}"
From c56168cd21a108c94bd17e14ff5725330de5ada5 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Thu, 4 Aug 2022 17:03:01 -0300
Subject: [PATCH 89/99] chore: tweak docker compose/init so celery container
works in dev
---
dev/celery/docker-init.sh | 11 ++++-------
docker/docker-compose.extend.yml | 1 +
2 files changed, 5 insertions(+), 7 deletions(-)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index fb959581c2..b7b01feeb3 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -24,14 +24,13 @@ fi
echo "Running initial checks..."
/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check
+CELERY_WORKER_OPTS=()
if [[ -n "${CELERY_UID}" ]]; then
# ensure that some group with the necessary GID exists in container
if ! id "${CELERY_UID}" ; then
adduser --system --uid "${CELERY_UID}" --no-create-home --disabled-login "celery-user-${CELERY_UID}"
fi
- UID_OPT="--uid=${CELERY_UID}"
-else
- UID_OPT=
+ CELERY_WORKER_OPTS+=("--uid=${CELERY_UID}")
fi
if [[ -n "${CELERY_GID}" ]]; then
@@ -39,9 +38,7 @@ if [[ -n "${CELERY_GID}" ]]; then
if ! getent group "${CELERY_GID}" ; then
addgroup --gid "${CELERY_GID}" "celery-group-${CELERY_GID}"
fi
- GID_OPT="--gid=${CELERY_GID}"
-else
- GID_OPT=
+ CELERY_WORKER_OPTS+=("--gid=${CELERY_GID}")
fi
@@ -56,6 +53,6 @@ cleanup () {
trap 'trap "" TERM; cleanup' TERM
# start celery in the background so we can trap the TERM signal
-celery --app="${CELERY_APP:-ietf}" worker "${UID_OPT}" ${GID_OPT} "$@" &
+celery --app="${CELERY_APP:-ietf}" worker "${CELERY_WORKER_OPTS[@]}" "$@" &
celery_pid=$!
wait "${celery_pid}"
diff --git a/docker/docker-compose.extend.yml b/docker/docker-compose.extend.yml
index 2fbb0e1c14..06e47bbb08 100644
--- a/docker/docker-compose.extend.yml
+++ b/docker/docker-compose.extend.yml
@@ -18,3 +18,4 @@ services:
celery:
volumes:
- .:/workspace
+ - app-assets:/assets
From 26980e9973b11883ac3d3824741d7e68a5b4726d Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 5 Aug 2022 12:09:12 -0300
Subject: [PATCH 90/99] ci: build mq docker image
---
.github/workflows/build-mq-broker.yml | 46 +++++++++++++++++++++++++++
dev/mq/Dockerfile | 17 ++++++++++
dev/mq/definitions.json | 30 +++++++++++++++++
dev/mq/ietf-rabbitmq-server.bash | 18 +++++++++++
dev/mq/rabbitmq.conf | 18 +++++++++++
5 files changed, 129 insertions(+)
create mode 100644 .github/workflows/build-mq-broker.yml
create mode 100644 dev/mq/Dockerfile
create mode 100644 dev/mq/definitions.json
create mode 100755 dev/mq/ietf-rabbitmq-server.bash
create mode 100644 dev/mq/rabbitmq.conf
diff --git a/.github/workflows/build-mq-broker.yml b/.github/workflows/build-mq-broker.yml
new file mode 100644
index 0000000000..8b77d6dfef
--- /dev/null
+++ b/.github/workflows/build-mq-broker.yml
@@ -0,0 +1,46 @@
+name: Build MQ Broker Docker Image
+
+on:
+ push:
+ branches:
+ - 'main'
+ - 'jennifer/submit-async'
+ paths:
+ - 'dev/mq/**'
+ - '.github/workflows/build-mq-worker.yml'
+
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v2
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v2
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v2
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Docker Build & Push
+ uses: docker/build-push-action@v3
+ with:
+ context: .
+ file: dev/mq/Dockerfile
+ platforms: linux/amd64,linux/arm64
+ push: true
+# tags: ghcr.io/ietf-tools/datatracker-mq:latest
+ tags: ghcr.io/painless-security/datatracker-mq:latest
+
diff --git a/dev/mq/Dockerfile b/dev/mq/Dockerfile
new file mode 100644
index 0000000000..e8871c30a9
--- /dev/null
+++ b/dev/mq/Dockerfile
@@ -0,0 +1,17 @@
+# Dockerfile for RabbitMQ worker
+#
+FROM rabbitmq:3-alpine
+LABEL maintainer="IETF Tools Team "
+
+# Copy the startup file
+COPY dev/mq/ietf-rabbitmq-server.bash /ietf-rabbitmq-server.bash
+RUN sed -i 's/\r$//' /ietf-rabbitmq-server.bash && \
+ chmod +x /ietf-rabbitmq-server.bash
+
+# Put the rabbitmq.conf in the conf.d so it runs after 10-defaults.conf.
+# Can override this for an individual container by mounting additional
+# config files in /etc/rabbitmq/conf.d.
+COPY dev/mq/rabbitmq.conf /etc/rabbitmq/conf.d/20-ietf-config.conf
+COPY dev/mq/definitions.json /definitions.json
+
+CMD ["/ietf-rabbitmq-server.bash"]
diff --git a/dev/mq/definitions.json b/dev/mq/definitions.json
new file mode 100644
index 0000000000..60e4fdba07
--- /dev/null
+++ b/dev/mq/definitions.json
@@ -0,0 +1,30 @@
+{
+ "permissions": [
+ {
+ "configure": ".*",
+ "read": ".*",
+ "user": "datatracker",
+ "vhost": "dt",
+ "write": ".*"
+ }
+ ],
+ "users": [
+ {
+ "hashing_algorithm": "rabbit_password_hashing_sha256",
+ "limits": {},
+ "name": "datatracker",
+ "password_hash": "",
+ "tags": []
+ }
+ ],
+ "vhosts": [
+ {
+ "limits": [],
+ "metadata": {
+ "description": "",
+ "tags": []
+ },
+ "name": "dt"
+ }
+ ]
+}
diff --git a/dev/mq/ietf-rabbitmq-server.bash b/dev/mq/ietf-rabbitmq-server.bash
new file mode 100755
index 0000000000..145b13e631
--- /dev/null
+++ b/dev/mq/ietf-rabbitmq-server.bash
@@ -0,0 +1,18 @@
+#!/bin/bash -x
+#
+# Environment parameters:
+#
+# CELERY_PASSWORD - password for the datatracker celery user
+#
+export RABBITMQ_PID_FILE=/var/run/rabbitmq.pid
+
+update_celery_password () {
+ rabbitmqctl wait "${RABBITMQ_PID_FILE}" --timeout 300
+ rabbitmqctl await_startup --timeout 300
+ rabbitmqctl change_password datatracker <
Date: Fri, 5 Aug 2022 13:39:57 -0300
Subject: [PATCH 91/99] fix: move rabbitmq.pid to writeable location
---
dev/mq/ietf-rabbitmq-server.bash | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dev/mq/ietf-rabbitmq-server.bash b/dev/mq/ietf-rabbitmq-server.bash
index 145b13e631..212753ddc6 100755
--- a/dev/mq/ietf-rabbitmq-server.bash
+++ b/dev/mq/ietf-rabbitmq-server.bash
@@ -4,7 +4,7 @@
#
# CELERY_PASSWORD - password for the datatracker celery user
#
-export RABBITMQ_PID_FILE=/var/run/rabbitmq.pid
+export RABBITMQ_PID_FILE=/tmp/rabbitmq.pid
update_celery_password () {
rabbitmqctl wait "${RABBITMQ_PID_FILE}" --timeout 300
From e7949d0513df5d7a96a030dc4b8ddafbfe27e728 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 5 Aug 2022 14:01:15 -0300
Subject: [PATCH 92/99] fix: clear password when CELERY_PASSWORD is empty
Setting to an empty password is really not a good plan!
---
dev/INSTALL | 24 +++++++++++-------------
dev/mq/ietf-rabbitmq-server.bash | 6 +++++-
2 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/dev/INSTALL b/dev/INSTALL
index bbfc1791a2..f422af0f22 100644
--- a/dev/INSTALL
+++ b/dev/INSTALL
@@ -43,20 +43,18 @@ General Instructions for Deployment of a New Release
ietf/manage.py check
- 7. Switch to the docker directory and update async task docker image:
+ 7. Switch to the docker directory and update images:
cd /a/docker/datatracker-cel
docker image tag ghcr.io/ietf-tools/datatracker-celery:latest datatracker-celery-fallback
- docker-compose pull celery
+ docker image tag ghcr.io/ietf-tools/datatracker-mq:latest datatracker-mq-fallback
+ docker-compose pull
- 8. Stop and remove the async task container:
- Wait for these to finish cleanly. It may take up to about 10 minutes for the 'stop' command to
+ 8. Stop and remove the async task containers:
+ Wait for this to finish cleanly. It may take up to about 10 minutes for the 'stop' command to
complete if a long-running task is in progress.
- docker-compose stop celery
- docker-compose rm celery
-
- (Answer 'y' when prompted to remove the container.)
+ docker-compose down
9. Stop the datatracker
(consider doing this with a second shell at ietfa to avoid the exit and shift back to wwwrun)
@@ -82,7 +80,7 @@ General Instructions for Deployment of a New Release
exit # or CTRL-D, back to root level shell
sudo systemctl start datatracker.service datatracker.socket
- 13. Start async task worker:
+ 13. Start async task worker and message broker:
cd /a/docker/datatracker-cel
bash startcommand
@@ -95,13 +93,13 @@ General Instructions for Deployment of a New Release
restart in steps 11 and 12. To revert the docker update:
cd /a/docker/datatracker-cel
- docker-compose stop celery
- docker-compose rm celery
- docker image rm ghcr.io/ietf-tools/datatracker-celery:latest
+ docker-compose down
+ docker image rm ghcr.io/ietf-tools/datatracker-celery:latest ghcr.io/ietf-tools/datatracker-mq:latest
docker image tag datatracker-celery-fallback ghcr.io/ietf-tools/datatracker-celery:latest
+ docker image tag datatracker-mq-fallback ghcr.io/ietf-tools/datatracker-mq:latest
cd -
- If there were migrations at step 7, they will need to be reversed before the restart at step 11.
+ If there were migrations at step 10, they will need to be reversed before the restart at step 12.
If it's not obvious what to do to reverse the migrations, contact the dev team.
diff --git a/dev/mq/ietf-rabbitmq-server.bash b/dev/mq/ietf-rabbitmq-server.bash
index 212753ddc6..56effba179 100755
--- a/dev/mq/ietf-rabbitmq-server.bash
+++ b/dev/mq/ietf-rabbitmq-server.bash
@@ -9,9 +9,13 @@ export RABBITMQ_PID_FILE=/tmp/rabbitmq.pid
update_celery_password () {
rabbitmqctl wait "${RABBITMQ_PID_FILE}" --timeout 300
rabbitmqctl await_startup --timeout 300
- rabbitmqctl change_password datatracker <
Date: Thu, 18 Aug 2022 12:24:23 -0300
Subject: [PATCH 93/99] chore: add shutdown debugging option to celery image
---
dev/celery/docker-init.sh | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index b7b01feeb3..cc0b924bac 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -11,6 +11,8 @@
# UPDATES_REQUIREMENTS_FROM - path, relative to /workspace mount, to a pip requirements
# file that should be installed at container startup. Default is no package install/update.
#
+# DEBUG_TERM_TIMING - if non-empty, writes debug messages during shutdown after a TERM signal
+#
WORKSPACEDIR="/workspace"
cd "$WORKSPACEDIR" || exit 255
@@ -41,12 +43,22 @@ if [[ -n "${CELERY_GID}" ]]; then
CELERY_WORKER_OPTS+=("--gid=${CELERY_GID}")
fi
+log_term_timing_msgs () {
+ # output periodic debug message
+ while true; do
+ echo "Waiting for celery worker shutdown ($(date --utc --iso-8601=ns))"
+ sleep 0.5s
+ done
+}
cleanup () {
# Cleanly terminate the celery app by sending it a TERM, then waiting for it to exit.
if [[ -n "${celery_pid}" ]]; then
echo "Gracefully terminating celery worker. This may take a few minutes if tasks are in progress..."
kill -TERM "${celery_pid}"
+ if [[ -n "${DEBUG_TERM_TIMING}" ]]; then
+ log_term_timing_msgs &
+ fi
wait "${celery_pid}"
fi
}
From 352cd2bcd91f0e73c180891d1a0844b07eedb88d Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 19 Aug 2022 14:59:20 -0300
Subject: [PATCH 94/99] chore: add django-celery-beat package
---
ietf/settings.py | 5 +++++
requirements.txt | 1 +
2 files changed, 6 insertions(+)
diff --git a/ietf/settings.py b/ietf/settings.py
index d2a6df831d..c23a34f231 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -437,6 +437,7 @@ def skip_unreadable_post(record):
'analytical',
'django_vite',
'django_bootstrap5',
+ 'django_celery_beat',
'corsheaders',
'django_markup',
'django_password_strength',
@@ -1179,6 +1180,10 @@ def skip_unreadable_post(record):
# Celery configuration
CELERY_TIMEZONE = 'UTC'
CELERY_BROKER_URL = 'amqp://mq/'
+CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'
+CELERY_BEAT_SYNC_EVERY = 1 # update DB after every event
+assert not USE_TZ, 'Drop DJANGO_CELERY_BEAT_TZ_AWARE setting once USE_TZ is True!'
+DJANGO_CELERY_BEAT_TZ_AWARE = False
# Meetecho API setup: Uncomment this and provide real credentials to enable
# Meetecho conference creation for interim session requests
diff --git a/requirements.txt b/requirements.txt
index 25dfa31316..cd0d8a1168 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,6 +12,7 @@ defusedxml>=0.7.1 # for TastyPie when using xml; not a declared dependency
Django>=2.2.28,<3.0
django-analytical>=3.1.0
django-bootstrap5>=21.3
+django-celery-beat>=2.3.0
django-csp>=3.7
django-cors-headers>=3.11.0
django-debug-toolbar>=3.2.4
From dbabe82180937d706c762523667af1eac296c83e Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 19 Aug 2022 16:33:28 -0300
Subject: [PATCH 95/99] chore: run "celery beat" in datatracker-celery image
---
dev/celery/docker-init.sh | 17 +++++++++++------
docker-compose.yml | 19 +++++++++++++++++++
2 files changed, 30 insertions(+), 6 deletions(-)
diff --git a/dev/celery/docker-init.sh b/dev/celery/docker-init.sh
index cc0b924bac..9d00328ad0 100755
--- a/dev/celery/docker-init.sh
+++ b/dev/celery/docker-init.sh
@@ -4,6 +4,8 @@
#
# CELERY_APP - name of application to pass to celery (defaults to ietf)
#
+# CELERY_ROLE - 'worker' or 'beat' (defaults to 'worker')
+#
# CELERY_UID - numeric uid for the celery worker process
#
# CELERY_GID - numeric gid for the celery worker process
@@ -14,6 +16,7 @@
# DEBUG_TERM_TIMING - if non-empty, writes debug messages during shutdown after a TERM signal
#
WORKSPACEDIR="/workspace"
+CELERY_ROLE="${CELERY_ROLE:-worker}"
cd "$WORKSPACEDIR" || exit 255
@@ -23,16 +26,18 @@ if [[ -n "${UPDATE_REQUIREMENTS_FROM}" ]]; then
pip install --upgrade -r "${reqs_file}"
fi
-echo "Running initial checks..."
-/usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check
+if [[ "${CELERY_ROLE}" == "worker" ]]; then
+ echo "Running initial checks..."
+ /usr/local/bin/python $WORKSPACEDIR/ietf/manage.py check
+fi
-CELERY_WORKER_OPTS=()
+CELERY_OPTS=( "${CELERY_ROLE}" )
if [[ -n "${CELERY_UID}" ]]; then
# ensure that some group with the necessary GID exists in container
if ! id "${CELERY_UID}" ; then
adduser --system --uid "${CELERY_UID}" --no-create-home --disabled-login "celery-user-${CELERY_UID}"
fi
- CELERY_WORKER_OPTS+=("--uid=${CELERY_UID}")
+ CELERY_OPTS+=("--uid=${CELERY_UID}")
fi
if [[ -n "${CELERY_GID}" ]]; then
@@ -40,7 +45,7 @@ if [[ -n "${CELERY_GID}" ]]; then
if ! getent group "${CELERY_GID}" ; then
addgroup --gid "${CELERY_GID}" "celery-group-${CELERY_GID}"
fi
- CELERY_WORKER_OPTS+=("--gid=${CELERY_GID}")
+ CELERY_OPTS+=("--gid=${CELERY_GID}")
fi
log_term_timing_msgs () {
@@ -65,6 +70,6 @@ cleanup () {
trap 'trap "" TERM; cleanup' TERM
# start celery in the background so we can trap the TERM signal
-celery --app="${CELERY_APP:-ietf}" worker "${CELERY_WORKER_OPTS[@]}" "$@" &
+celery --app="${CELERY_APP:-ietf}" "${CELERY_OPTS[@]}" "$@" &
celery_pid=$!
wait "${celery_pid}"
diff --git a/docker-compose.yml b/docker-compose.yml
index 71f2821cb9..5dc40f1707 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -67,6 +67,7 @@ services:
image: ghcr.io/ietf-tools/datatracker-celery:latest
environment:
CELERY_APP: ietf
+ CELERY_ROLE: worker
UPDATE_REQUIREMENTS_FROM: requirements.txt
command:
- '--loglevel=INFO'
@@ -74,6 +75,24 @@ services:
- db
restart: unless-stopped
stop_grace_period: 1m
+ volumes:
+ - .:/workspace
+ - app-assets:/assets
+
+ beat:
+ image: datatracker-celery-test
+ environment:
+ CELERY_APP: ietf
+ CELERY_ROLE: beat
+ UPDATE_REQUIREMENTS_FROM: requirements.txt
+ command:
+ - '--loglevel=INFO'
+ depends_on:
+ - db
+ restart: unless-stopped
+ stop_grace_period: 1m
+ volumes:
+ - .:/workspace
volumes:
mariadb-data:
From de863d9b69884ca748751d29b6dbc5eb0d5fdcd4 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 19 Aug 2022 17:40:27 -0300
Subject: [PATCH 96/99] chore: fix docker image name
---
docker-compose.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker-compose.yml b/docker-compose.yml
index 5dc40f1707..983a0de989 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -80,7 +80,7 @@ services:
- app-assets:/assets
beat:
- image: datatracker-celery-test
+ image: ghcr.io/ietf-tools/datatracker-celery:latest
environment:
CELERY_APP: ietf
CELERY_ROLE: beat
From e96ddd2d1cf112949fa4c3950d8c5b793eac3a89 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 19 Aug 2022 18:35:16 -0300
Subject: [PATCH 97/99] feat: add task to cancel stale submissions
---
ietf/settings.py | 2 ++
ietf/submit/tasks.py | 23 ++++++++++++++++++++++-
2 files changed, 24 insertions(+), 1 deletion(-)
diff --git a/ietf/settings.py b/ietf/settings.py
index c23a34f231..12975f061a 100644
--- a/ietf/settings.py
+++ b/ietf/settings.py
@@ -844,6 +844,8 @@ def skip_unreadable_post(record):
# "ietf.submit.checkers.DraftYangvalidatorChecker",
)
+# Max time to allow for validation before a submission is subject to cancellation
+IDSUBMIT_MAX_VALIDATION_TIME = datetime.timedelta(minutes=20)
IDSUBMIT_MANUAL_STAGING_DIR = '/tmp/'
diff --git a/ietf/submit/tasks.py b/ietf/submit/tasks.py
index e2c580cd9f..57eabb1fc8 100644
--- a/ietf/submit/tasks.py
+++ b/ietf/submit/tasks.py
@@ -4,8 +4,12 @@
#
from celery import shared_task
+from django.db.models import Min
+from django.conf import settings
+from django.utils import timezone
+
from ietf.submit.models import Submission
-from ietf.submit.utils import process_uploaded_submission
+from ietf.submit.utils import cancel_submission, create_submission_event, process_uploaded_submission
from ietf.utils import log
@@ -19,6 +23,23 @@ def process_uploaded_submission_task(submission_id):
process_uploaded_submission(submission)
+@shared_task
+def cancel_stale_submissions():
+ now = timezone.now()
+ stale_submissions = Submission.objects.filter(
+ state_id='validating',
+ ).annotate(
+ submitted_at=Min('submissionevent__time'),
+ ).filter(
+ submitted_at__lt=now - settings.IDSUBMIT_MAX_VALIDATION_TIME,
+ )
+ for subm in stale_submissions:
+ age = now - subm.submitted_at
+ log.log(f'Canceling stale submission (id={subm.id}, age={age})')
+ cancel_submission(subm)
+ create_submission_event(None, subm, f'Submission canceled: validation checks took too long')
+
+
@shared_task(bind=True)
def poke(self):
log.log(f'Poked {self.name}, request id {self.request.id}')
From e5f1ab5dc4d3f8f979a31e6aa24011e3fa83ee67 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Fri, 19 Aug 2022 18:50:57 -0300
Subject: [PATCH 98/99] test: test the cancel_stale_submissions task
---
ietf/submit/tests.py | 26 ++++++++++++++++++++++++--
1 file changed, 24 insertions(+), 2 deletions(-)
diff --git a/ietf/submit/tests.py b/ietf/submit/tests.py
index df6bd04594..70baff4328 100644
--- a/ietf/submit/tests.py
+++ b/ietf/submit/tests.py
@@ -22,8 +22,8 @@
from django.test import override_settings
from django.test.client import RequestFactory
from django.urls import reverse as urlreverse
+from django.utils import timezone
from django.utils.encoding import force_str, force_text
-
import debug # pyflakes:ignore
from ietf.submit.utils import (expirable_submissions, expire_submission, find_submission_filenames,
@@ -47,7 +47,7 @@
from ietf.submit.forms import SubmissionBaseUploadForm, SubmissionAutoUploadForm
from ietf.submit.models import Submission, Preapproval, SubmissionExtResource
from ietf.submit.mail import add_submission_email, process_response_email
-from ietf.submit.tasks import process_uploaded_submission_task
+from ietf.submit.tasks import cancel_stale_submissions, process_uploaded_submission_task
from ietf.utils.accesstoken import generate_access_token
from ietf.utils.mail import outbox, empty_outbox, get_payload_text
from ietf.utils.models import VersionInfo
@@ -3360,6 +3360,28 @@ def test_status_of_validating_submission(self):
self.assertContains(r, s.name)
self.assertContains(r, 'still being processed and validated', status_code=200)
+ @override_settings(IDSUBMIT_MAX_VALIDATION_TIME=datetime.timedelta(minutes=30))
+ def test_cancel_stale_submissions(self):
+ fresh_submission = SubmissionFactory(state_id='validating')
+ fresh_submission.submissionevent_set.create(
+ desc='fake created event',
+ time=timezone.now() - datetime.timedelta(minutes=15),
+ )
+ stale_submission = SubmissionFactory(state_id='validating')
+ stale_submission.submissionevent_set.create(
+ desc='fake created event',
+ time=timezone.now() - datetime.timedelta(minutes=30, seconds=1),
+ )
+
+ cancel_stale_submissions()
+
+ fresh_submission = Submission.objects.get(pk=fresh_submission.pk)
+ self.assertEqual(fresh_submission.state_id, 'validating')
+ self.assertEqual(fresh_submission.submissionevent_set.count(), 1)
+
+ stale_submission = Submission.objects.get(pk=stale_submission.pk)
+ self.assertEqual(stale_submission.state_id, 'cancel')
+ self.assertEqual(stale_submission.submissionevent_set.count(), 2)
class ApiSubmitTests(BaseSubmitTestCase):
From f9da62cd36637838085ce277620a8c80ade95358 Mon Sep 17 00:00:00 2001
From: Jennifer Richards
Date: Mon, 22 Aug 2022 12:25:23 -0300
Subject: [PATCH 99/99] chore: make f-string with no interpolation a plain
string
---
ietf/submit/tasks.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ietf/submit/tasks.py b/ietf/submit/tasks.py
index 57eabb1fc8..21d4275b75 100644
--- a/ietf/submit/tasks.py
+++ b/ietf/submit/tasks.py
@@ -37,7 +37,7 @@ def cancel_stale_submissions():
age = now - subm.submitted_at
log.log(f'Canceling stale submission (id={subm.id}, age={age})')
cancel_submission(subm)
- create_submission_event(None, subm, f'Submission canceled: validation checks took too long')
+ create_submission_event(None, subm, 'Submission canceled: validation checks took too long')
@shared_task(bind=True)
|