Skip to content

Commit 3dc8df3

Browse files
feat: UploadLogFile command implementation
First approach, needs review
1 parent a288bb9 commit 3dc8df3

3 files changed

Lines changed: 224 additions & 1 deletion

File tree

src/dirac_cwl/commands/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Command classes for workflow pre/post-processing operations."""
22

33
from .core import PostProcessCommand, PreProcessCommand
4+
from .upload_log_file import UploadLogFile
45

5-
__all__ = ["PreProcessCommand", "PostProcessCommand"]
6+
__all__ = ["PreProcessCommand", "PostProcessCommand", "UploadLogFile"]
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Post-processing command for uploading logging information to a Storage Element."""
2+
3+
import glob
4+
import os
5+
import stat
6+
import time
7+
import zipfile
8+
9+
from DIRACCommon.Core.Utilities.ReturnValues import S_ERROR, S_OK, returnSingleResult
10+
11+
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
66+
67+
68+
class UploadLogFile(PostProcessCommand):
69+
"""Post-processing command for log file uploading."""
70+
71+
def execute(self, job_path, **kwargs):
72+
"""Execute the log uploading process.
73+
74+
:param job_path: Path to the job working directory.
75+
:param kwargs: Additional keyword arguments.
76+
"""
77+
# Obtain workflow information
78+
output_files = kwargs.get("outputs", None)
79+
job_id = kwargs.get("job_id", None)
80+
production_id = kwargs.get("production_id", None)
81+
namespace = kwargs.get("namespace", None)
82+
config_version = kwargs.get("config_version", None)
83+
84+
if not output_files:
85+
output_files = obtain_output_files(job_path)
86+
87+
if not job_path or not production_id or not namespace or not config_version:
88+
return S_ERROR("Not enough information to perform the log upload")
89+
90+
# Zip files
91+
zip_name = job_id.zfill(8) + ".zip"
92+
zip_path = os.path.join(job_path, zip_name)
93+
zip_files(zip_path, output_files)
94+
95+
# Obtain the log destination
96+
file_lfn = get_zip_lfn(production_id, job_id, namespace, config_version)
97+
98+
# Upload to the SE
99+
dm = DataManager()
100+
result = returnSingleResult(dm.put(file_lfn, zip_path, "LogSE"))
101+
102+
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
108+
return S_ERROR("Failed to upload to FailoverSE")
109+
110+
return S_OK()

test/test_commands.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""."""
2+
3+
import os
4+
import tempfile
5+
6+
import pytest
7+
from DIRACCommon.Core.Utilities.ReturnValues import S_OK
8+
from pytest_mock import MockerFixture
9+
10+
from dirac_cwl_proto.commands import UploadLogFile
11+
12+
13+
class TestUploadLogFile:
14+
"""Collection of tests for the UploadLogFile command."""
15+
16+
FILENAMES = ["file.txt", "file.log", "file.err", "file.out", "file.extra"]
17+
JOB_ID = "8042"
18+
PRODUCTION_ID = "95376"
19+
NAMESPACE = "MC"
20+
CONFIG_VERSION = "2016"
21+
22+
@pytest.fixture
23+
def basedir(self):
24+
"""Fixture to initialize the working directory."""
25+
with tempfile.TemporaryDirectory() as tmpdir:
26+
for file in self.FILENAMES:
27+
with open(os.path.join(tmpdir, file), "x") as f:
28+
f.write("EMPTY")
29+
30+
yield tmpdir
31+
32+
def test_upload_ok(self, basedir, mocker: MockerFixture):
33+
"""Test a correct upload."""
34+
base_lfn = f"/lhcb/{self.NAMESPACE}/{self.CONFIG_VERSION}/LOG/{self.PRODUCTION_ID.zfill(8)}/0000/"
35+
zip_name = self.JOB_ID.zfill(8) + ".zip"
36+
37+
expected_lfn = os.path.join(base_lfn, zip_name)
38+
expected_zip = os.path.join(basedir, zip_name)
39+
40+
mock_put = mocker.patch("dirac_cwl_proto.data_management_mocks.data_manager.MockDataManager.put")
41+
mock_putAndRegister = mocker.patch(
42+
"dirac_cwl_proto.data_management_mocks.data_manager.MockDataManager.putAndRegister",
43+
)
44+
45+
mock_put.return_value = S_OK({"Successful": {expected_lfn: "OKAY"}, "Failed": {}})
46+
47+
result = UploadLogFile().execute(
48+
basedir,
49+
job_id=self.JOB_ID,
50+
production_id=self.PRODUCTION_ID,
51+
namespace=self.NAMESPACE,
52+
config_version=self.CONFIG_VERSION,
53+
)
54+
55+
mock_put.assert_called_once_with(expected_lfn, expected_zip, "LogSE")
56+
mock_putAndRegister.assert_not_called()
57+
assert result["OK"]
58+
59+
def test_upload_ok_to_failover(self, basedir, mocker: MockerFixture):
60+
"""Test a failure to upload to the LogSE but a correct one to the Failover."""
61+
base_lfn = f"/lhcb/{self.NAMESPACE}/{self.CONFIG_VERSION}/LOG/{self.PRODUCTION_ID.zfill(8)}/0000/"
62+
zip_name = self.JOB_ID.zfill(8) + ".zip"
63+
64+
expected_lfn = os.path.join(base_lfn, zip_name)
65+
expected_zip = os.path.join(basedir, zip_name)
66+
67+
mock_put = mocker.patch("dirac_cwl_proto.data_management_mocks.data_manager.MockDataManager.put")
68+
mock_putAndRegister = mocker.patch(
69+
"dirac_cwl_proto.data_management_mocks.data_manager.MockDataManager.putAndRegister",
70+
)
71+
72+
mock_put.return_value = S_OK({"Successful": {}, "Failed": {expected_lfn: "Borked"}})
73+
mock_putAndRegister.return_value = S_OK({"Successful": {expected_lfn: "OKAY"}, "Failed": {}})
74+
75+
result = UploadLogFile().execute(
76+
basedir,
77+
job_id=self.JOB_ID,
78+
production_id=self.PRODUCTION_ID,
79+
namespace=self.NAMESPACE,
80+
config_version=self.CONFIG_VERSION,
81+
)
82+
83+
mock_put.assert_called_once_with(expected_lfn, expected_zip, "LogSE")
84+
mock_putAndRegister.assert_called_once_with(expected_lfn, expected_zip, "Tier1-Failover")
85+
assert result["OK"]
86+
87+
def test_upload_fail(self, basedir, mocker: MockerFixture):
88+
"""Test both a failure to LogSE and the FailoverSE."""
89+
base_lfn = f"/lhcb/{self.NAMESPACE}/{self.CONFIG_VERSION}/LOG/{self.PRODUCTION_ID.zfill(8)}/0000/"
90+
zip_name = self.JOB_ID.zfill(8) + ".zip"
91+
92+
expected_lfn = os.path.join(base_lfn, zip_name)
93+
94+
mock_put = mocker.patch("dirac_cwl_proto.data_management_mocks.data_manager.MockDataManager.put")
95+
mock_putAndRegister = mocker.patch(
96+
"dirac_cwl_proto.data_management_mocks.data_manager.MockDataManager.putAndRegister",
97+
)
98+
99+
mock_put.return_value = S_OK({"Successful": {}, "Failed": {expected_lfn: "Borked"}})
100+
mock_putAndRegister.return_value = S_OK({"Successful": {}, "Failed": {expected_lfn: "Borked"}})
101+
102+
result = UploadLogFile().execute(
103+
basedir,
104+
job_id=self.JOB_ID,
105+
production_id=self.PRODUCTION_ID,
106+
namespace=self.NAMESPACE,
107+
config_version=self.CONFIG_VERSION,
108+
)
109+
110+
assert not result["OK"]
111+
112+
# TO TEST: Failed to zip files - No outputs generated by the job

0 commit comments

Comments
 (0)