22
33import glob
44import os
5+ import random
56import stat
67import time
78import zipfile
8-
9- from DIRACCommon .Core .Utilities .ReturnValues import S_ERROR , S_OK , returnSingleResult
9+ from urllib .parse import urljoin
10+
11+ from DIRAC import S_ERROR , S_OK , siteName
12+ from DIRAC .ConfigurationSystem .Client .Helpers .Operations import Operations
13+ from DIRAC .Core .Utilities .Adler import fileAdler
14+ from DIRAC .Core .Utilities .ReturnValues import returnSingleResult
15+ from DIRAC .DataManagementSystem .Client .FailoverTransfer import FailoverTransfer
16+ from DIRAC .DataManagementSystem .Utilities .ResolveSE import getDestinationSEList
17+ from DIRAC .Resources .Catalog .PoolXMLFile import getGUID
18+ from DIRAC .Resources .Storage .StorageElement import StorageElement
19+ from DIRAC .WorkloadManagementSystem .Client .JobReport import JobReport
1020
1121from dirac_cwl_proto .commands import PostProcessCommand
12- from dirac_cwl_proto .data_management_mocks .data_manager import MockDataManager as DataManager
13-
14-
15- def zip_files (outputFile , files = None , directory = None ):
16- """Zip list of files."""
17- with zipfile .ZipFile (outputFile , "w" ) as zipped :
18- for fileIn in files :
19- # ZIP does not support timestamps before 1980, so for those we simply "touch"
20- st = os .stat (fileIn )
21- mtime = time .localtime (st .st_mtime )
22- dateTime = mtime [0 :6 ]
23- if dateTime [0 ] < 1980 :
24- os .utime (fileIn , None ) # same as "touch"
25-
26- zipped .write (fileIn )
27-
28-
29- def obtain_output_files (job_path ):
30- """Obtain the files to be added to the log zip from the outputs."""
31- log_file_extensions = [
32- "*.txt" ,
33- "*.log" ,
34- "*.out" ,
35- "*.output" ,
36- "*.xml" ,
37- "*.sh" ,
38- "*.info" ,
39- "*.err" ,
40- "prodConf*.py" ,
41- "prodConf*.json" ,
42- ]
43-
44- files = []
45-
46- for extension in log_file_extensions :
47- glob_list = glob .glob (extension , root_dir = job_path , recursive = True )
48- for check in glob_list :
49- path = os .path .join (job_path , check )
50- if os .path .isfile (path ):
51- os .chmod (path , stat .S_IRWXU | stat .S_IRGRP | stat .S_IXGRP | stat .S_IROTH + stat .S_IXOTH )
52- files .append (path )
53-
54- return files
55-
56-
57- def get_zip_lfn (production_id , job_id , namespace , config_version ):
58- """Form a logical file name from certain information from the workflow."""
59- production_id = str (production_id ).zfill (8 )
60- job_id = str (job_id ).zfill (8 )
61- jobindex = str (int (int (job_id ) / 10000 )).zfill (4 )
62-
63- log_path = os .path .join ("/lhcb" , namespace , config_version , "LOG" , production_id , jobindex , "" )
64- file_path = os .path .join (log_path , f"{ job_id } .zip" )
65- return file_path
6622
6723
6824class UploadLogFile (PostProcessCommand ):
@@ -75,36 +31,130 @@ def execute(self, job_path, **kwargs):
7531 :param kwargs: Additional keyword arguments.
7632 """
7733 # Obtain workflow information
78- output_files = kwargs .get ("outputs" , None )
7934 job_id = kwargs .get ("job_id" , None )
8035 production_id = kwargs .get ("production_id" , None )
8136 namespace = kwargs .get ("namespace" , None )
8237 config_version = kwargs .get ("config_version" , None )
8338
84- if not output_files :
85- output_files = obtain_output_files (job_path )
86-
8739 if not job_path or not production_id or not namespace or not config_version :
8840 return S_ERROR ("Not enough information to perform the log upload" )
8941
42+ ops = Operations ()
43+ log_extensions = ops .getValue ("LogFiles/Extensions" , [])
44+ log_se = ops .getValue ("LogStorage/LogSE" , "LogSE" )
45+
46+ job_report = JobReport (job_id )
47+
48+ output_files = self .obtain_output_files (job_path , log_extensions )
49+
50+ if not output_files :
51+ return S_OK ("No files to upload" )
52+
9053 # Zip files
9154 zip_name = job_id .zfill (8 ) + ".zip"
9255 zip_path = os .path .join (job_path , zip_name )
93- zip_files (zip_path , output_files )
56+
57+ try :
58+ self .zip_files (zip_path , output_files )
59+ except (AttributeError , OSError , ValueError ) as e :
60+ job_report .setApplicationStatus ("Failed to create zip of log files" )
61+ return S_OK (f"Failed to zip files: { repr (e )} " )
9462
9563 # Obtain the log destination
96- file_lfn = get_zip_lfn (production_id , job_id , namespace , config_version )
64+ zip_lfn = self . get_zip_lfn (production_id , job_id , namespace , config_version )
9765
9866 # Upload to the SE
99- dm = DataManager ()
100- result = returnSingleResult (dm .put (file_lfn , zip_path , "LogSE" ))
67+ result = returnSingleResult (StorageElement (log_se ).putFile ({zip_lfn : zip_path }))
10168
10269 if not result ["OK" ]: # Failed to uplaod to the LogSE
103- # TODO: "Tier1-Failover" should be a list of SEs and try until either it works or runs out of possible SEs
104- # The list is obtained from getDestinationSEList at ResolveSE.py in DIRAC
105- # The retry is done at transferAndRegisterFile at FailoverTransfer.py in DIRAC
106- result = returnSingleResult (dm .putAndRegister (file_lfn , zip_path , "Tier1-Failover" ))
107- if not result ["OK" ]: # Failed to upload to the Failover SE
70+ result = self .generate_failover_transfer (zip_path , zip_name , zip_lfn )
71+
72+ if not result ["OK" ]:
73+ job_report .setApplicationStatus ("Failed To Upload Logs" )
10874 return S_ERROR ("Failed to upload to FailoverSE" )
10975
110- return S_OK ()
76+ # Set the Log URL parameter
77+ result = returnSingleResult (StorageElement (log_se ).getURL (zip_path , protocol = "https" ))
78+ if not result ["OK" ]:
79+ # The rule for interpreting what is to be deflated can be found in /eos/lhcb/grid/prod/lhcb/logSE/.htaccess
80+ logHttpsURL = urljoin ("https://lhcb-dirac-logse.web.cern.ch/lhcb-dirac-logse/" , zip_lfn )
81+ else :
82+ logHttpsURL = result ["Value" ]
83+ job_report .setJobParameter ("Log URL" , f'<a href="{ logHttpsURL .replace ('.zip' ,'/' )} ">Log file directory</a>' )
84+
85+ return S_OK ("Log Files uploaded" )
86+
87+ def zip_files (self , outputFile , files = None , directory = None ):
88+ """Zip list of files."""
89+ with zipfile .ZipFile (outputFile , "w" ) as zipped :
90+ for fileIn in files :
91+ # ZIP does not support timestamps before 1980, so for those we simply "touch"
92+ st = os .stat (fileIn )
93+ mtime = time .localtime (st .st_mtime )
94+ dateTime = mtime [0 :6 ]
95+ if dateTime [0 ] < 1980 :
96+ os .utime (fileIn , None ) # same as "touch"
97+
98+ zipped .write (fileIn )
99+
100+ def obtain_output_files (self , job_path , extensions = []):
101+ """Obtain the files to be added to the log zip from the outputs."""
102+ log_file_extensions = extensions
103+
104+ if not log_file_extensions :
105+ log_file_extensions = [
106+ "*.txt" ,
107+ "*.log" ,
108+ "*.out" ,
109+ "*.output" ,
110+ "*.xml" ,
111+ "*.sh" ,
112+ "*.info" ,
113+ "*.err" ,
114+ "prodConf*.py" ,
115+ "prodConf*.json" ,
116+ ]
117+
118+ files = []
119+
120+ for extension in log_file_extensions :
121+ glob_list = glob .glob (extension , root_dir = job_path , recursive = True )
122+ for check in glob_list :
123+ path = os .path .join (job_path , check )
124+ if os .path .isfile (path ):
125+ os .chmod (path , stat .S_IRWXU | stat .S_IRGRP | stat .S_IXGRP | stat .S_IROTH + stat .S_IXOTH )
126+ files .append (path )
127+
128+ return files
129+
130+ def get_zip_lfn (self , production_id , job_id , namespace , config_version ):
131+ """Form a logical file name from certain information from the workflow."""
132+ production_id = str (production_id ).zfill (8 )
133+ job_id = str (job_id ).zfill (8 )
134+ jobindex = str (int (int (job_id ) / 10000 )).zfill (4 )
135+
136+ log_path = os .path .join ("/lhcb" , namespace , config_version , "LOG" , production_id , jobindex , "" )
137+ path = os .path .join (log_path , f"{ job_id } .zip" )
138+ return path
139+
140+ def generate_failover_transfer (self , zip_path , zip_name , zip_lfn ):
141+ """Prepare a failover transfer ."""
142+ failoverSEs = getDestinationSEList ("Tier1-Failover" , siteName ())
143+ random .shuffle (failoverSEs )
144+
145+ fileMetaDict = {
146+ "Size" : os .path .getsize (zip_path ),
147+ "LFN" : zip_lfn ,
148+ "GUID" : getGUID (zip_path ),
149+ "Checksum" : fileAdler (zip_path ),
150+ "ChecksumType" : "ADLER32" ,
151+ }
152+
153+ return FailoverTransfer ().transferAndRegisterFile (
154+ fileName = zip_name ,
155+ localPath = zip_path ,
156+ lfn = zip_lfn ,
157+ destinationSEList = failoverSEs ,
158+ fileMetaDict = fileMetaDict ,
159+ masterCatalogOnly = True ,
160+ )
0 commit comments