Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
4b3d8e7
Initial isolate command
hwikle-lanl Nov 25, 2025
845c6a1
Fix get_last_test_id
hwikle-lanl Nov 25, 2025
cd9d3d4
Add isolate to built-in commands
hwikle-lanl Nov 25, 2025
9ec331b
Tweak parser action
hwikle-lanl Nov 25, 2025
41cc611
Add first isolate command unit test
hwikle-lanl Nov 25, 2025
f842155
Ignore series and job directories when isolating
hwikle-lanl Nov 26, 2025
03cd02d
Add argument validation for isolate
hwikle-lanl Nov 26, 2025
ee06027
Add error handling
hwikle-lanl Nov 26, 2025
27e5355
Add unit test for isolating with archives
hwikle-lanl Nov 26, 2025
a7f71f0
Ignore job and series dirs on tarball creation
hwikle-lanl Nov 26, 2025
d91f1d6
Remove stray print function
hwikle-lanl Nov 26, 2025
216cc9f
Handle archive suffixes
hwikle-lanl Nov 26, 2025
6558190
Check for symlinks in isolate command unit tests
hwikle-lanl Nov 26, 2025
6ea4849
Add copytree_resolved function
hwikle-lanl Nov 26, 2025
ffc001e
Fix copytree_resolved
hwikle-lanl Nov 26, 2025
b39160e
Add simple unit test for copytree_resolved
hwikle-lanl Nov 26, 2025
4780b62
Progress towards isolate
hwikle-lanl Dec 1, 2025
42f141d
Add more unit tests for copytree_resolved
hwikle-lanl Dec 1, 2025
3014c29
Remove garbage
hwikle-lanl Dec 1, 2025
e80a6e5
Improve copytree_resolved unit test
hwikle-lanl Dec 1, 2025
da3d3bd
Fix style issue
hwikle-lanl Dec 1, 2025
930243e
Fix copytree_resolve unit tests
hwikle-lanl Dec 1, 2025
a868e94
Fix bad variable names
hwikle-lanl Dec 1, 2025
0bbbea4
Fix more style issues
hwikle-lanl Dec 1, 2025
dc02815
Complete copytree_resolve unit tests
hwikle-lanl Dec 1, 2025
73cef31
Use copytree_resolved in isolate cmd
hwikle-lanl Dec 1, 2025
fd7c389
Fix bad type annotation
hwikle-lanl Dec 1, 2025
5944ca2
Fix ignore_files argument
hwikle-lanl Dec 1, 2025
d6bb72c
Output environment to file
hwikle-lanl Dec 2, 2025
44eb42c
Fix environment file path
hwikle-lanl Dec 2, 2025
04959a2
Fix verbose output in run script
hwikle-lanl Dec 2, 2025
25f4a7d
Write kickoff script to isolated test
hwikle-lanl Dec 2, 2025
84bc915
Fix isolate command class methods
hwikle-lanl Dec 2, 2025
bd4ac92
Fix _isolate method
hwikle-lanl Dec 2, 2025
f4939c3
Fix isolate command ID parsing
hwikle-lanl Dec 2, 2025
94e2153
Fix missing import
hwikle-lanl Dec 2, 2025
f398fa1
Temporarily un-ignore job directory
hwikle-lanl Dec 2, 2025
8b7965e
Fix missing comma
hwikle-lanl Dec 2, 2025
95ed873
Fix scheduler node min and max
hwikle-lanl Dec 2, 2025
3ec0bb0
Remove flatten option from copytree_resolved
hwikle-lanl Dec 2, 2025
6fd4d90
Fix isolate kickoff script
hwikle-lanl Dec 2, 2025
ddd7411
Fix isolate command job name
hwikle-lanl Dec 2, 2025
8470e5b
Add run script to kickoff for pav isolate
hwikle-lanl Dec 2, 2025
2ee5194
Isolate run script
hwikle-lanl Dec 2, 2025
bdf08b9
Fix missing import
hwikle-lanl Dec 2, 2025
2f54c8e
Add newlines to build/run script
hwikle-lanl Dec 2, 2025
3ab3ec8
Fix ignore_files in copytree_resolved
hwikle-lanl Dec 2, 2025
a1c0a18
Fix missing import
hwikle-lanl Dec 2, 2025
fbb18dc
Fix missing import
hwikle-lanl Dec 2, 2025
6132c03
Fix copytree_resolved symlink behavior
hwikle-lanl Dec 2, 2025
6e7cff3
Fix syntax error
hwikle-lanl Dec 2, 2025
0cb5534
Fix copytree_resolved
hwikle-lanl Dec 2, 2025
589da50
Add more examples to copytree_resolved unit test
hwikle-lanl Dec 2, 2025
a4ec4e9
Fix copytree_resolved unit tests
hwikle-lanl Dec 2, 2025
cfda806
Fix copytree_resolved
hwikle-lanl Dec 3, 2025
96f306d
Use relative paths when creating symlinks
hwikle-lanl Dec 3, 2025
51f969e
Style
hwikle-lanl Dec 3, 2025
9f7fec5
Remove unnecessary return value
hwikle-lanl Dec 3, 2025
5a2cd42
Refine isolate command unit tests
hwikle-lanl Dec 3, 2025
bbda33d
Fix isolate command unit tests
hwikle-lanl Dec 3, 2025
bf05bc8
Add docstrings to unit tests
hwikle-lanl Dec 3, 2025
c0dc04c
Add docstrings to isolate command
hwikle-lanl Dec 3, 2025
6806f3a
Incorporate node_range into kickoff.isolated script
hwikle-lanl Dec 3, 2025
5802b24
Fix pav-lib.bash path
hwikle-lanl Dec 3, 2025
6d3ecad
Remove accidents
hwikle-lanl Dec 3, 2025
99241a1
Fix style issue
hwikle-lanl Dec 3, 2025
ce3c112
Add create_kickoff_script method to scheduler class
hwikle-lanl Dec 3, 2025
64f6c6d
Fix style issues
hwikle-lanl Dec 3, 2025
de64971
Pass some unit tests
hwikle-lanl Dec 3, 2025
1bf874f
Pass logging unit test
hwikle-lanl Dec 3, 2025
e22d598
Tweak written scripts slightly
hwikle-lanl Dec 3, 2025
e33af18
Remove pavilion environment variables from kickoff script when isolating
hwikle-lanl Dec 3, 2025
9901811
Refine isolate command unit tests
hwikle-lanl Dec 3, 2025
629bd6e
Fix isolate command unit tests
hwikle-lanl Dec 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions lib/pavilion/cmd_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import logging
import sys
import time
import os
from pathlib import Path
from typing import List, TextIO, Union, Iterator, Optional
from collections import defaultdict
Expand Down Expand Up @@ -523,18 +524,31 @@ def get_last_test_id(pav_cfg: "PavConfig", errfile: TextIO) -> Optional[TestID]:
if last_series is None:
return None

test_ids = list(last_series.tests.keys())
id_pairs = list(last_series.tests.keys())

if len(test_ids) == 0:
if len(id_pairs) == 0:
output.fprint(
errfile,
f"Most recent series contains no tests.")
return None

if len(test_ids) > 1:
if len(id_pairs) > 1:
output.fprint(
errfile,
f"Multiple tests exist in last series. Could not unambiguously identify last test.")
return None

return TestID(test_ids[0])
return TestID(str(id_pairs[0][1]))


def list_files(path: Path, include_root: bool = False) -> Iterator[Path]:
"""Recursively list all files in a directory, optionally including the directory itself."""

for root, dirs, files in os.walk(path):
if include_root:
yield Path(root)

for fname in files:
yield Path(root) / fname
for dname in dirs:
yield Path(root) / dname
1 change: 1 addition & 0 deletions lib/pavilion/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
'config': ('config', 'ConfigCommand'),
'graph': ('graph', 'GraphCommand'),
'group': ('group', 'GroupCommand'),
'isolate': ('isolate', 'IsolateCommand'),
'list': ('list_cmd', 'ListCommand'),
'log': ('log', 'LogCommand'),
'ls': ('ls', 'LSCommand'),
Expand Down
216 changes: 216 additions & 0 deletions lib/pavilion/commands/isolate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
from argparse import ArgumentParser, Namespace, Action
from pathlib import Path
import tarfile
import sys
import shutil
import tempfile
from typing import Iterable

from pavilion import output
from pavilion import schedulers
from pavilion.config import PavConfig
from pavilion.test_run import TestRun
from pavilion.test_ids import TestID
from pavilion.cmd_utils import get_last_test_id, get_tests_by_id, list_files
from pavilion.utils import copytree_resolved
from pavilion.scriptcomposer import ScriptComposer
from pavilion.errors import SchedulerPluginError
from pavilion.schedulers.config import validate_config, calc_node_range
from .base_classes import Command


class IsolateCommand(Command):
"""Isolates an existing test run in a form that can be run without Pavilion."""

IGNORE_FILES = ("series", "job")
KICKOFF_FN = "kickoff.isolated"

def __init__(self):
super().__init__(
"isolate",
"Isolate an existing test run.",
short_help="Isolate a test run."
)

def _setup_arguments(self, parser: ArgumentParser) -> None:
"""Setup the argument parser for the isolate command."""

parser.add_argument(
"test_id",
type=TestID,
nargs="?",
help="test ID"
)

parser.add_argument(
"path",
type=Path,
help="isolation path"
)

parser.add_argument(
"-a",
"--archive",
action="store_true",
default=False,
help="archive the test"
)

parser.add_argument(
"-z",
"--zip",
default=False,
help="compress the test archive",
action="store_true"
)

def run(self, pav_cfg: PavConfig, args: Namespace) -> int:
"""Run the isolate command."""

if args.zip and not args.archive:
output.fprint(self.errfile, "--archive must be specified to use --zip.")

return 1

test_id = args.test_id

if args.test_id is None:
test_id = get_last_test_id(pav_cfg, self.errfile)

if test_id is None:
output.fprint(self.errfile, "No last test found.", color=output.RED)

return 2

tests = get_tests_by_id(pav_cfg, [test_id], self.errfile)

if len(tests) == 0:
output.fprint(self.errfile, "Could not find test '{}'".format(test_id))

return 3

elif len(tests) > 1:
output.fprint(
self.errfile, "Matched multiple tests. Printing file contents for first "
"test only (test {})".format(tests[0].full_id),
color=output.YELLOW)

return 4

test = next(iter(tests))

return self._isolate(pav_cfg, test, args.path, args.archive, args.zip)

@classmethod
def _isolate(cls, pav_cfg: PavConfig, test: TestRun, dest: Path, archive: bool,
zip: bool) -> int:
"""Given a test run and a destination path, isolate that test run, optionally
creating a tarball."""

if not test.path.is_dir():
output.fprint(sys.stderr, "Directory '{}' does not exist."
.format(test.path.as_posix()), color=output.RED)

return 5

if dest.exists():
output.fprint(
sys.stderr,
f"Unable to isolate test {test.id}. Destination {dest} already exists.",
color=output.RED)

return 6

if archive:
cls._write_tarball(pav_cfg,
test,
dest,
zip,
cls.IGNORE_FILES)

else:
try:
copytree_resolved(test.path, dest, ignore_files=cls.IGNORE_FILES)
except OSError as err:
output.fprint(
sys.stderr,
f"Unable to isolate test {test.id} at {dest}: {err}",
color=output.RED)

return 8

pav_lib_bash = pav_cfg.pav_root / 'bin' / TestRun.PAV_LIB_FN
shutil.copyfile(pav_lib_bash, dest / TestRun.PAV_LIB_FN)

cls._write_kickoff_script(pav_cfg, test, dest / cls.KICKOFF_FN)

return 0

@classmethod
def _write_tarball(cls, pav_cfg: PavConfig, test: TestRun, dest: Path, zip: bool,
ignore_files: Iterable[str]) -> None:
"""Given a test run object, create a tarball of its run directory in the specified
location."""

if zip:
if len(dest.suffixes) == 0:
dest = dest.with_suffix(".tgz")

modestr = "w:gz"
else:
if len(dest.suffixes) == 0:
dest = dest.with_suffix(".tar")

modestr = "w:"

with tempfile.TemporaryDirectory() as tmp:
tmp = Path(tmp)
tmp_dest = tmp / dest.stem
tmp_dest.mkdir()
copytree_resolved(test.path, tmp_dest, ignore_files=ignore_files)

# Copy Pavilion bash library into tarball
pav_lib_bash = pav_cfg.pav_root / 'bin' / TestRun.PAV_LIB_FN
shutil.copyfile(pav_lib_bash, tmp_dest / TestRun.PAV_LIB_FN)

cls._write_kickoff_script(pav_cfg, test, tmp_dest / cls.KICKOFF_FN)

try:
with tarfile.open(dest, modestr) as tarf:
for fname in list_files(tmp):
tarf.add(
fname,
arcname=fname.relative_to(tmp),
recursive=False)
except (tarfile.TarError, OSError):
output.fprint(
sys.stderr,
f"Unable to isolate test {test.id} at {dest}.",
color=output.RED)

return 7

@classmethod
def _write_kickoff_script(cls, pav_cfg: PavConfig, test: TestRun, script_path: Path) -> None:
"""Write a special kickoff script that can be used to run the given test independently of
Pavilion."""

try:
sched = schedulers.get_plugin(test.scheduler)
except SchedulerPluginError:
output.fprint(
sys.stderr,
f"Unable to generate kickoff script for test {test_id}: unable to load scheduler"
f" {test.scheduler}."
)
return 9

sched_config = validate_config(test.config['schedule'])
node_range = calc_node_range(sched_config, sched_config['cluster_info']['node_count'])

script = sched.create_kickoff_script(
pav_cfg,
test,
isolate=True)

script.write(script_path)
Loading