diff --git a/gracedb/alert.py b/gracedb/alert.py index 8b2cfb6b8dd61d06c7defc96fbfb85f7dd027964..f61d4353aec3a617aa480fb1aa094756c6aec462 100644 --- a/gracedb/alert.py +++ b/gracedb/alert.py @@ -14,6 +14,10 @@ from userprofile.models import Trigger, AnalysisType import glue.ligolw.utils import ligo.lvalert.utils +import logging + +log = logging.getLogger('gracedb.alert') + def issueAlert(event, location, temp_data_loc): issueXMPPAlert(event, location, temp_data_loc) issueEmailAlert(event, location) @@ -116,9 +120,13 @@ def issueXMPPAlert(event, location, temp_data_loc, alert_type="new", description nodename = "%s_%s"% (event.group.name, event.get_analysisType_display()) nodename = nodename.lower() + log.debug('issueXMPPAlert: %s %s' % (event.graceid(), nodename)) + if nodename not in settings.XMPP_ALERT_CHANNELS: + log.debug("issueXMPPAlert: did not send alert") return + log.debug("issueXMPPAlert: attempting to send alert") env = {} env["PYTHONPATH"] = ":".join(sys.path) @@ -146,12 +154,18 @@ def issueXMPPAlert(event, location, temp_data_loc, alert_type="new", description glue.ligolw.utils.write_fileobj(xmldoc, buf) msg = buf.getvalue() + log.debug("issueXMPPAlert: writing message %s" % msg) + p.stdin.write(msg) p.stdin.close() + res = None for i in range(1,10): res = p.poll() if res == None: time.sleep(1) else: + log.debug("issueXMPPAlert: return code %s" % res) break + if res is None: + log.debug("issueXMPPAlert: failed to see child process terminate") diff --git a/gracedb/api.py b/gracedb/api.py index c5ba81c791c4ea43d09caa7e16d462527bdbea34..cb138f868da27b27ddabf01631719d238ba1967e 100644 --- a/gracedb/api.py +++ b/gracedb/api.py @@ -12,12 +12,16 @@ from gracedb.models import Event, Group, EventLog, Slot from gracedb.views import create_label from translator import handle_uploaded_data +from alert import issueAlertForUpdate + import os import urllib import errno import logging import shutil +from utils.vfile import VersionedFile + ################################################################## REST_FRAMEWORK_SETTINGS = getattr(settings, 'REST_FRAMEWORK', {}) @@ -341,7 +345,7 @@ class EventDetail(APIView): # XXX handle duplicate file names. f = request.FILES['eventFile'] uploadDestination = os.path.join(eventDir, "private", f.name) - fdest = open(uploadDestination, 'w') + fdest = VersionedFile(uploadDestination, 'w') # Save uploaded file into user private area. #for chunk in f.chunks(): # fdest.write(chunk) @@ -446,7 +450,11 @@ class EventLabel(APIView): def put(self, request, graceid, label): #return Response("Not Implemented", status=status.HTTP_501_NOT_IMPLEMENTED) - create_label(graceid, label, request.ligouser) + try: + create_label(graceid, label, request.ligouser) + except ValueError, e: + return Response(e.message, + status=status.HTTP_400_BAD_REQUEST) return Response("Created", status=status.HTTP_201_CREATED) @@ -751,100 +759,37 @@ class Files(APIView): except Event.DoesNotExist: return HttpResponseNotFound("Event not found") - # Construct the file path just as Brian does above. - general = False if filename.startswith("general/"): - filename = filename[len("general/"):] - general = True - - filepath = os.path.join(event.datadir(general), filename) - - if not os.path.exists(filepath): - # Awesome. The thing does not exist. This is the first time a file - # by this name is being uploaded. - # Write the file as "filename,0". - linkpath = filepath - filepath += ',0' - filename += ',0' - fdest = open(filepath, 'w') - # Check out line 392 in the client. - # I think the key name for the file is 'upload' - f = request.FILES['upload'] - for chunk in f.chunks(): - fdest.write(chunk) - fdest.close() - - # Make a relative symlink. - os.symlink(filename,linkpath) + # No writing to general/ + return HttpResponseForbidden("cannot write to general directory") - rv = {} - rv['permalink'] = reverse("files", args=[graceid, filename], request=request) - response = Response(rv, status=status.HTTP_201_CREATED) + filepath = os.path.join(event.datadir(), filename) - elif os.path.islink(filepath): - # Great. The thing is a symlink. We can do our version-y stuff now. - - # Read contents of directory. Establish the number of existing versions. - # All we need is the bare filename (i.e., not the full path) - filedir = event.datadir(general) - lastVersion = 0 - for dirname, dirnames, filenames in os.walk(filedir): - for fname in filenames: - if fname.find(',') > 0: - if fname.split(',')[0] == filename: - lastVersion = max(lastVersion,int(fname.split(',')[1])) - - linkpath = filepath # Set the link path to the original file path. - notOpenYet = True - failedAttempts = 0 - while notOpenYet: - # find the new filename - newFilename = filename + ',%d' % (lastVersion+1) - # update the file path according to the new filename. - filepath = os.path.join(filedir,newFilename) - try: - # os.O_EXCL causes the open to fail if the file already exists. - fd = os.open(filepath, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0644) - fdest = os.fdopen(fd,"w") - notOpenYet = False - except OSError as e: - if e.errno==errno.EACCES: - return HttpResponseForbidden("No permission to write to event directory.") - else: - # Note: could also check whether e.errno==errno.EEXIST - ++failedAttempts - if failedAttempts >= MAX_FAILED_OPEN_ATTEMPTS: - return HttpResponseServerError("Cannot open file for writing: %s" % e) - # Under race conditions, increment lastVersion. - if e.errno==errno.EACCES: - ++lastVersion - - # Still with me? Then write the file. + try: + # Open / Write the file. + fdest = VersionedFile(filepath, 'w') f = request.FILES['upload'] for chunk in f.chunks(): fdest.write(chunk) fdest.close() - # Move the symlink, using os.rename to avoid race conditions. - tmplink = os.path.join(filedir,'tmplink') - os.symlink(newFilename,tmplink) - os.rename(tmplink,linkpath) - rv = {} - rv['permalink'] = reverse("files", args=[graceid, newFilename], request=request) + # XXX this seems wobbly. + longname = fdest.name + shortname = longname[longname.rfind(filename):] + rv['permalink'] = reverse( + "files", args=[graceid, shortname], request=request) response = Response(rv, status=status.HTTP_201_CREATED) + except Exception, e: + # XXX This needs some thought. + response = Response(str(e), status=status.HTTP_400_BAD_REQUEST) - elif os.path.isfile(filepath): - # The thing is a file and not a symlink. We will not allow a put request to the file - # resource (for now, anyway). - response = HttpResponseForbidden("%s is a file. Versioning is not supported with legacy data. Please change your filename to avoid clobbering." % filename) - elif not filename: - # Not good. There's nothing we can do without a filename. - response = HttpResponseBadRequest("Must have a filename for upload.") - elif os.path.isdir(filepath): - response = HttpResponseForbidden("%s is a directory" % filename) - else: - response = HttpResponseServerError("Should not happen.") + try: + description = "UPLOAD: {0}".format(filename) + issueAlertForUpdate(event, description, doxmpp=True) + except: + # XXX something should be done here. + pass return response diff --git a/gracedb/serialize/utils.py b/gracedb/serialize.py similarity index 99% rename from gracedb/serialize/utils.py rename to gracedb/serialize.py index 95e14ffd710b116ecace8e055cee9d058c35523c..f6f532128295391bf54394927e8c5575bc17a93b 100644 --- a/gracedb/serialize/utils.py +++ b/gracedb/serialize.py @@ -9,6 +9,8 @@ from glue.ligolw import ligolw from glue.ligolw import table from glue.ligolw import lsctables +from utils.vfile import VersionedFile + ############################################################################## # # useful variables @@ -87,11 +89,11 @@ def write_output_files(root_dir, xmldoc, log_content, \ write the xml-format coinc tables and log file """ - f = open(root_dir+'/'+xml_fname,'w') + f = VersionedFile(root_dir+'/'+xml_fname,'w') xmldoc.write(f) f.close() - f = open(root_dir+'/'+log_fname,'w') + f = VersionedFile(root_dir+'/'+log_fname,'w') f.write(log_content) f.close() diff --git a/gracedb/serialize/__init__.py b/gracedb/serialize/__init__.py deleted file mode 100644 index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0000000000000000000000000000000000000000 diff --git a/gracedb/settings b/gracedb/settings new file mode 120000 index 0000000000000000000000000000000000000000..1aea006e66684dac4f99a8b3134ae5e473aaae3b --- /dev/null +++ b/gracedb/settings @@ -0,0 +1 @@ +/home/bmoe/gracedb/settings \ No newline at end of file diff --git a/gracedb/translator.py b/gracedb/translator.py index 8f998ac05d4b7a97a7bec0df7c9c0893da49d4d4..80a57c1bf937884d65ee4fde451ebd7738e4aa2c 100644 --- a/gracedb/translator.py +++ b/gracedb/translator.py @@ -6,12 +6,13 @@ from models import EventLog import glue import glue.ligolw.utils -from gracedb.serialize.utils import populate_inspiral_tables, \ +from gracedb.serialize import populate_inspiral_tables, \ populate_omega_tables, \ write_output_files from VOEventLib.Vutil import parse, getWhereWhen from utils import isoToGps +from utils.vfile import VersionedFile def handle_uploaded_data(event, datafilename, log_filename='event.log', @@ -370,7 +371,7 @@ class Translator(object): def writeLogfile(self, path): data = self.logData() if data: - f = open(path, 'w') + f = VersionedFile(path, 'w') f.write(data) f.close() return True diff --git a/gracedb/urls_rest.py b/gracedb/urls_rest.py index f0e630872afd2d65894e84f282082ad7f996c84b..d915fe9e4996cdfb41625b04da04a7d4318aa751 100644 --- a/gracedb/urls_rest.py +++ b/gracedb/urls_rest.py @@ -10,7 +10,8 @@ from gracedb.api import Files, FileMeta from gracedb.api import EventNeighbors, EventLabel urlpatterns = patterns('gracedb.api', - url (r'^$', GracedbRoot.as_view(), name="api-root"), + url (r'^/?$', GracedbRoot.as_view(), name="api-root"), + # Event Resources # events/[{graceid}[/{version}]] diff --git a/gracedb/views.py b/gracedb/views.py index 793a2bd994125ab960ec247adbc959fa6fd5eb3f..7568abb00b160cc9caed0de8bd1c0bc33059d2b8 100644 --- a/gracedb/views.py +++ b/gracedb/views.py @@ -17,6 +17,8 @@ from translator import handle_uploaded_data import urllib +from utils.vfile import VersionedFile + import os import re from django.core.mail import mail_admins @@ -222,7 +224,7 @@ def _createEventFromForm(request, form): os.chmod( os.path.join(eventDir,"general"), 041777 ) f = request.FILES['eventFile'] uploadDestination = os.path.join(eventDir, "private", f.name) - fdest = open(uploadDestination, 'w') + fdest = VersionedFile(uploadDestination, 'w') # Save uploaded file into user private area. for chunk in f.chunks(): fdest.write(chunk) @@ -261,7 +263,7 @@ def _createEventFromForm(request, form): def _saveUploadedFile(event, uploadedFile): # XXX Hardcoding. fname = os.path.join(GRACEDB_DATA_DIR, event.graceid(), "private", uploadedFile.name) - f = open(fname, "w") + f = VersionedFile(fname, "w") for chunk in uploadedFile.chunks(): f.write(chunk) f.close() @@ -324,6 +326,7 @@ def upload(request): msg = "ERROR: Event '%s' does not exist" % graceid else: #event issuer comment + # XXX Note: filename or comment oughta have a version log = EventLog(event=event, issuer=request.ligouser, filename=uploadedfile.name, @@ -337,9 +340,8 @@ def upload(request): # XXX # Badnesses: # Same hardcoded path in multiple places. - # What if we're clobbering an existing file? fname = os.path.join(GRACEDB_DATA_DIR, event.graceid(), "private", uploadedfile.name) - f = open(fname, "w") + f = VersionedFile(fname, 'w') for chunk in uploadedfile.chunks(): f.write(chunk) f.close() diff --git a/utils/vfile.py b/utils/vfile.py new file mode 100644 index 0000000000000000000000000000000000000000..b733e9ce1a1074f79984f79d44a4c069f45862cb --- /dev/null +++ b/utils/vfile.py @@ -0,0 +1,165 @@ + +import os +import tempfile +import logging +import errno +import shutil + + +class VersionedFile(file): + """Open a versioned file. + + VersionedFile(name [, mode [, version [, OTHER FILE ARGS]]]) -> file object + """ + def __init__(self, name, *args, **kwargs): + + self.log = logging.getLogger('VersionedFile') + + if ',' in name: + # XXX too strict. + raise IOError("versioned file name cannot contain a ','") + + if len(args): + mode = args[0] + else: + mode = kwargs.get('mode', "") + + if len(args) > 1: + version = args[1] + # Remove from arglist to prep for passing to parent class. + args = args[:2] + else: + version = None + if 'version' in kwargs: + version = kwargs['version'] + # Remove from kwargs to prep for passing to parent class. + del kwargs['version'] + + self.writing = ('w' in mode) or ('a' in mode) or ('+' in mode) + + absname = os.path.abspath(name) + self.absname = absname + fullname = name + self.fullname = fullname + + # If we are merely reading, just open the file as requested. + # Easy Peasy. + + if not self.writing: + actual_name = self._name_for_version(version) + self.log.debug( + "opening file '{0}' with mode '{1}'" + .format(actual_name, mode)) + file.__init__(self, actual_name, *args, **kwargs) + return + + # Otherwise... + + # Specific version requsted? For writing? + # I don't think so. + if version is not None: + # XXX IOError appropriate here? + e = IOError( + "Cannot write to a specific version of a VersionedFile") + e.errno = errno.EINVAL + e.filename = fullname + raise e + + # XXX No appending. Could conceivably copy the latest + # (or specified) version of the file, then open w/append. + if 'a' in mode: + # XXX IOError appropriate here? + e = IOError("Cannot (currently) append to a VersionedFile") + e.errno = errno.EINVAL + e.filename = fullname + raise e + + version = max([-1] + self.known_versions()) + 1 + + if os.path.exists(fullname) and not os.path.islink(fullname): + # It is not versioned. Versionize it. + if version != 0: + raise IOError("VersionedFile symlink inconsistency.") + # XXX risky. race condition. + #os.rename(fullname, self._name_for_version(version)) + shutil.move(fullname, self._name_for_version(version)) + self._repoint_symlink() + version += 1 + + # Open file, which must not exist. + + failedAttempts = 0 + + while failedAttempts < 5: + actual_name = self._name_for_version(version) + self.log.debug( + "opening file '{0}' with mode '{1}'" + .format(actual_name, mode)) + try: + # os.O_EXCL causes the open to fail if the file already exists. + fd = os.open(actual_name, + os.O_WRONLY | os.O_CREAT | os.O_EXCL, + 0644) + # re-open + file.__init__(self, actual_name, *args, **kwargs) + # lose fd we used to ensure file creation. + os.close(fd) + break + except OSError, e: + if e.error != errno.EEXIST: + raise + version += 1 + failedAttempts += 1 + + if failedAttempts >= 5: + raise IOError("Too many attempts to open file") + + def _name_for_version(self, version): + if version is None: + return self.fullname + return "{0},{1}".format(self.fullname, version) + + def _repoint_symlink(self): + # re-point symlink to latest version + last_version = max(self.known_versions()) + # XXX Maybe ought to check that we are removing a symlink. + try: + # XXX Another race condition. File will not exist for a very brief time. + os.unlink(self.fullname) + except: + # Do not care if file does not exist. + pass + os.symlink(self._name_for_version(last_version), self.fullname) + return + +# XXX This fails when renaming/mv-ing across devices. + # XXX assumption: tempfile name will remain unique after closing + tmp = tempfile.NamedTemporaryFile(delete=True) + tmpname = tmp.name + tmp.close() + os.symlink(self._name_for_version(last_version), tmpname) + #os.rename(tmp.name, self.fullname) + shutil.move(tmp.name, self.fullname) + + def known_versions(self): + path = self.absname + d = os.path.dirname(path) or '.' + name = os.path.basename(path) + # XXX what if stuff after ',' is not an int. + return [int(f.split(',')[1]) + for f in os.listdir(d) if f.startswith(name + ',')] + + def close(self): + if self.writing: + # no need to update symlink if we were only reading. + # can cause trouble if we were reading a non-versioned + # file -- trying to discover the lastest version fails + # painfully. (max(known_versions()) => max([])) + self._repoint_symlink() + if not self.closed: + file.close(self) + + def __del__(self): + # XXX file does not have a __del__ method. Should we? + if not self.closed: + self.close()