Skip to content

Commit f142e9d

Browse files
feat: Change UploadLogFile DataManager Mocks to real DIRAC Classes
Improve UploadLogFile tests
1 parent ef1fd54 commit f142e9d

2 files changed

Lines changed: 304 additions & 103 deletions

File tree

src/dirac_cwl/commands/upload_log_file.py

Lines changed: 120 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -2,67 +2,23 @@
22

33
import glob
44
import os
5+
import random
56
import stat
67
import time
78
import 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

1121
from 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

6824
class 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

Comments
 (0)