Exemple #1
0
class FTSJob( object ):
  """ Class describing one FTS job
  """

  # # initial states
  INITSTATES = ( "Submitted", "Ready", "Staging" )
  # # ongoing transfer states
  TRANSSTATES = ( "Active", "Hold" )
  # # failed states
  FAILEDSTATES = ( "Canceled", "Failed" )
  # # finished (careful, must be capitalized)
  FINALSTATES = ( "Finished", "Finisheddirty", "FinishedDirty", "Failed", "Canceled" )


  # # missing source regexp patterns
  missingSourceErrors = [
    re.compile( r".*INVALID_PATH\] Failed" ),
    re.compile( r".*INVALID_PATH\] No such file or directory" ),
    re.compile( r".*INVALID_PATH\] The requested file either does not exist" ),
    re.compile( r".*INVALID_PATH\] the server sent an error response: 500 500"\
               " Command failed. : open error: No such file or directory" ),
    re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[USER_ERROR\] source file doesnt exist" ) ]

  def __init__( self, fromDict = None ):
    """c'tor

    :param self: self reference
    :param dict fromDict: data dict
    """
    self.__data__ = dict.fromkeys( self.tableDesc()["Fields"].keys(), None )

    now = datetime.datetime.utcnow().replace( microsecond = 0 )
    self.__data__["CreationTime"] = now
    self.__data__["SubmitTime"] = now
    self.__data__["LastUpdate"] = now
    self.__data__["Status"] = "Submitted"
    self.__data__["Completeness"] = 0
    self.__data__["FTSJobID"] = 0
    self._regTime = 0.
    self._regSuccess = 0
    self._regTotal = 0
    self.__files__ = TypedList( allowedTypes = FTSFile )

    self._fc = FileCatalog()
    self._fts3context = None

    self._states = tuple( set( self.INITSTATES + self.TRANSSTATES + self.FAILEDSTATES + self.FINALSTATES ) )

    fromDict = fromDict if fromDict else {}
    for ftsFileDict in fromDict.get( "FTSFiles", [] ):
      self +=FTSFile( ftsFileDict )
    if "FTSFiles" in fromDict:
      del fromDict["FTSFiles"]
    for key, value in fromDict.items():
      if key not in self.__data__:
        raise AttributeError( "Unknown FTSJob attribute '%s'" % key )
      if value:
        setattr( self, key, value )
    self._log = gLogger.getSubLogger( "req_%s/FTSJob-%s" % ( self.RequestID, self.FTSGUID ) , True )

  @staticmethod
  def tableDesc():
    """ get table desc """
    return { "Fields" :
             { "FTSJobID" : "INTEGER NOT NULL AUTO_INCREMENT",
               "FTSGUID" :  "VARCHAR(64) NOT NULL",
               "OperationID": "INTEGER NOT NULL",
               "RequestID": "INTEGER NOT NULL",
               "SourceSE" : "VARCHAR(128) NOT NULL",
               "TargetSE" : "VARCHAR(128) NOT NULL",
               "FTSServer" : "VARCHAR(255) NOT NULL",
               "TargetToken": "VARCHAR(255)",
               "SourceToken": "VARCHAR(255)",
               "Size": "BIGINT NOT NULL",
               "Files": "INTEGER NOT NULL",
               "Completeness": "INTEGER NOT NULL DEFAULT 0",
               "FailedFiles": "INTEGER DEFAULT 0",
               "FailedSize": "INTEGER DEFAULT 0",
               "Status" : "ENUM( 'Submitted', 'Ready', 'Staging', 'Canceled', 'Active', 'Hold', "\
                "'Failed', 'Finished', 'FinishedDirty', 'Assigned' ) DEFAULT 'Submitted'",
               "Error" : "VARCHAR(255)",
               "CreationTime" : "DATETIME",
               "SubmitTime" : "DATETIME",
               "LastUpdate" : "DATETIME"  },
             "PrimaryKey" : [ "FTSJobID" ],
             "Indexes" : { "FTSJobID" : [ "FTSJobID" ], "FTSGUID": [ "FTSGUID" ] } }

  @property
  def FTSJobID( self ):
    """ FTSJobID getter """
    return self.__data__["FTSJobID"]

  @FTSJobID.setter
  def FTSJobID( self, value ):
    """ FTSJobID setter """
    self.__data__["FTSJobID"] = long( value ) if value else 0

  @property
  def RequestID( self ):
    """ RequestID getter """
    return self.__data__["RequestID"]

  @RequestID.setter
  def RequestID( self, value ):
    """ RequestID setter """
    self.__data__["RequestID"] = long( value ) if value else 0

  @property
  def OperationID( self ):
    """ OperationID getter """
    return self.__data__["OperationID"]

  @OperationID.setter
  def OperationID( self, value ):
    """ OperationID setter """
    self.__data__["OperationID"] = long( value ) if value else 0

  @property
  def FTSGUID( self ):
    """ FTSGUID prop """
    return self.__data__["FTSGUID"]

  @FTSGUID.setter
  def FTSGUID( self, value ):
    """ FTSGUID setter """
    if value:
      if type( value ) not in ( str, unicode ):
        raise TypeError( "FTSGUID should be a string!" )
      if not checkGuid( value ):
        raise ValueError( "'%s' is not a valid GUID!" % str( value ) )
    self.__data__["FTSGUID"] = value

  @property
  def FTSServer( self ):
    """ FTSServer getter """
    return self.__data__["FTSServer"]

  @FTSServer.setter
  def FTSServer( self, url ):
    """ FTSServer getter """
    self.__data__["FTSServer"] = url

    # I REALLY don't see that happening
    # but in case we change the server after the
    # context was created, I reset it
    # (I don't initialize because maybe we are in FTS2 mode...)
    self._fts3context = None

  @property
  def Completeness( self ):
    """ completeness getter """
    return self.__data__["Completeness"]

  @Completeness.setter
  def Completeness( self, value ):
    """ completeness setter """
    self.__data__["Completeness"] = int( value ) if value else 0

  @property
  def Error( self ):
    """ error getter """
    return self.__data__["Error"]

  @Error.setter
  def Error( self, error ):
    """ error setter """
    self.__data__["Error"] = str( error )[255:]

  @property
  def Files( self ):
    """ nb files getter """
    self.__data__["Files"] = len( self )
    return self.__data__["Files"]

  @Files.setter
  def Files( self, value ):
    """ nb files setter """
    self.__data__["Files"] = len( self )

  @property
  def Status( self ):
    """ status prop """
    if not self.__data__["Status"]:
      self.__data__["Status"] = "Waiting"
    return self.__data__["Status"]

  @Status.setter
  def Status( self, value ):
    """ status setter """
    value = self._normalizedStatus( value.strip() )
    if value not in self._states:
      raise ValueError( "Unknown FTSJob Status: '%s'" % str( value ) )
    self.__data__["Status"] = value

  @property
  def FailedFiles( self ):
    """ nb failed files getter """
    self.__data__["FailedFiles"] = len( [ ftsFile for ftsFile in self
                                         if ftsFile.Status in FTSFile.FAILED_STATES ] )
    return self.__data__["FailedFiles"]

  @FailedFiles.setter
  def FailedFiles( self, value ):
    """ nb failed files setter """
    if value:
      self.__data__["FailedFiles"] = value
    else:
      self.__data__["FailedFiles"] = len( [ftsFile for ftsFile in self if ftsFile.Status in FTSFile.FAILED_STATES] )

  @property
  def Size( self ):
    """ size getter """
    # if not self.__data__["Size"]:
    self.__data__["Size"] = sum( ftsFile.Size for ftsFile in self )
    return self.__data__["Size"]

  @Size.setter
  def Size( self, value ):
    """ size setter """
    if value:
      self.__data__["Size"] = value
    else:
      self.__data__["Size"] = sum( ftsFile.Size for ftsFile in self )

  @property
  def FailedSize( self ):
    """ size getter """
    if not self.__data__["FailedSize"]:
      self.__data__["FailedSize"] = sum( ftsFile.Size for ftsFile in self if ftsFile.Status in FTSFile.FAILED_STATES )
    return self.__data__["FailedSize"]

  @FailedSize.setter
  def FailedSize( self, value ):
    """ size setter """
    if value:
      self.__data__["FailedSize"] = value
    else:
      self.__data__["FailedSize"] = sum( ftsFile.Size for ftsFile in self if ftsFile.Status in FTSFile.FAILED_STATES )

  @property
  def CreationTime( self ):
    """ creation time getter """
    return self.__data__["CreationTime"]

  @CreationTime.setter
  def CreationTime( self, value = None ):
    """ creation time setter """
    if type( value ) not in ( datetime.datetime, str ) :
      raise TypeError( "CreationTime should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["CreationTime"] = value

  @property
  def SubmitTime( self ):
    """ request's submission time getter """
    return self.__data__["SubmitTime"]

  @SubmitTime.setter
  def SubmitTime( self, value = None ):
    """ submission time setter """
    if type( value ) not in ( datetime.datetime, str ):
      raise TypeError( "SubmitTime should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["SubmitTime"] = value

  @property
  def LastUpdate( self ):
    """ last update getter """
    return self.__data__["LastUpdate"]

  @LastUpdate.setter
  def LastUpdate( self, value = None ):
    """ last update setter """
    if type( value ) not in  ( datetime.datetime, str ):
      raise TypeError( "LastUpdate should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["LastUpdate"] = value

  @property
  def TargetSE( self ):
    """ target SE getter """
    return self.__data__["TargetSE"]

  @TargetSE.setter
  def TargetSE( self, targetSE ):
    """ target SE setter """
    self.__data__["TargetSE"] = targetSE

  @property
  def SourceSE( self ):
    """ source SE getter """
    return self.__data__["SourceSE"]

  @SourceSE.setter
  def SourceSE( self, sourceSE ):
    """ source SE setter """
    self.__data__["SourceSE"] = sourceSE

  @property
  def SourceToken( self ):
    """ source token getter """
    return self.__data__["SourceToken"]

  @SourceToken.setter
  def SourceToken( self, sourceToken ):
    """ source SE setter """
    self.__data__["SourceToken"] = sourceToken

  @property
  def TargetToken( self ):
    """ target token getter """
    return self.__data__["TargetToken"]

  @TargetToken.setter
  def TargetToken( self, targetToken ):
    """ target SE setter """
    self.__data__["TargetToken"] = targetToken

  # # FTSJobFiles arithmetics
  def __contains__( self, subFile ):
    """ in operator """
    return subFile in self.__files__

  def __iadd__( self, ftsFile ):
    """ += operator """
    if ftsFile not in self:
      self.__files__.append( ftsFile )
      ftsFile._parent = self
      self.Files
      self.Size
    return self

  def __add__( self, ftsFile ):
    """ + operator """
    self +=ftsFile

  def addFile( self, ftsFile ):
    """ add :ftsFile: to FTS job """
    self +=ftsFile

  def subFile( self, ftsFile ):
    """ remove ftsFile from this job """
    if ftsFile in self:
      ftsFile._parent = None
      self.__files__.remove( ftsFile )

  # # helpers for looping
  def __iter__( self ):
    """ files iterator """
    return self.__files__.__iter__()

  def __getitem__( self, i ):
    """ [] op for files """
    return self.__files__.__getitem__( i )

  def __delitem__( self, i ):
    """ del ftsJob[i] """
    self.__files__.__delitem__( i )

  def __setitem__( self, i, ftsFile ):
    """ ftsJob[i] = ftsFile """
    self.__files__.__setitem__( i, ftsFile )

  def fileStatusList( self ):
    """ get list of files statuses """
    return [ ftsFile.Status for ftsFile in self ]

  def __nonzero__( self ):
    """ for comparisons
    """
    return True

  def __len__( self ):
    """ nb of subFiles """
    return len( self.__files__ )

  def _surlPairs( self ):
    """ create and return SURL pair file """
    surls = []
    for ftsFile in self:
      checksum = "%s:%s" % ( ftsFile.ChecksumType, ftsFile.Checksum ) if ftsFile.ChecksumType and ftsFile.Checksum else ""
      surls.append( "%s %s %s" % ( ftsFile.SourceSURL, ftsFile.TargetSURL, checksum ) )
    return "\n".join( surls )

  def submitFTS2( self, command = 'glite-transfer-submit', pinTime = False ):
    """ submit fts job using FTS2 client """
    if self.FTSGUID:
      return S_ERROR( "FTSJob has already been submitted" )
    surls = self._surlPairs()
    if not surls:
      return S_ERROR( "No files to submit" )
    fd, fileName = tempfile.mkstemp()
    surlFile = os.fdopen( fd, 'w' )
    surlFile.write( surls )
    surlFile.close()
    submitCommand = command.split() + \
                     [ "-s",
                     self.FTSServer,
                     "-f",
                     fileName,
                     "-o",
                     "-K" ]
    if self.TargetToken:
      submitCommand += [ "-t", self.TargetToken]
    if self.SourceToken:
      submitCommand += [ "-S", self.SourceToken ]
    if pinTime:
      submitCommand += [ "--copy-pin-lifetime", "%d" % pinTime, "--bring-online", '86400' ]

    submit = executeGridCommand( "", submitCommand )
    os.remove( fileName )
    if not submit["OK"]:
      return submit
    returnCode, output, errStr = submit["Value"]
    if returnCode != 0:
      return S_ERROR( errStr if errStr else output )
    self.FTSGUID = output.replace( "\n", "" )
    self.Status = "Submitted"
    for ftsFile in self:
      ftsFile.FTSGUID = self.FTSGUID
      ftsFile.Status = "Submitted"
    return S_OK()

  def _normalizedStatus( self, status ):
    for st in self._states:
      if status.lower() == st.lower():
        return st
    return status

  def monitorFTS2( self, command = "glite-transfer-status", full = False ):
    """ monitor fts job """
    if not self.FTSGUID:
      return S_ERROR( "FTSGUID not set, FTS job not submitted?" )

    monitorCommand = command.split() + \
                       ["--verbose",
                       "-s",
                       self.FTSServer,
                       self.FTSGUID ]

    if full:
      monitorCommand.append( "-l" )

    monitor = executeGridCommand( "", monitorCommand )
    if not monitor["OK"]:
      return monitor
    returnCode, outputStr, errStr = monitor["Value"]

    # Returns a non zero status if error
    if returnCode != 0:
      if 'was not found' in outputStr and not errStr:
        errStr = 'Job was not found'
      return S_ERROR( errStr )

    outputStr = outputStr.replace( "'" , "" ).replace( "<", "" ).replace( ">", "" )

    # # set FTS job status
    regExp = re.compile( "Status:\\s+(\\S+)" )

    # with FTS3 this can be uppercase
    self.Status = re.search( regExp, outputStr ).group( 1 )

    statusSummary = {}
    # This is capitalized, even in FTS3!
    for state in FTSFile.ALL_STATES:
      regExp = re.compile( "\\s+%s:\\s+(\\d+)" % state )
      if regExp.search( outputStr ):
        statusSummary[state] = int( re.search( regExp, outputStr ).group( 1 ) )

    total = sum( statusSummary.values() )
    completed = sum( statusSummary.get( state, 0 ) for state in FTSFile.FINAL_STATES )
    self.Completeness = 100 * completed / total if total else 0

    if not full:
      return S_OK( statusSummary )

    # The order of informations is not the same for glite- and fts- !!!
    # In order: new fts-, old fts-, glite-
    realJob = len( self ) != 0
    iExptr = None
    for iExptr, exptr in enumerate( ( 
                   '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Staging:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
                   '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
                   '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Retries:[ ]+(\\d+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)'
                   ) ):
      regExp = re.compile( exptr, re.S )
      fileInfo = re.findall( regExp, outputStr )
      if fileInfo:
        break
    if not fileInfo:
      return S_ERROR( "Error monitoring job (no regexp match)" )
    for info in fileInfo:
      if iExptr == 0:
        # version >= 3.2.30
        sourceURL, targetURL, fileStatus, reason, duration, _retries, _staging = info
      elif iExptr == 1:
        # version FTS3 < 3.2.30
        sourceURL, targetURL, fileStatus, reason, duration, _retries = info
      elif iExptr == 2:
        # version FTS2
        sourceURL, targetURL, fileStatus, _retries, reason, duration = info
      else:
        return S_ERROR( 'Error monitoring job (implement match %d)' % iExptr )
      candidateFile = None

      if not realJob:
        # This is used by the CLI monitoring of jobs in case no file was specified
        candidateFile = FTSFile()
        candidateFile.LFN = overlap( sourceURL, targetURL )
        candidateFile.SourceSURL = sourceURL
        candidateFile.Size = 0
        self +=candidateFile
      else:
        for ftsFile in self:
          if ftsFile.SourceSURL == sourceURL:
            candidateFile = ftsFile
            break
        if not candidateFile:
          continue
      # Can be uppercase for FTS3
      if not candidateFile.TargetSURL:
        candidateFile.TargetSURL = targetURL
      candidateFile.Status = fileStatus
      candidateFile.Error = reason
      candidateFile._duration = duration

      if candidateFile.Status == "Failed":
        for missingSource in self.missingSourceErrors:
          if missingSource.match( reason ):
            candidateFile.Error = "MissingSource"
      # If the staging info was present, record it
      if len( info ) > 6:
        candidateFile._staging = info[6]
    # # register successful files
    if self.Status in FTSJob.FINALSTATES:
      return self.finalize()

    return S_OK()

  def submitFTS3( self, pinTime = False ):
    """ submit fts job using FTS3 rest API """

    if self.FTSGUID:
      return S_ERROR( "FTSJob already has been submitted" )

    transfers = []

    for ftsFile in self:
      trans = fts3.new_transfer( ftsFile.SourceSURL,
                                ftsFile.TargetSURL,
                                checksum = 'ADLER32:%s'%ftsFile.Checksum,
                                filesize = ftsFile.Size )
      transfers.append( trans )

    source_spacetoken = self.SourceToken if self.SourceToken else None
    dest_spacetoken = self.TargetToken if self.TargetToken else None
    copy_pin_lifetime = pinTime if pinTime else None
    bring_online = 86400 if pinTime else None

    job = fts3.new_job( transfers = transfers, overwrite = True,
            source_spacetoken = source_spacetoken, spacetoken = dest_spacetoken,
            bring_online = bring_online, copy_pin_lifetime = copy_pin_lifetime, retry = 3 )

    try:
      if not self._fts3context:
        self._fts3context = fts3.Context( endpoint = self.FTSServer, request_class = ftsSSLRequest, verify = False )
      context = self._fts3context
      self.FTSGUID = fts3.submit( context, job )

    except Exception as e:
      return S_ERROR( "Error at submission: %s" % e )


    self.Status = "Submitted"
    self._log = gLogger.getSubLogger( "req_%s/FTSJob-%s" % ( self.RequestID, self.FTSGUID ) , True )
    for ftsFile in self:
      ftsFile.FTSGUID = self.FTSGUID
      ftsFile.Status = "Submitted"
    return S_OK()

  def monitorFTS3( self, full = False ):
    if not self.FTSGUID:
      return S_ERROR( "FTSGUID not set, FTS job not submitted?" )

    jobStatusDict = None
    try:
      if not self._fts3context:
        self._fts3context = fts3.Context( endpoint = self.FTSServer, request_class = ftsSSLRequest, verify = False )
      context = self._fts3context
      jobStatusDict = fts3.get_job_status( context, self.FTSGUID, list_files = True )
    except Exception as e:
      return S_ERROR( "Error getting the job status %s" % e )

    self.Status = jobStatusDict['job_state'].capitalize()

    filesInfoList = jobStatusDict['files']
    statusSummary = {}
    for fileDict in filesInfoList:
      file_state = fileDict['file_state'].capitalize()
      statusSummary[file_state] = statusSummary.get( file_state, 0 ) + 1

    total = len( filesInfoList )
    completed = sum( [ statusSummary.get( state, 0 ) for state in FTSFile.FINAL_STATES ] )
    self.Completeness = 100 * completed / total

    if not full:
      return S_OK( statusSummary )

    ftsFilesPrinted = False
    for fileDict in filesInfoList:
      sourceURL = fileDict['source_surl']
      targetURL = fileDict['dest_surl']
      fileStatus = fileDict['file_state'].capitalize()
      reason = fileDict['reason']
      duration = fileDict['tx_duration']
      candidateFile = None
      for ftsFile in self:
        if ftsFile.SourceSURL == sourceURL and ftsFile.TargetSURL == targetURL :
          candidateFile = ftsFile
          break
      if candidateFile is None:
        self._log.warn( 'FTSFile not found', 'Source: %s, Target: %s' % ( sourceURL, targetURL ) )
        if not ftsFilesPrinted:
          ftsFilesPrinted = True
          if not len( self ):
            self._log.warn( 'Monitored FTS job is empty!' )
          else:
            self._log.warn( 'All FTS files are:', '\n' + '\n'.join( ['Source: %s, Target: %s' % ( ftsFile.SourceSURL, ftsFile.TargetSURL ) for ftsFile in self] ) )
      else:
        candidateFile.Status = fileStatus
        candidateFile.Error = reason
        candidateFile._duration = duration

        if candidateFile.Status == "Failed":
          for missingSource in self.missingSourceErrors:
            if missingSource.match( reason ):
              candidateFile.Error = "MissingSource"

    # # register successful files
    if self.Status in FTSJob.FINALSTATES:
      return self.finalize()
    return S_OK()


  def monitorFTS( self, ftsVersion, command = "glite-transfer-status", full = False ):
    """ Wrapper calling the proper method for a given version of FTS"""

    if ftsVersion == "FTS2":
      return self.monitorFTS2( command = command, full = full )
    elif ftsVersion == "FTS3":
      return self.monitorFTS3( full = full )
    else:
      return S_ERROR( "monitorFTS: unknown FTS version %s" % ftsVersion )


  def submitFTS( self, ftsVersion, command = 'glite-transfer-submit', pinTime = False ):
    """ Wrapper calling the proper method for a given version of FTS"""

    if ftsVersion == "FTS2":
      return self.submitFTS2( command = command, pinTime = pinTime )
    elif ftsVersion == "FTS3":
      return self.submitFTS3( pinTime = pinTime )
    else:
      return S_ERROR( "submitFTS: unknown FTS version %s" % ftsVersion )


  def finalize( self ):
    """ register successfully transferred  files """

    if self.Status not in FTSJob.FINALSTATES:
      return S_OK()

    if not len( self ):
      return S_ERROR( "Empty job in finalize" )

    startTime = time.time()
    targetSE = StorageElement( self.TargetSE )
    toRegister = [ ftsFile for ftsFile in self if ftsFile.Status == "Finished" ]
    toRegisterDict = {}
    for ftsFile in toRegister:
      pfn = returnSingleResult( targetSE.getURL( ftsFile.LFN, protocol = 'srm' ) )
      if pfn["OK"]:
        pfn = pfn["Value"]
        toRegisterDict[ ftsFile.LFN ] = { "PFN": pfn, "SE": self.TargetSE }
      else:
        self._log.error( "Error getting SRM URL", pfn['Message'] )

    if toRegisterDict:
      self._regTotal += len( toRegisterDict )
      register = self._fc.addReplica( toRegisterDict )
      self._regTime += time.time() - startTime
      if not register["OK"]:
        self._log.error( 'Error registering replica', register['Message'] )
        for ftsFile in toRegister:
          ftsFile.Error = "AddCatalogReplicaFailed"
        return register
      register = register["Value"]
      self._regSuccess += len( register.get( 'Successful', {} ) )
      if self._regSuccess:
        self._log.info( 'Successfully registered %d replicas' % self._regSuccess )
      failedFiles = register.get( "Failed", {} )
      errorReason = {}
      for lfn, reason in failedFiles.items():
        errorReason.setdefault( str( reason ), [] ).append( lfn )
      for reason in errorReason:
        self._log.error( 'Error registering %d replicas' % len( errorReason[reason] ), reason )
      for ftsFile in toRegister:
        if ftsFile.LFN in failedFiles:
          ftsFile.Error = "AddCatalogReplicaFailed"
    else:
      statuses = set( [ftsFile.Status for ftsFile in self] )
      self._log.warn( "No replicas to register for FTSJob (%s) - Files status: '%s'" % \
                      ( self.Status, ','.join( sorted( statuses ) ) ) )

    return S_OK()

  def toSQL( self ):
    """ prepare SQL INSERT or UPDATE statement

    :return: str with SQL fragment
    """
    colVals = []
    for column, value in self.__data__.items():
      if value and column not in ( "FTSJobID", "LastUpdate" ):
        colStr = "`%s`" % column
        if isinstance( value, datetime.datetime ) or isinstance( value, basestring ):
          valStr = "'%s'" % value
        else:
          valStr = str( value )
        colVals.append( ( colStr, valStr ) )
    colVals.append( ( "`LastUpdate`", "UTC_TIMESTAMP()" ) )
    query = []
    if self.FTSJobID:
      query.append( "UPDATE `FTSJob` SET " )
      query.append( ",".join( [ "%s=%s" % item for item in colVals  ] ) )
      query.append( " WHERE `FTSJobID`=%d;\n" % self.FTSJobID )
    else:
      query.append( "INSERT INTO `FTSJob` " )
      columns = "(%s)" % ",".join( [ column for column, value in colVals ] )
      values = "(%s)" % ",".join( [ value for column, value in colVals ] )
      query.append( columns )
      query.append( " VALUES %s;" % values )

    return S_OK( "".join( query ) )

  def toJSON( self ):
    """ dump to JSON format """
    digest = dict( zip( self.__data__.keys(),
                        [ str( val ) if val else "" for val in self.__data__.values() ] ) )
    digest["FTSFiles"] = []
    for ftsFile in self:
      fileJSON = ftsFile.toJSON()
      if not fileJSON["OK"]:
        return fileJSON
      digest["FTSFiles"].append( fileJSON["Value"] )
    return S_OK( digest )
Exemple #2
0
class FTSJob(Record):
    """
  .. class:: FTSJob

  class describing one FTS job
  """

    # # initial states
    INITSTATES = ("Submitted", "Ready", "Staging")
    # # ongoing transfer states
    TRANSSTATES = ("Active", "Hold")
    # # failed states
    FAILEDSTATES = ("Canceled", "Failed")
    # # finished
    FINALSTATES = ("Finished", "FinishedDirty", "Failed", "Canceled")

    # # missing source regexp patterns
    missingSourceErrors = [
      re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[INVALID_PATH\] Failed" ),
      re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[INVALID_PATH\] No such file or directory" ),
      re.compile( r"SOURCE error during PREPARATION phase: \[INVALID_PATH\] Failed" ),
      re.compile( r"SOURCE error during PREPARATION phase: \[INVALID_PATH\] The requested file either does not exist" ),
      re.compile( r"TRANSFER error during TRANSFER phase: \[INVALID_PATH\] the server sent an error response: 500 500"\
                 " Command failed. : open error: No such file or directory" ),
      re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[USER_ERROR\] source file doesnt exist" ) ]

    def __init__(self, fromDict=None):
        """c'tor

    :param self: self reference
    :param dict fromDict: data dict
    """
        Record.__init__(self)
        now = datetime.datetime.utcnow().replace(microsecond=0)
        self.__data__["CreationTime"] = now
        self.__data__["SubmitTime"] = now
        self.__data__["LastUpdate"] = now
        self.__data__["Status"] = "Submitted"
        self.__data__["Completeness"] = 0
        self.__data__["FTSJobID"] = 0
        self._regTime = 0.
        self._regSuccess = 0
        self._regTotal = 0
        self.__files__ = TypedList(allowedTypes=FTSFile)

        self._fc = FileCatalog()

        self._log = gLogger.getSubLogger("FTSJob-%s" % self.FTSJobID, True)

        self._states = tuple(
            set(self.INITSTATES + self.TRANSSTATES + self.FAILEDSTATES +
                self.FINALSTATES))

        fromDict = fromDict if fromDict else {}
        for ftsFileDict in fromDict.get("FTSFiles", []):
            self += FTSFile(ftsFileDict)
        if "FTSFiles" in fromDict: del fromDict["FTSFiles"]
        for key, value in fromDict.items():
            if key not in self.__data__:
                raise AttributeError("Unknown FTSJob attribute '%s'" % key)
            if value:
                setattr(self, key, value)

    @staticmethod
    def tableDesc():
        """ get table desc """
        return { "Fields" :
                 { "FTSJobID" : "INTEGER NOT NULL AUTO_INCREMENT",
                   "FTSGUID" :  "VARCHAR(64) NOT NULL",
                   "OperationID": "INTEGER NOT NULL",
                   "RequestID": "INTEGER NOT NULL",
                   "SourceSE" : "VARCHAR(128) NOT NULL",
                   "TargetSE" : "VARCHAR(128) NOT NULL",
                   "FTSServer" : "VARCHAR(255) NOT NULL",
                   "TargetToken": "VARCHAR(255)",
                   "SourceToken": "VARCHAR(255)",
                   "Size": "BIGINT NOT NULL",
                   "Files": "INTEGER NOT NULL",
                   "Completeness": "INTEGER NOT NULL DEFAULT 0",
                   "FailedFiles": "INTEGER DEFAULT 0",
                   "FailedSize": "INTEGER DEFAULT 0",
                   "Status" : "ENUM( 'Submitted', 'Ready', 'Staging', 'Canceled', 'Active', 'Hold', "\
                    "'Failed', 'Finished', 'FinishedDirty', 'Assigned' ) DEFAULT 'Submitted'",
                   "Error" : "VARCHAR(255)",
                   "CreationTime" : "DATETIME",
                   "SubmitTime" : "DATETIME",
                   "LastUpdate" : "DATETIME"  },
                 "PrimaryKey" : [ "FTSJobID" ],
                 "Indexes" : { "FTSJobID" : [ "FTSJobID" ], "FTSGUID": [ "FTSGUID" ] } }

    @property
    def FTSJobID(self):
        """ FTSJobID getter """
        return self.__data__["FTSJobID"]

    @FTSJobID.setter
    def FTSJobID(self, value):
        """ FTSJobID setter """
        self.__data__["FTSJobID"] = long(value) if value else 0

    @property
    def RequestID(self):
        """ RequestID getter """
        return self.__data__["RequestID"]

    @RequestID.setter
    def RequestID(self, value):
        """ RequestID setter """
        self.__data__["RequestID"] = long(value) if value else 0

    @property
    def OperationID(self):
        """ OperationID getter """
        return self.__data__["OperationID"]

    @OperationID.setter
    def OperationID(self, value):
        """ OperationID setter """
        self.__data__["OperationID"] = long(value) if value else 0

    @property
    def FTSGUID(self):
        """ FTSGUID prop """
        return self.__data__["FTSGUID"]

    @FTSGUID.setter
    def FTSGUID(self, value):
        """ FTSGUID setter """
        if value:
            if type(value) not in (str, unicode):
                raise TypeError("FTSGUID should be a string!")
            if not checkGuid(value):
                raise ValueError("'%s' is not a valid GUID!" % str(value))
        self.__data__["FTSGUID"] = value

    @property
    def FTSServer(self):
        """ FTSServer getter """
        return self.__data__["FTSServer"]

    @FTSServer.setter
    def FTSServer(self, url):
        """ FTSServer getter """
        self.__data__["FTSServer"] = url

    @property
    def Completeness(self):
        """ completeness getter """
        return self.__data__["Completeness"]

    @Completeness.setter
    def Completeness(self, value):
        """ completeness setter """
        self.__data__["Completeness"] = int(value) if value else 0

    @property
    def Error(self):
        """ error getter """
        return self.__data__["Error"]

    @Error.setter
    def Error(self, error):
        """ error setter """
        self.__data__["Error"] = str(error)[255:]

    @property
    def Files(self):
        """ nb files getter """
        self.__data__["Files"] = len(self)
        return self.__data__["Files"]

    @Files.setter
    def Files(self, value):
        """ nb files setter """
        self.__data__["Files"] = len(self)

    @property
    def Status(self):
        """ status prop """
        if not self.__data__["Status"]:
            self.__data__["Status"] = "Waiting"
        return self.__data__["Status"]

    @Status.setter
    def Status(self, value):
        """ status setter """
        value = self._normalizedStatus(value.strip())
        if value not in self._states:
            raise ValueError("Unknown FTSJob Status: '%s'" % str(value))
        self.__data__["Status"] = value

    @property
    def FailedFiles(self):
        """ nb failed files getter """
        self.__data__["FailedFiles"] = len([
            ftsFile for ftsFile in self
            if ftsFile.Status in FTSFile.FAILED_STATES
        ])
        return self.__data__["FailedFiles"]

    @FailedFiles.setter
    def FailedFiles(self, value):
        """ nb failed files setter """
        if value:
            self.__data__["FailedFiles"] = value
        else:
            self.__data__["FailedFiles"] = sum([
                ftsFile for ftsFile in self
                if ftsFile.Status in FTSFile.FAILED_STATES
            ])

    @property
    def Size(self):
        """ size getter """
        # if not self.__data__["Size"]:
        self.__data__["Size"] = sum([ftsFile.Size for ftsFile in self])
        return self.__data__["Size"]

    @Size.setter
    def Size(self, value):
        """ size setter """
        if value:
            self.__data__["Size"] = value
        else:
            self.__data__["Size"] = sum([ftsFile.Size for ftsFile in self])

    @property
    def FailedSize(self):
        """ size getter """
        if not self.__data__["FailedSize"]:
            self.__data__["FailedSize"] = sum([
                ftsFile.Size for ftsFile in self
                if ftsFile.Status in FTSFile.FAILED_STATES
            ])
        return self.__data__["FailedSize"]

    @FailedSize.setter
    def FailedSize(self, value):
        """ size setter """
        if value:
            self.__data__["FailedSize"] = value
        else:
            self.__data__["FailedSize"] = sum([
                ftsFile.Size for ftsFile in self
                if ftsFile.Status in FTSFile.FAILED_STATES
            ])

    @property
    def CreationTime(self):
        """ creation time getter """
        return self.__data__["CreationTime"]

    @CreationTime.setter
    def CreationTime(self, value=None):
        """ creation time setter """
        if type(value) not in (datetime.datetime, str):
            raise TypeError("CreationTime should be a datetime.datetime!")
        if type(value) == str:
            value = datetime.datetime.strptime(
                value.split(".")[0], '%Y-%m-%d %H:%M:%S')
        self.__data__["CreationTime"] = value

    @property
    def SubmitTime(self):
        """ request's submission time getter """
        return self.__data__["SubmitTime"]

    @SubmitTime.setter
    def SubmitTime(self, value=None):
        """ submission time setter """
        if type(value) not in (datetime.datetime, str):
            raise TypeError("SubmitTime should be a datetime.datetime!")
        if type(value) == str:
            value = datetime.datetime.strptime(
                value.split(".")[0], '%Y-%m-%d %H:%M:%S')
        self.__data__["SubmitTime"] = value

    @property
    def LastUpdate(self):
        """ last update getter """
        return self.__data__["LastUpdate"]

    @LastUpdate.setter
    def LastUpdate(self, value=None):
        """ last update setter """
        if type(value) not in (datetime.datetime, str):
            raise TypeError("LastUpdate should be a datetime.datetime!")
        if type(value) == str:
            value = datetime.datetime.strptime(
                value.split(".")[0], '%Y-%m-%d %H:%M:%S')
        self.__data__["LastUpdate"] = value

    @property
    def TargetSE(self):
        """ target SE getter """
        return self.__data__["TargetSE"]

    @TargetSE.setter
    def TargetSE(self, targetSE):
        """ target SE setter """
        self.__data__["TargetSE"] = targetSE

    @property
    def SourceSE(self):
        """ source SE getter """
        return self.__data__["SourceSE"]

    @SourceSE.setter
    def SourceSE(self, sourceSE):
        """ source SE setter """
        self.__data__["SourceSE"] = sourceSE

    @property
    def SourceToken(self):
        """ source token getter """
        return self.__data__["SourceToken"]

    @SourceToken.setter
    def SourceToken(self, sourceToken):
        """ source SE setter """
        self.__data__["SourceToken"] = sourceToken

    @property
    def TargetToken(self):
        """ target token getter """
        return self.__data__["TargetToken"]

    @TargetToken.setter
    def TargetToken(self, targetToken):
        """ target SE setter """
        self.__data__["TargetToken"] = targetToken

    # # FTSJobFiles arithmetics
    def __contains__(self, subFile):
        """ in operator """
        return subFile in self.__files__

    def __iadd__(self, ftsFile):
        """ += operator """
        if ftsFile not in self:
            self.__files__.append(ftsFile)
            ftsFile._parent = self
            self.Files
            self.Size
        return self

    def __add__(self, ftsFile):
        """ + operator """
        self += ftsFile

    def addFile(self, ftsFile):
        """ add :ftsFile: to FTS job """
        self += ftsFile

    def subFile(self, ftsFile):
        """ remove ftsFile from this job """
        if ftsFile in self:
            ftsFile._parent = None
            self.__files__.remove(ftsFile)

    # # helpers for looping
    def __iter__(self):
        """ files iterator """
        return self.__files__.__iter__()

    def __getitem__(self, i):
        """ [] op for files """
        return self.__files__.__getitem__(i)

    def __delitem__(self, i):
        """ del ftsJob[i] """
        self.__files__.__delitem__(i)

    def __setitem__(self, i, ftsFile):
        """ ftsJob[i] = ftsFile """
        self.__files__.__setitem__(i, ftsFile)

    def fileStatusList(self):
        """ get list of files statuses """
        return [ftsFile.Status for ftsFile in self]

    def __nonzero__(self):
        """ for comparisons
    """
        return True

    def __len__(self):
        """ nb of subFiles """
        return len(self.__files__)

    def _surlPairs(self):
        """ create and return SURL pair file """
        surls = []
        for ftsFile in self:
            checksum = "%s:%s" % (
                ftsFile.ChecksumType, ftsFile.Checksum
            ) if ftsFile.ChecksumType and ftsFile.Checksum else ""
            surls.append("%s %s %s" %
                         (ftsFile.SourceSURL, ftsFile.TargetSURL, checksum))
        return "\n".join(surls)

    def submitFTS2(self, command='glite-transfer-submit', pinTime=False):
        """ submit fts job using FTS2 client """
        if self.FTSGUID:
            return S_ERROR("FTSJob has already been submitted")
        surls = self._surlPairs()
        if not surls:
            return S_ERROR("No files to submit")
        fd, fileName = tempfile.mkstemp()
        surlFile = os.fdopen(fd, 'w')
        surlFile.write(surls)
        surlFile.close()
        submitCommand = command.split() + \
                         [ "-s",
                         self.FTSServer,
                         "-f",
                         fileName,
                         "-o",
                         "-K" ]
        if self.TargetToken:
            submitCommand += ["-t", self.TargetToken]
        if self.SourceToken:
            submitCommand += ["-S", self.SourceToken]
        if pinTime:
            submitCommand += [
                "--copy-pin-lifetime",
                "%d" % pinTime, "--bring-online", '86400'
            ]

        submit = executeGridCommand("", submitCommand)
        os.remove(fileName)
        if not submit["OK"]:
            return submit
        returnCode, output, errStr = submit["Value"]
        if returnCode != 0:
            return S_ERROR(errStr if errStr else output)
        self.FTSGUID = output.replace("\n", "")
        self.Status = "Submitted"
        for ftsFile in self:
            ftsFile.FTSGUID = self.FTSGUID
            ftsFile.Status = "Submitted"
        return S_OK()

    def _normalizedStatus(self, status):
        for st in self._states:
            if status.lower() == st.lower():
                return st
        return status

    def monitorFTS2(self, command="glite-transfer-status", full=False):
        """ monitor fts job """
        if not self.FTSGUID:
            return S_ERROR("FTSGUID not set, FTS job not submitted?")

        monitorCommand = command.split() + \
                           ["--verbose",
                           "-s",
                           self.FTSServer,
                           self.FTSGUID ]

        if full:
            monitorCommand.append("-l")

        monitor = executeGridCommand("", monitorCommand)
        if not monitor["OK"]:
            return monitor
        returnCode, outputStr, errStr = monitor["Value"]

        # Returns a non zero status if error
        if returnCode != 0:
            if 'was not found' in outputStr and not errStr:
                errStr = 'Job was not found'
            return S_ERROR(errStr)

        outputStr = outputStr.replace("'", "").replace("<",
                                                       "").replace(">", "")

        # # set FTS job status
        regExp = re.compile("Status:\\s+(\\S+)")

        # with FTS3 this can be uppercase
        self.Status = re.search(regExp, outputStr).group(1)

        statusSummary = {}
        # This is capitalized, even in FTS3!
        for state in FTSFile.ALL_STATES:
            regExp = re.compile("\\s+%s:\\s+(\\d+)" % state)
            if regExp.search(outputStr):
                statusSummary[state] = int(
                    re.search(regExp, outputStr).group(1))

        total = sum(statusSummary.values())
        completed = sum(
            [statusSummary.get(state, 0) for state in FTSFile.FINAL_STATES])
        self.Completeness = 100 * completed / total if total else 0

        if not full:
            return S_OK(statusSummary)

        # The order of informations is not the same for glite- and fts- !!!
        # In order: new fts-, old fts-, glite-
        iExptr = None
        for iExptr, exptr in enumerate(
            ('[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Staging:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
             '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
             '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Retries:[ ]+(\\d+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)'
             )):
            regExp = re.compile(exptr, re.S)
            fileInfo = re.findall(regExp, outputStr)
            if fileInfo:
                break
        if not fileInfo:
            return S_ERROR("Error monitoring job (no regexp match)")
        for info in fileInfo:
            if iExptr == 0:
                # version >= 3.2.30
                sourceURL, targetURL, fileStatus, reason, duration, _retries, _staging = info
            elif iExptr == 1:
                # version FTS3 < 3.2.30
                sourceURL, targetURL, fileStatus, reason, duration, _retries = info
            elif iExptr == 2:
                # version FTS2
                sourceURL, targetURL, fileStatus, _retries, reason, duration = info
            else:
                return S_ERROR('Error monitoring job (implement match %d)' %
                               iExptr)
            candidateFile = None
            for ftsFile in self:
                if ftsFile.SourceSURL == sourceURL:
                    candidateFile = ftsFile
                    break
            if not candidateFile:
                continue
            # Can be uppercase for FTS3
            if not candidateFile.TargetSURL:
                candidateFile.TargetSURL = targetURL
            candidateFile.Status = fileStatus
            candidateFile.Error = reason
            candidateFile._duration = duration

            if candidateFile.Status == "Failed":
                for missingSource in self.missingSourceErrors:
                    if missingSource.match(reason):
                        candidateFile.Error = "MissingSource"
            # If the staging info was present, record it
            if len(info) > 6:
                candidateFile._staging = info[6]
        # # register successful files
        if self.Status in FTSJob.FINALSTATES:
            return self.finalize()

        return S_OK()

    def finalize(self):
        """ register successfully transferred  files """

        if self.Status not in FTSJob.FINALSTATES:
            return S_OK()

        startTime = time.time()
        targetSE = StorageElement(self.TargetSE)
        toRegister = [
            ftsFile for ftsFile in self if ftsFile.Status == "Finished"
        ]
        toRegisterDict = {}
        for ftsFile in toRegister:
            pfn = returnSingleResult(
                targetSE.getPfnForProtocol(ftsFile.TargetSURL,
                                           protocol="SRM2",
                                           withPort=False))
            if not pfn["OK"]:
                continue
            pfn = pfn["Value"]
            toRegisterDict[ftsFile.LFN] = {"PFN": pfn, "SE": self.TargetSE}

        if toRegisterDict:
            self._regTotal += len(toRegisterDict)
            register = self._fc.addReplica(toRegisterDict)
            self._regTime += time.time() - startTime
            if not register["OK"]:
                for ftsFile in toRegister:
                    ftsFile.Error = "AddCatalogReplicaFailed"
                return register
            register = register["Value"]
            self._regSuccess += len(register.get('Successful', {}))
            failedFiles = register.get("Failed", {})
            for ftsFile in toRegister:
                if ftsFile.LFN in failedFiles:
                    ftsFile.Error = "AddCatalogReplicaFailed"

        return S_OK()

    def toSQL(self):
        """ prepare SQL INSERT or UPDATE statement

    :return: str with SQL fragment
    """
        colVals = [
            ("`%s`" % column, "'%s'" %
             value if type(value) in (str, datetime.datetime) else str(value))
            for column, value in self.__data__.items()
            if value and column not in ("FTSJobID", "LastUpdate")
        ]
        colVals.append(("`LastUpdate`", "UTC_TIMESTAMP()"))
        query = []
        if self.FTSJobID:
            query.append("UPDATE `FTSJob` SET ")
            query.append(",".join(["%s=%s" % item for item in colVals]))
            query.append(" WHERE `FTSJobID`=%d;\n" % self.FTSJobID)
        else:
            query.append("INSERT INTO `FTSJob` ")
            columns = "(%s)" % ",".join([column for column, value in colVals])
            values = "(%s)" % ",".join([value for column, value in colVals])
            query.append(columns)
            query.append(" VALUES %s;" % values)

        return S_OK("".join(query))

    def toJSON(self):
        """ dump to JSON format """
        digest = dict(
            zip(self.__data__.keys(),
                [str(val) if val else "" for val in self.__data__.values()]))
        digest["FTSFiles"] = []
        for ftsFile in self:
            fileJSON = ftsFile.toJSON()
            if not fileJSON["OK"]:
                return fileJSON
            digest["FTSFiles"].append(fileJSON["Value"])
        return S_OK(digest)
Exemple #3
0
class FTSJob( object ):
  """
  class describing one FTS job
  """

  # # initial states
  INITSTATES = ( "Submitted", "Ready", "Staging" )
  # # ongoing transfer states
  TRANSSTATES = ( "Active", "Hold" )
  # # failed states
  FAILEDSTATES = ( "Canceled", "Failed" )
  # # finished (careful, must be capitalized)
  FINALSTATES = ( "Finished", "Finisheddirty", "FinishedDirty", "Failed", "Canceled" )


  # # missing source regexp patterns
  missingSourceErrors = [
    re.compile( r".*INVALID_PATH\] Failed" ),
    re.compile( r".*INVALID_PATH\] No such file or directory" ),
    re.compile( r".*INVALID_PATH\] The requested file either does not exist" ),
    re.compile( r".*INVALID_PATH\] the server sent an error response: 500 500"\
               " Command failed. : open error: No such file or directory" ),
    re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[USER_ERROR\] source file doesnt exist" ) ]

  def __init__( self, fromDict = None ):
    """c'tor

    :param self: self reference
    :param dict fromDict: data dict
    """
    self.__data__ = dict.fromkeys( self.tableDesc()["Fields"].keys(), None )

    now = datetime.datetime.utcnow().replace( microsecond = 0 )
    self.__data__["CreationTime"] = now
    self.__data__["SubmitTime"] = now
    self.__data__["LastUpdate"] = now
    self.__data__["Status"] = "Submitted"
    self.__data__["Completeness"] = 0
    self.__data__["FTSJobID"] = 0
    self._regTime = 0.
    self._regSuccess = 0
    self._regTotal = 0
    self.__files__ = TypedList( allowedTypes = FTSFile )

    self._fc = FileCatalog()

    self._states = tuple( set( self.INITSTATES + self.TRANSSTATES + self.FAILEDSTATES + self.FINALSTATES ) )

    fromDict = fromDict if fromDict else {}
    for ftsFileDict in fromDict.get( "FTSFiles", [] ):
      self +=FTSFile( ftsFileDict )
    if "FTSFiles" in fromDict:
      del fromDict["FTSFiles"]
    for key, value in fromDict.items():
      if key not in self.__data__:
        raise AttributeError( "Unknown FTSJob attribute '%s'" % key )
      if value:
        setattr( self, key, value )
    self._log = gLogger.getSubLogger( "req_%s/FTSJob-%s" % ( self.RequestID, self.FTSGUID ) , True )

  @staticmethod
  def tableDesc():
    """ get table desc """
    return { "Fields" :
             { "FTSJobID" : "INTEGER NOT NULL AUTO_INCREMENT",
               "FTSGUID" :  "VARCHAR(64) NOT NULL",
               "OperationID": "INTEGER NOT NULL",
               "RequestID": "INTEGER NOT NULL",
               "SourceSE" : "VARCHAR(128) NOT NULL",
               "TargetSE" : "VARCHAR(128) NOT NULL",
               "FTSServer" : "VARCHAR(255) NOT NULL",
               "TargetToken": "VARCHAR(255)",
               "SourceToken": "VARCHAR(255)",
               "Size": "BIGINT NOT NULL",
               "Files": "INTEGER NOT NULL",
               "Completeness": "INTEGER NOT NULL DEFAULT 0",
               "FailedFiles": "INTEGER DEFAULT 0",
               "FailedSize": "INTEGER DEFAULT 0",
               "Status" : "ENUM( 'Submitted', 'Ready', 'Staging', 'Canceled', 'Active', 'Hold', "\
                "'Failed', 'Finished', 'FinishedDirty', 'Assigned' ) DEFAULT 'Submitted'",
               "Error" : "VARCHAR(255)",
               "CreationTime" : "DATETIME",
               "SubmitTime" : "DATETIME",
               "LastUpdate" : "DATETIME"  },
             "PrimaryKey" : [ "FTSJobID" ],
             "Indexes" : { "FTSJobID" : [ "FTSJobID" ], "FTSGUID": [ "FTSGUID" ] } }

  @property
  def FTSJobID( self ):
    """ FTSJobID getter """
    return self.__data__["FTSJobID"]

  @FTSJobID.setter
  def FTSJobID( self, value ):
    """ FTSJobID setter """
    self.__data__["FTSJobID"] = long( value ) if value else 0

  @property
  def RequestID( self ):
    """ RequestID getter """
    return self.__data__["RequestID"]

  @RequestID.setter
  def RequestID( self, value ):
    """ RequestID setter """
    self.__data__["RequestID"] = long( value ) if value else 0

  @property
  def OperationID( self ):
    """ OperationID getter """
    return self.__data__["OperationID"]

  @OperationID.setter
  def OperationID( self, value ):
    """ OperationID setter """
    self.__data__["OperationID"] = long( value ) if value else 0

  @property
  def FTSGUID( self ):
    """ FTSGUID prop """
    return self.__data__["FTSGUID"]

  @FTSGUID.setter
  def FTSGUID( self, value ):
    """ FTSGUID setter """
    if value:
      if type( value ) not in ( str, unicode ):
        raise TypeError( "FTSGUID should be a string!" )
      if not checkGuid( value ):
        raise ValueError( "'%s' is not a valid GUID!" % str( value ) )
    self.__data__["FTSGUID"] = value

  @property
  def FTSServer( self ):
    """ FTSServer getter """
    return self.__data__["FTSServer"]

  @FTSServer.setter
  def FTSServer( self, url ):
    """ FTSServer getter """
    self.__data__["FTSServer"] = url

  @property
  def Completeness( self ):
    """ completeness getter """
    return self.__data__["Completeness"]

  @Completeness.setter
  def Completeness( self, value ):
    """ completeness setter """
    self.__data__["Completeness"] = int( value ) if value else 0

  @property
  def Error( self ):
    """ error getter """
    return self.__data__["Error"]

  @Error.setter
  def Error( self, error ):
    """ error setter """
    self.__data__["Error"] = str( error )[255:]

  @property
  def Files( self ):
    """ nb files getter """
    self.__data__["Files"] = len( self )
    return self.__data__["Files"]

  @Files.setter
  def Files( self, value ):
    """ nb files setter """
    self.__data__["Files"] = len( self )

  @property
  def Status( self ):
    """ status prop """
    if not self.__data__["Status"]:
      self.__data__["Status"] = "Waiting"
    return self.__data__["Status"]

  @Status.setter
  def Status( self, value ):
    """ status setter """
    value = self._normalizedStatus( value.strip() )
    if value not in self._states:
      raise ValueError( "Unknown FTSJob Status: '%s'" % str( value ) )
    self.__data__["Status"] = value

  @property
  def FailedFiles( self ):
    """ nb failed files getter """
    self.__data__["FailedFiles"] = len( [ ftsFile for ftsFile in self
                                         if ftsFile.Status in FTSFile.FAILED_STATES ] )
    return self.__data__["FailedFiles"]

  @FailedFiles.setter
  def FailedFiles( self, value ):
    """ nb failed files setter """
    if value:
      self.__data__["FailedFiles"] = value
    else:
      self.__data__["FailedFiles"] = sum( [ ftsFile for ftsFile in self
                                           if ftsFile.Status in FTSFile.FAILED_STATES ] )

  @property
  def Size( self ):
    """ size getter """
    # if not self.__data__["Size"]:
    self.__data__["Size"] = sum( [ ftsFile.Size for ftsFile in self ] )
    return self.__data__["Size"]

  @Size.setter
  def Size( self, value ):
    """ size setter """
    if value:
      self.__data__["Size"] = value
    else:
      self.__data__["Size"] = sum( [ ftsFile.Size for ftsFile in self ] )

  @property
  def FailedSize( self ):
    """ size getter """
    if not self.__data__["FailedSize"]:
      self.__data__["FailedSize"] = sum( [ ftsFile.Size for ftsFile in self
                                          if ftsFile.Status in FTSFile.FAILED_STATES ] )
    return self.__data__["FailedSize"]

  @FailedSize.setter
  def FailedSize( self, value ):
    """ size setter """
    if value:
      self.__data__["FailedSize"] = value
    else:
      self.__data__["FailedSize"] = sum( [ ftsFile.Size for ftsFile in self
                                          if ftsFile.Status in FTSFile.FAILED_STATES ] )

  @property
  def CreationTime( self ):
    """ creation time getter """
    return self.__data__["CreationTime"]

  @CreationTime.setter
  def CreationTime( self, value = None ):
    """ creation time setter """
    if type( value ) not in ( datetime.datetime, str ) :
      raise TypeError( "CreationTime should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["CreationTime"] = value

  @property
  def SubmitTime( self ):
    """ request's submission time getter """
    return self.__data__["SubmitTime"]

  @SubmitTime.setter
  def SubmitTime( self, value = None ):
    """ submission time setter """
    if type( value ) not in ( datetime.datetime, str ):
      raise TypeError( "SubmitTime should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["SubmitTime"] = value

  @property
  def LastUpdate( self ):
    """ last update getter """
    return self.__data__["LastUpdate"]

  @LastUpdate.setter
  def LastUpdate( self, value = None ):
    """ last update setter """
    if type( value ) not in  ( datetime.datetime, str ):
      raise TypeError( "LastUpdate should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["LastUpdate"] = value

  @property
  def TargetSE( self ):
    """ target SE getter """
    return self.__data__["TargetSE"]

  @TargetSE.setter
  def TargetSE( self, targetSE ):
    """ target SE setter """
    self.__data__["TargetSE"] = targetSE

  @property
  def SourceSE( self ):
    """ source SE getter """
    return self.__data__["SourceSE"]

  @SourceSE.setter
  def SourceSE( self, sourceSE ):
    """ source SE setter """
    self.__data__["SourceSE"] = sourceSE

  @property
  def SourceToken( self ):
    """ source token getter """
    return self.__data__["SourceToken"]

  @SourceToken.setter
  def SourceToken( self, sourceToken ):
    """ source SE setter """
    self.__data__["SourceToken"] = sourceToken

  @property
  def TargetToken( self ):
    """ target token getter """
    return self.__data__["TargetToken"]

  @TargetToken.setter
  def TargetToken( self, targetToken ):
    """ target SE setter """
    self.__data__["TargetToken"] = targetToken

  # # FTSJobFiles arithmetics
  def __contains__( self, subFile ):
    """ in operator """
    return subFile in self.__files__

  def __iadd__( self, ftsFile ):
    """ += operator """
    if ftsFile not in self:
      self.__files__.append( ftsFile )
      ftsFile._parent = self
      self.Files
      self.Size
    return self

  def __add__( self, ftsFile ):
    """ + operator """
    self +=ftsFile

  def addFile( self, ftsFile ):
    """ add :ftsFile: to FTS job """
    self +=ftsFile

  def subFile( self, ftsFile ):
    """ remove ftsFile from this job """
    if ftsFile in self:
      ftsFile._parent = None
      self.__files__.remove( ftsFile )

  # # helpers for looping
  def __iter__( self ):
    """ files iterator """
    return self.__files__.__iter__()

  def __getitem__( self, i ):
    """ [] op for files """
    return self.__files__.__getitem__( i )

  def __delitem__( self, i ):
    """ del ftsJob[i] """
    self.__files__.__delitem__( i )

  def __setitem__( self, i, ftsFile ):
    """ ftsJob[i] = ftsFile """
    self.__files__.__setitem__( i, ftsFile )

  def fileStatusList( self ):
    """ get list of files statuses """
    return [ ftsFile.Status for ftsFile in self ]

  def __nonzero__( self ):
    """ for comparisons
    """
    return True

  def __len__( self ):
    """ nb of subFiles """
    return len( self.__files__ )

  def _surlPairs( self ):
    """ create and return SURL pair file """
    surls = []
    for ftsFile in self:
      checksum = "%s:%s" % ( ftsFile.ChecksumType, ftsFile.Checksum ) if ftsFile.ChecksumType and ftsFile.Checksum else ""
      surls.append( "%s %s %s" % ( ftsFile.SourceSURL, ftsFile.TargetSURL, checksum ) )
    return "\n".join( surls )

  def submitFTS2( self, command = 'glite-transfer-submit', pinTime = False ):
    """ submit fts job using FTS2 client """
    if self.FTSGUID:
      return S_ERROR( "FTSJob has already been submitted" )
    surls = self._surlPairs()
    if not surls:
      return S_ERROR( "No files to submit" )
    fd, fileName = tempfile.mkstemp()
    surlFile = os.fdopen( fd, 'w' )
    surlFile.write( surls )
    surlFile.close()
    submitCommand = command.split() + \
                     [ "-s",
                     self.FTSServer,
                     "-f",
                     fileName,
                     "-o",
                     "-K" ]
    if self.TargetToken:
      submitCommand += [ "-t", self.TargetToken]
    if self.SourceToken:
      submitCommand += [ "-S", self.SourceToken ]
    if pinTime:
      submitCommand += [ "--copy-pin-lifetime", "%d" % pinTime, "--bring-online", '86400' ]

    submit = executeGridCommand( "", submitCommand )
    os.remove( fileName )
    if not submit["OK"]:
      return submit
    returnCode, output, errStr = submit["Value"]
    if returnCode != 0:
      return S_ERROR( errStr if errStr else output )
    self.FTSGUID = output.replace( "\n", "" )
    self.Status = "Submitted"
    for ftsFile in self:
      ftsFile.FTSGUID = self.FTSGUID
      ftsFile.Status = "Submitted"
    return S_OK()

  def _normalizedStatus( self, status ):
    for st in self._states:
      if status.lower() == st.lower():
        return st
    return status

  def monitorFTS2( self, command = "glite-transfer-status", full = False ):
    """ monitor fts job """
    if not self.FTSGUID:
      return S_ERROR( "FTSGUID not set, FTS job not submitted?" )

    monitorCommand = command.split() + \
                       ["--verbose",
                       "-s",
                       self.FTSServer,
                       self.FTSGUID ]

    if full:
      monitorCommand.append( "-l" )

    monitor = executeGridCommand( "", monitorCommand )
    if not monitor["OK"]:
      return monitor
    returnCode, outputStr, errStr = monitor["Value"]

    # Returns a non zero status if error
    if returnCode != 0:
      if 'was not found' in outputStr and not errStr:
        errStr = 'Job was not found'
      return S_ERROR( errStr )

    outputStr = outputStr.replace( "'" , "" ).replace( "<", "" ).replace( ">", "" )

    # # set FTS job status
    regExp = re.compile( "Status:\\s+(\\S+)" )

    # with FTS3 this can be uppercase
    self.Status = re.search( regExp, outputStr ).group( 1 )

    statusSummary = {}
    # This is capitalized, even in FTS3!
    for state in FTSFile.ALL_STATES:
      regExp = re.compile( "\\s+%s:\\s+(\\d+)" % state )
      if regExp.search( outputStr ):
        statusSummary[state] = int( re.search( regExp, outputStr ).group( 1 ) )

    total = sum( statusSummary.values() )
    completed = sum( [ statusSummary.get( state, 0 ) for state in FTSFile.FINAL_STATES ] )
    self.Completeness = 100 * completed / total if total else 0

    if not full:
      return S_OK( statusSummary )

    # The order of informations is not the same for glite- and fts- !!!
    # In order: new fts-, old fts-, glite-
    realJob = len( self ) != 0
    iExptr = None
    for iExptr, exptr in enumerate( ( 
                   '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Staging:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
                   '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
                   '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Retries:[ ]+(\\d+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)'
                   ) ):
      regExp = re.compile( exptr, re.S )
      fileInfo = re.findall( regExp, outputStr )
      if fileInfo:
        break
    if not fileInfo:
      return S_ERROR( "Error monitoring job (no regexp match)" )
    for info in fileInfo:
      if iExptr == 0:
        # version >= 3.2.30
        sourceURL, targetURL, fileStatus, reason, duration, _retries, _staging = info
      elif iExptr == 1:
        # version FTS3 < 3.2.30
        sourceURL, targetURL, fileStatus, reason, duration, _retries = info
      elif iExptr == 2:
        # version FTS2
        sourceURL, targetURL, fileStatus, _retries, reason, duration = info
      else:
        return S_ERROR( 'Error monitoring job (implement match %d)' % iExptr )
      candidateFile = None

      if not realJob:
        # This is used by the CLI monitoring of jobs in case no file was specified
        candidateFile = FTSFile()
        candidateFile.LFN = overlap( sourceURL, targetURL )
        candidateFile.SourceSURL = sourceURL
        candidateFile.Size = 0
        self +=candidateFile
      else:
        for ftsFile in self:
          if ftsFile.SourceSURL == sourceURL:
            candidateFile = ftsFile
            break
        if not candidateFile:
          continue
      # Can be uppercase for FTS3
      if not candidateFile.TargetSURL:
        candidateFile.TargetSURL = targetURL
      candidateFile.Status = fileStatus
      candidateFile.Error = reason
      candidateFile._duration = duration

      if candidateFile.Status == "Failed":
        for missingSource in self.missingSourceErrors:
          if missingSource.match( reason ):
            candidateFile.Error = "MissingSource"
      # If the staging info was present, record it
      if len( info ) > 6:
        candidateFile._staging = info[6]
    # # register successful files
    if self.Status in FTSJob.FINALSTATES:
      return self.finalize()

    return S_OK()

  def submitFTS3( self, pinTime = False ):
    """ submit fts job using FTS3 rest API """

    if self.FTSGUID:
      return S_ERROR( "FTSJob already has been submitted" )

    transfers = []

    for ftsFile in self:
      trans = fts3.new_transfer( ftsFile.SourceSURL,
                                ftsFile.TargetSURL,
                                checksum = ftsFile.Checksum,
                                filesize = ftsFile.Size )
      transfers.append( trans )

    source_spacetoken = self.SourceToken if self.SourceToken else None
    dest_spacetoken = self.TargetToken if self.TargetToken else None
    copy_pin_lifetime = pinTime if pinTime else None
    bring_online = 86400 if pinTime else None

    job = fts3.new_job( transfers = transfers, overwrite = True,
            source_spacetoken = source_spacetoken, spacetoken = dest_spacetoken,
            bring_online = bring_online, copy_pin_lifetime = copy_pin_lifetime, retry = 3 )

    try:
      context = fts3.Context( self.FTSServer )
      self.FTSGUID = fts3.submit( context, job )

    except Exception, e:
      return S_ERROR( "Error at submission: %s" % e )


    self.Status = "Submitted"
    self._log = gLogger.getSubLogger( "req_%s/FTSJob-%s" % ( self.RequestID, self.FTSGUID ) , True )
    for ftsFile in self:
      ftsFile.FTSGUID = self.FTSGUID
      ftsFile.Status = "Submitted"
    return S_OK()
Exemple #4
0
class FTSJob( Record ):
  """
  .. class:: FTSJob

  class describing one FTS job
  """
  # # replica manager
  __replicaManager = None

  # # initial states
  INITSTATES = ( "Submitted", "Ready", "Staging" )
  # # ongoing transfer states
  TRANSSTATES = ( "Active", "Hold" )
  # # failed states
  FAILEDSTATES = ( "Canceled", "Failed" )
  # # finished
  FINALSTATES = ( "Finished", "FinishedDirty", "Failed", "Canceled" )


  # # missing source regexp patterns
  missingSourceErrors = [
    re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[INVALID_PATH\] Failed" ),
    re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[INVALID_PATH\] No such file or directory" ),
    re.compile( r"SOURCE error during PREPARATION phase: \[INVALID_PATH\] Failed" ),
    re.compile( r"SOURCE error during PREPARATION phase: \[INVALID_PATH\] The requested file either does not exist" ),
    re.compile( r"TRANSFER error during TRANSFER phase: \[INVALID_PATH\] the server sent an error response: 500 500"\
               " Command failed. : open error: No such file or directory" ),
    re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[USER_ERROR\] source file doesnt exist" ) ]

  def __init__( self, fromDict = None ):
    """c'tor

    :param self: self reference
    :param dict fromDict: data dict
    """
    Record.__init__( self )
    now = datetime.datetime.utcnow().replace( microsecond = 0 )
    self.__data__["CreationTime"] = now
    self.__data__["SubmitTime"] = now
    self.__data__["LastUpdate"] = now
    self.__data__["Status"] = "Submitted"
    self.__data__["Completeness"] = 0
    self.__data__["FTSJobID"] = 0
    self.__files__ = TypedList( allowedTypes = FTSFile )

    self._log = gLogger.getSubLogger( "FTSJob-%s" % self.FTSJobID , True )

    fromDict = fromDict if fromDict else {}
    for ftsFileDict in fromDict.get( "FTSFiles", [] ):
      self +=FTSFile( ftsFileDict )
    if "FTSFiles" in fromDict: del fromDict["FTSFiles"]
    for key, value in fromDict.items():
      if key not in self.__data__:
        raise AttributeError( "Unknown FTSJob attribute '%s'" % key )
      if value:
        setattr( self, key, value )

  @classmethod
  def replicaManager( cls ):
    """ get replica manager """
    if not cls.__replicaManager:
      cls.__replicaManager = ReplicaManager()
    return cls.__replicaManager

  @staticmethod
  def tableDesc():
    """ get table desc """
    return { "Fields" :
             { "FTSJobID" : "INTEGER NOT NULL AUTO_INCREMENT",
               "FTSGUID" :  "VARCHAR(64) NOT NULL",
               "OperationID": "INTEGER NOT NULL",
               "RequestID": "INTEGER NOT NULL",
               "SourceSE" : "VARCHAR(128) NOT NULL",
               "TargetSE" : "VARCHAR(128) NOT NULL",
               "FTSServer" : "VARCHAR(255) NOT NULL",
               "TargetToken": "VARCHAR(255)",
               "SourceToken": "VARCHAR(255)",
               "Size": "INTEGER NOT NULL",
               "Files": "INTEGER NOT NULL",
               "Completeness": "INTEGER NOT NULL DEFAULT 0",
               "FailedFiles": "INTEGER DEFAULT 0",
               "FailedSize": "INTEGER DEFAULT 0",
               "Status" : "ENUM( 'Submitted', 'Ready', 'Staging', 'Canceled', 'Active', 'Hold', "\
                "'Failed', 'Finished', 'FinishedDirty' ) DEFAULT 'Submitted'",
               "Error" : "VARCHAR(255)",
               "CreationTime" : "DATETIME",
               "SubmitTime" : "DATETIME",
               "LastUpdate" : "DATETIME"  },
             "PrimaryKey" : [ "FTSJobID" ],
             "Indexes" : { "FTSJobID" : [ "FTSJobID" ], "FTSGUID": [ "FTSGUID" ] } }

  @property
  def FTSJobID( self ):
    """ FTSJobID getter """
    return self.__data__["FTSJobID"]

  @FTSJobID.setter
  def FTSJobID( self, value ):
    """ FTSJobID setter """
    self.__data__["FTSJobID"] = long( value ) if value else 0

  @property
  def RequestID( self ):
    """ RequestID getter """
    return self.__data__["RequestID"]

  @RequestID.setter
  def RequestID( self, value ):
    """ RequestID setter """
    value = long( value ) if value else 0
    self.__data__["RequestID"] = value

  @property
  def OperationID( self ):
    """ OperationID getter """
    return self.__data__["OperationID"]

  @OperationID.setter
  def OperationID( self, value ):
    """ OperationID setter """
    value = long( value ) if value else 0
    self.__data__["OperationID"] = value

  @property
  def FTSGUID( self ):
    """ FTSGUID prop """
    return self.__data__["FTSGUID"]

  @FTSGUID.setter
  def FTSGUID( self, value ):
    """ FTSGUID setter """
    if value:
      if type( value ) not in ( str, unicode ):
        raise TypeError( "FTSGUID should be a string!" )
      if not checkGuid( value ):
        raise ValueError( "'%s' is not a valid GUID!" % str( value ) )
    self.__data__["FTSGUID"] = value

  @property
  def FTSServer( self ):
    """ FTSServer getter """
    return self.__data__["FTSServer"]

  @FTSServer.setter
  def FTSServer( self, url ):
    """ FTSServer getter """
    self.__data__["FTSServer"] = url

  @property
  def Completeness( self ):
    """ completeness getter """
    return self.__data__["Completeness"]

  @Completeness.setter
  def Completeness( self, value ):
    """ completeness setter """
    self.__data__["Completeness"] = int( value ) if value else 0

  @property
  def Error( self ):
    """ error getter """
    return self.__data__["Error"]

  @Error.setter
  def Error( self, error ):
    """ error setter """
    self.__data__["Error"] = str( error )[255:]

  @property
  def Files( self ):
    """ nb files getter """
    self.__data__["Files"] = len( self )
    return self.__data__["Files"]

  @Files.setter
  def Files( self, value ):
    """ nb files setter """
    self.__data__["Files"] = len( self )

  @property
  def Status( self ):
    """ status prop """
    if not self.__data__["Status"]:
      self.__data__["Status"] = "Waiting"
    return self.__data__["Status"]

  @Status.setter
  def Status( self, value ):
    """ status setter """
    reStatus = re.compile( "Submitted|Ready|Staging|Hold|Canceled|Active|Failed|Finished|FinishedDirty|Assigned" )
    if not reStatus.match( value ):
      raise ValueError( "Unknown FTSJob Status: %s" % str( value ) )
    self.__data__["Status"] = value

  @property
  def FailedFiles( self ):
    """ nb failed files getter """
    self.__data__["FailedFiles"] = len( [ ftsFile for ftsFile in self
                                         if ftsFile.Status in FTSFile.FAILED_STATES ] )
    return self.__data__["FailedFiles"]

  @FailedFiles.setter
  def FailedFiles( self, value ):
    """ nb failed files setter """
    if value:
      self.__data__["FailedFiles"] = value
    else:
      self.__data__["FailedFiles"] = sum( [ ftsFile for ftsFile in self
                                           if ftsFile.Status in FTSFile.FAILED_STATES ] )

  @property
  def Size( self ):
    """ size getter """
    # if not self.__data__["Size"]:
    self.__data__["Size"] = sum( [ ftsFile.Size for ftsFile in self ] )
    return self.__data__["Size"]

  @Size.setter
  def Size( self, value ):
    """ size setter """
    if value:
      self.__data__["Size"] = value
    else:
      self.__data__["Size"] = sum( [ ftsFile.Size for ftsFile in self ] )

  @property
  def FailedSize( self ):
    """ size getter """
    if not self.__data__["FailedSize"]:
      self.__data__["FailedSize"] = sum( [ ftsFile.Size for ftsFile in self
                                          if ftsFile.Status in FTSFile.FAILED_STATES ] )
    return self.__data__["FailedSize"]

  @FailedSize.setter
  def FailedSize( self, value ):
    """ size setter """
    if value:
      self.__data__["FailedSize"] = value
    else:
      self.__data__["FailedSize"] = sum( [ ftsFile.Size for ftsFile in self
                                          if ftsFile.Status in FTSFile.FAILED_STATES ] )

  @property
  def CreationTime( self ):
    """ creation time getter """
    return self.__data__["CreationTime"]

  @CreationTime.setter
  def CreationTime( self, value = None ):
    """ creation time setter """
    if type( value ) not in ( datetime.datetime, str ) :
      raise TypeError( "CreationTime should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["CreationTime"] = value

  @property
  def SubmitTime( self ):
    """ request's submission time getter """
    return self.__data__["SubmitTime"]

  @SubmitTime.setter
  def SubmitTime( self, value = None ):
    """ submission time setter """
    if type( value ) not in ( datetime.datetime, str ):
      raise TypeError( "SubmitTime should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["SubmitTime"] = value

  @property
  def LastUpdate( self ):
    """ last update getter """
    return self.__data__["LastUpdate"]

  @LastUpdate.setter
  def LastUpdate( self, value = None ):
    """ last update setter """
    if type( value ) not in  ( datetime.datetime, str ):
      raise TypeError( "LastUpdate should be a datetime.datetime!" )
    if type( value ) == str:
      value = datetime.datetime.strptime( value.split( "." )[0], '%Y-%m-%d %H:%M:%S' )
    self.__data__["LastUpdate"] = value

  @property
  def TargetSE( self ):
    """ target SE getter """
    return self.__data__["TargetSE"]

  @TargetSE.setter
  def TargetSE( self, targetSE ):
    """ target SE setter """
    self.__data__["TargetSE"] = targetSE

  @property
  def SourceSE( self ):
    """ source SE getter """
    return self.__data__["SourceSE"]

  @SourceSE.setter
  def SourceSE( self, sourceSE ):
    """ source SE setter """
    self.__data__["SourceSE"] = sourceSE

  @property
  def SourceToken( self ):
    """ source token getter """
    return self.__data__["SourceToken"]

  @SourceToken.setter
  def SourceToken( self, sourceToken ):
    """ source SE setter """
    self.__data__["SourceToken"] = sourceToken

  @property
  def TargetToken( self ):
    """ target token getter """
    return self.__data__["TargetToken"]

  @TargetToken.setter
  def TargetToken( self, targetToken ):
    """ target SE setter """
    self.__data__["TargetToken"] = targetToken

  # # FTSJobFiles arithmetics
  def __contains__( self, subFile ):
    """ in operator """
    return subFile in self.__files__

  def __iadd__( self, ftsFile ):
    """ += operator """
    if ftsFile not in self:
      self.__files__.append( ftsFile )
      ftsFile._parent = self
      self.Files
      self.Size
    return self

  def __add__( self, ftsFile ):
    """ + operator """
    self +=ftsFile

  def addFile( self, ftsFile ):
    """ add :ftsFile: to FTS job """
    self +=ftsFile

  def subFile( self, ftsFile ):
    """ remove ftsFile from this job """
    if ftsFile in self:
      ftsFile._parent = None
      self.__files__.remove( ftsFile )

  # # helpers for looping
  def __iter__( self ):
    """ files iterator """
    return self.__files__.__iter__()

  def __getitem__( self, i ):
    """ [] op for files """
    return self.__files__.__getitem__( i )

  def __delitem__( self, i ):
    """ del ftsJob[i] """
    self.__files__.__delitem__( i )

  def __setitem__( self, i, ftsFile ):
    """ ftsJob[i] = ftsFile """
    self.__files__.__setitem__( i, ftsFile )

  def fileStatusList( self ):
    """ get list of files statuses """
    return [ ftsFile.Status for ftsFile in self ]

  def __len__( self ):
    """ nb of subFiles """
    return len( self.__files__ )

  def _surlPairs( self ):
    """ create and return SURL pair file """
    surls = []
    for ftsFile in self:
      checksum = "%s:%s" % ( ftsFile.ChecksumType, ftsFile.Checksum ) if ftsFile.ChecksumType and ftsFile.Checksum else ""
      surls.append( "%s %s %s" % ( ftsFile.SourceSURL, ftsFile.TargetSURL, checksum ) )
    return "\n".join( surls )

  def submitFTS2( self, stageFiles = False ):
    """ submit fts job using FTS2 client """
    if self.FTSGUID:
      return S_ERROR( "FTSJob already has been submitted" )
    surls = self._surlPairs()
    if not surls:
      return S_ERROR( "No files to submit" )
    fd, fileName = tempfile.mkstemp()
    surlFile = os.fdopen( fd, 'w' )
    surlFile.write( surls )
    surlFile.close()
    submitCommand = [ "glite-transfer-submit",
                     "-s",
                     self.FTSServer,
                     "-f",
                     fileName,
                     "-o",
                     "--compare-checksums" ]
    if self.TargetToken:
      submitCommand.append( "-t %s" % self.TargetToken )
    if self.SourceToken:
      submitCommand.append( "-S %s" % self.SourceToken )
    if stageFiles:
      submitCommand.append( "--copy-pin-lifetime 86400" )

    submit = executeGridCommand( "", submitCommand )
    os.remove( fileName )
    if not submit["OK"]:
      return submit
    returnCode, output, errStr = submit["Value"]
    if not returnCode == 0:
      return S_ERROR( errStr )
    self.FTSGUID = output.replace( "\n", "" )
    self.Status = "Submitted"
    for ftsFile in self:
      ftsFile.FTSGUID = self.FTSGUID
      ftsFile.Status = "Submitted"
    return S_OK()

  def monitorFTS2( self, full = False ):
    """ monitor fts job """
    if not self.FTSGUID:
      return S_ERROR( "FTSGUID not set, FTS job not submitted?" )

    monitorCommand = [ "glite-transfer-status",
                       "--verbose",
                       "-s",
                       self.FTSServer,
                       self.FTSGUID ]

    if full:
      monitorCommand.append( "-l" )

    monitor = executeGridCommand( "", monitorCommand )
    if not monitor["OK"]:
      return monitor
    returnCode, outputStr, errStr = monitor["Value"]

    # Returns a non zero status if error
    if returnCode != 0:
      return S_ERROR( errStr )

    outputStr = outputStr.replace( "'" , "" ).replace( "<", "" ).replace( ">", "" )

    # # set FTS job status
    regExp = re.compile( "Status:\s+(\S+)" )

    self.Status = re.search( regExp, outputStr ).group( 1 )

    statusSummary = {}
    for state in FTSFile.ALL_STATES:
      regExp = re.compile( "\s+%s:\s+(\d+)" % state )
      if regExp.search( outputStr ):
        statusSummary[state] = int( re.search( regExp, outputStr ).group( 1 ) )

    total = sum( statusSummary.values() )
    completed = sum( [ statusSummary.get( state, 0 ) for state in FTSFile.FINAL_STATES ] )
    self.Completeness = 100 * completed / total

    if not full:
      return S_OK( statusSummary )

    regExp = re.compile( "[ ]+Source:[ ]+(\S+)\n[ ]+Destination:[ ]+(\S+)\n[ ]+State:[ ]+(\S+)\n[ ]+Retries:[ ]+(\d+)\n[ ]+Reason:[ ]+([\S ]+).+?[ ]+Duration:[ ]+(\d+)", re.S )
    fileInfo = re.findall( regExp, outputStr )
    for sourceURL, targetURL, fileStatus, retries, reason, duration in fileInfo:
      candidateFile = None
      for ftsFile in self:
        if ftsFile.SourceSURL == sourceURL:
          candidateFile = ftsFile
          break
      if not candidateFile:
        continue
      candidateFile.Status = fileStatus
      candidateFile.Error = reason

      if candidateFile.Status == "Failed":
        for missingSource in self.missingSourceErrors:
          if missingSource.match( reason ):
            candidateFile.Error = "MissingSource"

    # # register successful files
    if self.Status in FTSJob.FINALSTATES:
      return self.finalize()

    return S_OK()

  def finalize( self ):
    """ register successfully transferred  files """

    if self.Status not in FTSJob.FINALSTATES:
      return S_OK()

    targetSE = StorageElement( self.TargetSE )
    toRegister = [ ftsFile for ftsFile in self if ftsFile.Status == "Finished" ]
    toRegisterDict = {}
    for ftsFile in toRegister:
      pfn = targetSE.getPfnForProtocol( ftsFile.TargetSURL, "SRM2", withPort = False )
      if not pfn["OK"]:
        continue
      pfn = pfn["Value"]
      toRegisterDict[ ftsFile.LFN ] = { "PFN": pfn, "SE": self.TargetSE }

    if toRegisterDict:
      register = self.replicaManager().addCatalogReplica( toRegisterDict )
      if not register["OK"]:
        for ftsFile in toRegister:
          ftsFile.Error = "AddCatalogReplicaFailed"
          print ftsFile.Error
        return register
      register = register["Value"]
      failedFiles = register["Failed"] if "Failed" in register else {}
      for ftsFile in toRegister:
        if ftsFile.LFN in failedFiles:
          ftsFile.Error = "AddCatalogReplicaFailed"
          print ftsFile.Error

    return S_OK()

  def toSQL( self ):
    """ prepare SQL INSERT or UPDATE statement

    :return: str with SQL fragment
    """
    colVals = [ ( "`%s`" % column, "'%s'" % value if type( value ) in ( str, datetime.datetime ) else str( value ) )
                for column, value in self.__data__.items()
                if value and column not in  ( "FTSJobID", "LastUpdate" ) ]
    colVals.append( ( "`LastUpdate`", "UTC_TIMESTAMP()" ) )
    query = []
    if self.FTSJobID:
      query.append( "UPDATE `FTSJob` SET " )
      query.append( ",".join( [ "%s=%s" % item for item in colVals  ] ) )
      query.append( " WHERE `FTSJobID`=%d;\n" % self.FTSJobID )
    else:
      query.append( "INSERT INTO `FTSJob` " )
      columns = "(%s)" % ",".join( [ column for column, value in colVals ] )
      values = "(%s)" % ",".join( [ value for column, value in colVals ] )
      query.append( columns )
      query.append( " VALUES %s;" % values )

    return S_OK( "".join( query ) )

  def toJSON( self ):
    """ dump to JSON format """
    digest = dict( zip( self.__data__.keys(),
                        [ str( val ) if val else "" for val in self.__data__.values() ] ) )
    digest["FTSFiles"] = []
    for ftsFile in self:
      fileJSON = ftsFile.toJSON()
      if not fileJSON["OK"]:
        return fileJSON
      digest["FTSFiles"].append( fileJSON["Value"] )
    return S_OK( digest )
Exemple #5
0
class FTSJob(object):
    """
  class describing one FTS job
  """

    # # initial states
    INITSTATES = ("Submitted", "Ready", "Staging")
    # # ongoing transfer states
    TRANSSTATES = ("Active", "Hold")
    # # failed states
    FAILEDSTATES = ("Canceled", "Failed")
    # # finished (careful, must be capitalized)
    FINALSTATES = ("Finished", "Finisheddirty", "FinishedDirty", "Failed",
                   "Canceled")

    # # missing source regexp patterns
    missingSourceErrors = [
      re.compile( r".*INVALID_PATH\] Failed" ),
      re.compile( r".*INVALID_PATH\] No such file or directory" ),
      re.compile( r".*INVALID_PATH\] The requested file either does not exist" ),
      re.compile( r".*INVALID_PATH\] the server sent an error response: 500 500"\
                 " Command failed. : open error: No such file or directory" ),
      re.compile( r"SOURCE error during TRANSFER_PREPARATION phase: \[USER_ERROR\] source file doesnt exist" ) ]

    def __init__(self, fromDict=None):
        """c'tor

    :param self: self reference
    :param dict fromDict: data dict
    """
        self.__data__ = dict.fromkeys(self.tableDesc()["Fields"].keys(), None)

        now = datetime.datetime.utcnow().replace(microsecond=0)
        self.__data__["CreationTime"] = now
        self.__data__["SubmitTime"] = now
        self.__data__["LastUpdate"] = now
        self.__data__["Status"] = "Submitted"
        self.__data__["Completeness"] = 0
        self.__data__["FTSJobID"] = 0
        self._regTime = 0.
        self._regSuccess = 0
        self._regTotal = 0
        self.__files__ = TypedList(allowedTypes=FTSFile)

        self._fc = FileCatalog()

        self._states = tuple(
            set(self.INITSTATES + self.TRANSSTATES + self.FAILEDSTATES +
                self.FINALSTATES))

        fromDict = fromDict if fromDict else {}
        for ftsFileDict in fromDict.get("FTSFiles", []):
            self += FTSFile(ftsFileDict)
        if "FTSFiles" in fromDict:
            del fromDict["FTSFiles"]
        for key, value in fromDict.items():
            if key not in self.__data__:
                raise AttributeError("Unknown FTSJob attribute '%s'" % key)
            if value:
                setattr(self, key, value)
        self._log = gLogger.getSubLogger(
            "req_%s/FTSJob-%s" % (self.RequestID, self.FTSGUID), True)

    @staticmethod
    def tableDesc():
        """ get table desc """
        return { "Fields" :
                 { "FTSJobID" : "INTEGER NOT NULL AUTO_INCREMENT",
                   "FTSGUID" :  "VARCHAR(64) NOT NULL",
                   "OperationID": "INTEGER NOT NULL",
                   "RequestID": "INTEGER NOT NULL",
                   "SourceSE" : "VARCHAR(128) NOT NULL",
                   "TargetSE" : "VARCHAR(128) NOT NULL",
                   "FTSServer" : "VARCHAR(255) NOT NULL",
                   "TargetToken": "VARCHAR(255)",
                   "SourceToken": "VARCHAR(255)",
                   "Size": "BIGINT NOT NULL",
                   "Files": "INTEGER NOT NULL",
                   "Completeness": "INTEGER NOT NULL DEFAULT 0",
                   "FailedFiles": "INTEGER DEFAULT 0",
                   "FailedSize": "INTEGER DEFAULT 0",
                   "Status" : "ENUM( 'Submitted', 'Ready', 'Staging', 'Canceled', 'Active', 'Hold', "\
                    "'Failed', 'Finished', 'FinishedDirty', 'Assigned' ) DEFAULT 'Submitted'",
                   "Error" : "VARCHAR(255)",
                   "CreationTime" : "DATETIME",
                   "SubmitTime" : "DATETIME",
                   "LastUpdate" : "DATETIME"  },
                 "PrimaryKey" : [ "FTSJobID" ],
                 "Indexes" : { "FTSJobID" : [ "FTSJobID" ], "FTSGUID": [ "FTSGUID" ] } }

    @property
    def FTSJobID(self):
        """ FTSJobID getter """
        return self.__data__["FTSJobID"]

    @FTSJobID.setter
    def FTSJobID(self, value):
        """ FTSJobID setter """
        self.__data__["FTSJobID"] = long(value) if value else 0

    @property
    def RequestID(self):
        """ RequestID getter """
        return self.__data__["RequestID"]

    @RequestID.setter
    def RequestID(self, value):
        """ RequestID setter """
        self.__data__["RequestID"] = long(value) if value else 0

    @property
    def OperationID(self):
        """ OperationID getter """
        return self.__data__["OperationID"]

    @OperationID.setter
    def OperationID(self, value):
        """ OperationID setter """
        self.__data__["OperationID"] = long(value) if value else 0

    @property
    def FTSGUID(self):
        """ FTSGUID prop """
        return self.__data__["FTSGUID"]

    @FTSGUID.setter
    def FTSGUID(self, value):
        """ FTSGUID setter """
        if value:
            if type(value) not in (str, unicode):
                raise TypeError("FTSGUID should be a string!")
            if not checkGuid(value):
                raise ValueError("'%s' is not a valid GUID!" % str(value))
        self.__data__["FTSGUID"] = value

    @property
    def FTSServer(self):
        """ FTSServer getter """
        return self.__data__["FTSServer"]

    @FTSServer.setter
    def FTSServer(self, url):
        """ FTSServer getter """
        self.__data__["FTSServer"] = url

    @property
    def Completeness(self):
        """ completeness getter """
        return self.__data__["Completeness"]

    @Completeness.setter
    def Completeness(self, value):
        """ completeness setter """
        self.__data__["Completeness"] = int(value) if value else 0

    @property
    def Error(self):
        """ error getter """
        return self.__data__["Error"]

    @Error.setter
    def Error(self, error):
        """ error setter """
        self.__data__["Error"] = str(error)[255:]

    @property
    def Files(self):
        """ nb files getter """
        self.__data__["Files"] = len(self)
        return self.__data__["Files"]

    @Files.setter
    def Files(self, value):
        """ nb files setter """
        self.__data__["Files"] = len(self)

    @property
    def Status(self):
        """ status prop """
        if not self.__data__["Status"]:
            self.__data__["Status"] = "Waiting"
        return self.__data__["Status"]

    @Status.setter
    def Status(self, value):
        """ status setter """
        value = self._normalizedStatus(value.strip())
        if value not in self._states:
            raise ValueError("Unknown FTSJob Status: '%s'" % str(value))
        self.__data__["Status"] = value

    @property
    def FailedFiles(self):
        """ nb failed files getter """
        self.__data__["FailedFiles"] = len([
            ftsFile for ftsFile in self
            if ftsFile.Status in FTSFile.FAILED_STATES
        ])
        return self.__data__["FailedFiles"]

    @FailedFiles.setter
    def FailedFiles(self, value):
        """ nb failed files setter """
        if value:
            self.__data__["FailedFiles"] = value
        else:
            self.__data__["FailedFiles"] = sum([
                ftsFile for ftsFile in self
                if ftsFile.Status in FTSFile.FAILED_STATES
            ])

    @property
    def Size(self):
        """ size getter """
        # if not self.__data__["Size"]:
        self.__data__["Size"] = sum([ftsFile.Size for ftsFile in self])
        return self.__data__["Size"]

    @Size.setter
    def Size(self, value):
        """ size setter """
        if value:
            self.__data__["Size"] = value
        else:
            self.__data__["Size"] = sum([ftsFile.Size for ftsFile in self])

    @property
    def FailedSize(self):
        """ size getter """
        if not self.__data__["FailedSize"]:
            self.__data__["FailedSize"] = sum([
                ftsFile.Size for ftsFile in self
                if ftsFile.Status in FTSFile.FAILED_STATES
            ])
        return self.__data__["FailedSize"]

    @FailedSize.setter
    def FailedSize(self, value):
        """ size setter """
        if value:
            self.__data__["FailedSize"] = value
        else:
            self.__data__["FailedSize"] = sum([
                ftsFile.Size for ftsFile in self
                if ftsFile.Status in FTSFile.FAILED_STATES
            ])

    @property
    def CreationTime(self):
        """ creation time getter """
        return self.__data__["CreationTime"]

    @CreationTime.setter
    def CreationTime(self, value=None):
        """ creation time setter """
        if type(value) not in (datetime.datetime, str):
            raise TypeError("CreationTime should be a datetime.datetime!")
        if type(value) == str:
            value = datetime.datetime.strptime(
                value.split(".")[0], '%Y-%m-%d %H:%M:%S')
        self.__data__["CreationTime"] = value

    @property
    def SubmitTime(self):
        """ request's submission time getter """
        return self.__data__["SubmitTime"]

    @SubmitTime.setter
    def SubmitTime(self, value=None):
        """ submission time setter """
        if type(value) not in (datetime.datetime, str):
            raise TypeError("SubmitTime should be a datetime.datetime!")
        if type(value) == str:
            value = datetime.datetime.strptime(
                value.split(".")[0], '%Y-%m-%d %H:%M:%S')
        self.__data__["SubmitTime"] = value

    @property
    def LastUpdate(self):
        """ last update getter """
        return self.__data__["LastUpdate"]

    @LastUpdate.setter
    def LastUpdate(self, value=None):
        """ last update setter """
        if type(value) not in (datetime.datetime, str):
            raise TypeError("LastUpdate should be a datetime.datetime!")
        if type(value) == str:
            value = datetime.datetime.strptime(
                value.split(".")[0], '%Y-%m-%d %H:%M:%S')
        self.__data__["LastUpdate"] = value

    @property
    def TargetSE(self):
        """ target SE getter """
        return self.__data__["TargetSE"]

    @TargetSE.setter
    def TargetSE(self, targetSE):
        """ target SE setter """
        self.__data__["TargetSE"] = targetSE

    @property
    def SourceSE(self):
        """ source SE getter """
        return self.__data__["SourceSE"]

    @SourceSE.setter
    def SourceSE(self, sourceSE):
        """ source SE setter """
        self.__data__["SourceSE"] = sourceSE

    @property
    def SourceToken(self):
        """ source token getter """
        return self.__data__["SourceToken"]

    @SourceToken.setter
    def SourceToken(self, sourceToken):
        """ source SE setter """
        self.__data__["SourceToken"] = sourceToken

    @property
    def TargetToken(self):
        """ target token getter """
        return self.__data__["TargetToken"]

    @TargetToken.setter
    def TargetToken(self, targetToken):
        """ target SE setter """
        self.__data__["TargetToken"] = targetToken

    # # FTSJobFiles arithmetics
    def __contains__(self, subFile):
        """ in operator """
        return subFile in self.__files__

    def __iadd__(self, ftsFile):
        """ += operator """
        if ftsFile not in self:
            self.__files__.append(ftsFile)
            ftsFile._parent = self
            self.Files
            self.Size
        return self

    def __add__(self, ftsFile):
        """ + operator """
        self += ftsFile

    def addFile(self, ftsFile):
        """ add :ftsFile: to FTS job """
        self += ftsFile

    def subFile(self, ftsFile):
        """ remove ftsFile from this job """
        if ftsFile in self:
            ftsFile._parent = None
            self.__files__.remove(ftsFile)

    # # helpers for looping
    def __iter__(self):
        """ files iterator """
        return self.__files__.__iter__()

    def __getitem__(self, i):
        """ [] op for files """
        return self.__files__.__getitem__(i)

    def __delitem__(self, i):
        """ del ftsJob[i] """
        self.__files__.__delitem__(i)

    def __setitem__(self, i, ftsFile):
        """ ftsJob[i] = ftsFile """
        self.__files__.__setitem__(i, ftsFile)

    def fileStatusList(self):
        """ get list of files statuses """
        return [ftsFile.Status for ftsFile in self]

    def __nonzero__(self):
        """ for comparisons
    """
        return True

    def __len__(self):
        """ nb of subFiles """
        return len(self.__files__)

    def _surlPairs(self):
        """ create and return SURL pair file """
        surls = []
        for ftsFile in self:
            checksum = "%s:%s" % (
                ftsFile.ChecksumType, ftsFile.Checksum
            ) if ftsFile.ChecksumType and ftsFile.Checksum else ""
            surls.append("%s %s %s" %
                         (ftsFile.SourceSURL, ftsFile.TargetSURL, checksum))
        return "\n".join(surls)

    def submitFTS2(self, command='glite-transfer-submit', pinTime=False):
        """ submit fts job using FTS2 client """
        if self.FTSGUID:
            return S_ERROR("FTSJob has already been submitted")
        surls = self._surlPairs()
        if not surls:
            return S_ERROR("No files to submit")
        fd, fileName = tempfile.mkstemp()
        surlFile = os.fdopen(fd, 'w')
        surlFile.write(surls)
        surlFile.close()
        submitCommand = command.split() + \
                         [ "-s",
                         self.FTSServer,
                         "-f",
                         fileName,
                         "-o",
                         "-K" ]
        if self.TargetToken:
            submitCommand += ["-t", self.TargetToken]
        if self.SourceToken:
            submitCommand += ["-S", self.SourceToken]
        if pinTime:
            submitCommand += [
                "--copy-pin-lifetime",
                "%d" % pinTime, "--bring-online", '86400'
            ]

        submit = executeGridCommand("", submitCommand)
        os.remove(fileName)
        if not submit["OK"]:
            return submit
        returnCode, output, errStr = submit["Value"]
        if returnCode != 0:
            return S_ERROR(errStr if errStr else output)
        self.FTSGUID = output.replace("\n", "")
        self.Status = "Submitted"
        for ftsFile in self:
            ftsFile.FTSGUID = self.FTSGUID
            ftsFile.Status = "Submitted"
        return S_OK()

    def _normalizedStatus(self, status):
        for st in self._states:
            if status.lower() == st.lower():
                return st
        return status

    def monitorFTS2(self, command="glite-transfer-status", full=False):
        """ monitor fts job """
        if not self.FTSGUID:
            return S_ERROR("FTSGUID not set, FTS job not submitted?")

        monitorCommand = command.split() + \
                           ["--verbose",
                           "-s",
                           self.FTSServer,
                           self.FTSGUID ]

        if full:
            monitorCommand.append("-l")

        monitor = executeGridCommand("", monitorCommand)
        if not monitor["OK"]:
            return monitor
        returnCode, outputStr, errStr = monitor["Value"]

        # Returns a non zero status if error
        if returnCode != 0:
            if 'was not found' in outputStr and not errStr:
                errStr = 'Job was not found'
            return S_ERROR(errStr)

        outputStr = outputStr.replace("'", "").replace("<",
                                                       "").replace(">", "")

        # # set FTS job status
        regExp = re.compile("Status:\\s+(\\S+)")

        # with FTS3 this can be uppercase
        self.Status = re.search(regExp, outputStr).group(1)

        statusSummary = {}
        # This is capitalized, even in FTS3!
        for state in FTSFile.ALL_STATES:
            regExp = re.compile("\\s+%s:\\s+(\\d+)" % state)
            if regExp.search(outputStr):
                statusSummary[state] = int(
                    re.search(regExp, outputStr).group(1))

        total = sum(statusSummary.values())
        completed = sum(
            [statusSummary.get(state, 0) for state in FTSFile.FINAL_STATES])
        self.Completeness = 100 * completed / total if total else 0

        if not full:
            return S_OK(statusSummary)

        # The order of informations is not the same for glite- and fts- !!!
        # In order: new fts-, old fts-, glite-
        realJob = len(self) != 0
        iExptr = None
        for iExptr, exptr in enumerate(
            ('[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Staging:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
             '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)\n[ ]+Retries:[ ]+(\\d+)',
             '[ ]+Source:[ ]+(\\S+)\n[ ]+Destination:[ ]+(\\S+)\n[ ]+State:[ ]+(\\S+)\n[ ]+Retries:[ ]+(\\d+)\n[ ]+Reason:[ ]+([\\S ]+).+?[ ]+Duration:[ ]+(\\d+)'
             )):
            regExp = re.compile(exptr, re.S)
            fileInfo = re.findall(regExp, outputStr)
            if fileInfo:
                break
        if not fileInfo:
            return S_ERROR("Error monitoring job (no regexp match)")
        for info in fileInfo:
            if iExptr == 0:
                # version >= 3.2.30
                sourceURL, targetURL, fileStatus, reason, duration, _retries, _staging = info
            elif iExptr == 1:
                # version FTS3 < 3.2.30
                sourceURL, targetURL, fileStatus, reason, duration, _retries = info
            elif iExptr == 2:
                # version FTS2
                sourceURL, targetURL, fileStatus, _retries, reason, duration = info
            else:
                return S_ERROR('Error monitoring job (implement match %d)' %
                               iExptr)
            candidateFile = None

            if not realJob:
                # This is used by the CLI monitoring of jobs in case no file was specified
                candidateFile = FTSFile()
                candidateFile.LFN = overlap(sourceURL, targetURL)
                candidateFile.SourceSURL = sourceURL
                candidateFile.Size = 0
                self += candidateFile
            else:
                for ftsFile in self:
                    if ftsFile.SourceSURL == sourceURL:
                        candidateFile = ftsFile
                        break
                if not candidateFile:
                    continue
            # Can be uppercase for FTS3
            if not candidateFile.TargetSURL:
                candidateFile.TargetSURL = targetURL
            candidateFile.Status = fileStatus
            candidateFile.Error = reason
            candidateFile._duration = duration

            if candidateFile.Status == "Failed":
                for missingSource in self.missingSourceErrors:
                    if missingSource.match(reason):
                        candidateFile.Error = "MissingSource"
            # If the staging info was present, record it
            if len(info) > 6:
                candidateFile._staging = info[6]
        # # register successful files
        if self.Status in FTSJob.FINALSTATES:
            return self.finalize()

        return S_OK()

    def submitFTS3(self, pinTime=False):
        """ submit fts job using FTS3 rest API """

        if self.FTSGUID:
            return S_ERROR("FTSJob already has been submitted")

        transfers = []

        for ftsFile in self:
            trans = fts3.new_transfer(ftsFile.SourceSURL,
                                      ftsFile.TargetSURL,
                                      checksum=ftsFile.Checksum,
                                      filesize=ftsFile.Size)
            transfers.append(trans)

        source_spacetoken = self.SourceToken if self.SourceToken else None
        dest_spacetoken = self.TargetToken if self.TargetToken else None
        copy_pin_lifetime = pinTime if pinTime else None
        bring_online = 86400 if pinTime else None

        job = fts3.new_job(transfers=transfers,
                           overwrite=True,
                           source_spacetoken=source_spacetoken,
                           spacetoken=dest_spacetoken,
                           bring_online=bring_online,
                           copy_pin_lifetime=copy_pin_lifetime,
                           retry=3)

        try:
            context = fts3.Context(self.FTSServer)
            self.FTSGUID = fts3.submit(context, job)

        except Exception, e:
            return S_ERROR("Error at submission: %s" % e)

        self.Status = "Submitted"
        self._log = gLogger.getSubLogger(
            "req_%s/FTSJob-%s" % (self.RequestID, self.FTSGUID), True)
        for ftsFile in self:
            ftsFile.FTSGUID = self.FTSGUID
            ftsFile.Status = "Submitted"
        return S_OK()