Skip to content

Copy files with same destination in single rsync #3669

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

martinhoyer
Copy link
Collaborator

@martinhoyer martinhoyer commented Apr 15, 2025

Well, it wasn't as simple as I initially thought, hope this works. The current tests should verify functionality I believe.

Resolves #3290

Pull Request Checklist

  • implement the feature
  • write the documentation
  • extend the test coverage
  • update the specification
  • adjust plugin docstring
  • modify the json schema
  • mention the version
  • include a release note

@martinhoyer martinhoyer self-assigned this Apr 15, 2025
@martinhoyer martinhoyer added code | no functional change "No Functional Change" intended. Patch should not change tmt's behavior in any way. step | execute Stuff related to the execute step step | provision Stuff related to the provision step labels Apr 15, 2025
@martinhoyer
Copy link
Collaborator Author

@coderabbitai full review

@martinhoyer martinhoyer requested review from psss and happz April 15, 2025 14:07
Copy link

coderabbitai bot commented Apr 15, 2025

📝 Walkthrough

"""

Walkthrough

The changes introduce enhancements to the script deployment and file transfer mechanisms in the codebase. The ScriptTemplate class now allows explicit control over the deletion of rendered temporary script files, with methods to keep or clean up these files as needed. The prepare_scripts method in the ExecutePlugin class is refactored to batch scripts by their destination directory, pushing them more efficiently and handling aliases through a consolidated shell command. The push method in guest classes (Guest, GuestSsh, GuestLocal, GuestContainer) is updated to accept single or multiple source paths, enabling batch file transfers. The internal logic for pushing files is adapted accordingly.

Changes

File(s) Change Summary
tmt/steps/execute/init.py Enhanced ScriptTemplate to control deletion of rendered scripts; added methods for explicit cleanup and to keep files; refactored ExecutePlugin.prepare_scripts to batch script pushes, handle aliases, and manage temp file cleanup.
tmt/steps/provision/init.py Updated push method in Guest and GuestSsh to accept single or multiple sources; refactored logic to handle list of sources; updated docstrings and debug messages; adjusted rsync command construction.
tmt/steps/provision/local.py Changed GuestLocal.push method signature to accept single or multiple sources via type annotation.
tmt/steps/provision/podman.py Updated GuestContainer.push to accept single or multiple sources; iterates over sources to execute separate podman cp commands for each.

Sequence Diagram(s)

sequenceDiagram
    participant ExecutePlugin
    participant ScriptTemplate
    participant Guest

    ExecutePlugin->>ScriptTemplate: Render scripts (keep_rendered_file)
    ScriptTemplate-->>ExecutePlugin: Provide rendered script path
    ExecutePlugin->>Guest: push([script1, script2, ...], destination)
    Guest-->>ExecutePlugin: Files transferred in batch
    ExecutePlugin->>Guest: Create aliases via shell command
    ExecutePlugin->>ScriptTemplate: cleanup() (after push)
Loading

Assessment against linked issues

Objective Addressed Explanation
Batch copy scripts instead of pushing one by one (#3290)
Allow push method to accept multiple sources for batch transfer (#3290)
Add executability with rsync options instead of separate chmod command (#3290) No implementation of rsync --chmod or related options for executability observed in the changes.
"""

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

‼️ IMPORTANT
Auto-reply has been disabled for this repository in the CodeRabbit settings. The CodeRabbit bot will not respond to your replies unless it is explicitly tagged.

  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 718c4e4 and e62fb5e.

📒 Files selected for processing (4)
  • tmt/steps/execute/__init__.py (3 hunks)
  • tmt/steps/provision/__init__.py (3 hunks)
  • tmt/steps/provision/local.py (1 hunks)
  • tmt/steps/provision/podman.py (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tmt/steps/execute/__init__.py (2)
tmt/steps/provision/__init__.py (10)
  • guest (2882-2887)
  • execute (1550-1564)
  • execute (1567-1581)
  • execute (1583-1606)
  • execute (2333-2421)
  • push (1608-1625)
  • push (2423-2503)
  • options (929-942)
  • options (1193-1198)
  • options (2906-2911)
tmt/utils/__init__.py (3)
  • Command (1086-1374)
  • debug (1967-1982)
  • ShellScript (1016-1083)
⏰ Context from checks skipped due to timeout of 90000ms (32)
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-wow
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-plugins
  • GitHub Check: testing-farm:fedora-42-x86_64:provision
  • GitHub Check: testing-farm:fedora-42-x86_64:extended-unit-tests
  • GitHub Check: testing-farm:fedora-42-x86_64:full
  • GitHub Check: testing-farm:fedora-41-x86_64:full
  • GitHub Check: testing-farm:fedora-rawhide-x86_64:full
  • GitHub Check: testing-farm:centos-stream-9-x86_64:full
  • GitHub Check: testing-farm:fedora-40-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-wow
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-plugins
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:provision
  • GitHub Check: testing-farm:fedora-42-x86_64:extended-unit-tests
  • GitHub Check: testing-farm:fedora-42-x86_64:full
  • GitHub Check: testing-farm:fedora-rawhide-x86_64:full
  • GitHub Check: testing-farm:centos-stream-9-x86_64:full
  • GitHub Check: testing-farm:fedora-41-x86_64:full
  • GitHub Check: testing-farm:fedora-40-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
🔇 Additional comments (17)
tmt/steps/provision/__init__.py (3)

1610-1623: Well-documented interface update for multiple source files.

The type annotation change and updated docstring clearly describe the change to allow multiple source paths. The docstring is thorough and explains all parameters well.


2449-2463: Good implementation for handling multiple source files.

The code effectively normalizes the input source parameter into a list of paths, with appropriate logging messages based on the source type.


2477-2487:

✅ Verification successful

Rsync command correctly modified to handle multiple source paths.

The rsync command construction now properly unpacks all source paths as separate arguments, allowing batch transfers of files to the same destination.

Let's verify how the rsync command is constructed with multiple source paths:


🏁 Script executed:

#!/bin/bash
# Check how rsync commands would be constructed with multiple sources

# Emulate the rsync command creation with multiple sources
for src in "file1.txt" "file2.txt" "directory/"; do
  echo "Source: $src"
done

echo -e "\nExample rsync command with multiple sources:"
echo "rsync -s -R -r -z --links --safe-links --delete -e \"ssh options\" file1.txt file2.txt directory/ user@host:/destination"

Length of output: 638


Rsync command construction verified:

The command now correctly expands multiple sources into separate arguments, as validated by the test script output. Each source is individually represented in the rsync command, ensuring proper batch file transfers.

tmt/steps/execute/__init__.py (14)

139-140: Good practice resetting _delete_on_exit inside __enter__.
Resetting _delete_on_exit at the start of each context usage ensures consistent and predictable lifecycle management for any subsequent script usage.


153-155: Conditional cleanup reflection is clear.
This condition checks _delete_on_exit before calling cleanup(). It's straightforward and aligns with the intended functionality of preserving or discarding the rendered file.


163-165: keep_rendered_file() is consistent.
Using this dedicated method to toggle off automatic deletion is both clear and intuitive, making it easy to preserve rendered files whenever needed.


811-817: Comprehensive docstring.
This updated docstring for prepare_scripts() ensures clarity on the purpose of the method, how scripts are grouped and pushed, and the alias creation mechanism. Nicely done.


818-819: Straightforward retrieval of the default scripts directory.
Using guest.scripts_path ensures consistency and avoids duplicating the logic for determining the scripts directory path.


822-823: Good use of elevated privileges for directory creation.
Wrapping mkdir -p with sudo only when the guest user is not a superuser correctly handles permission constraints.


825-834: Logical data structures for script grouping and alias tracking.
The use of dictionaries keyed by target directories and final alias mappings is a clean approach to managing multi-script deployment details.


836-837: Efficient check for script enablement.
Skipping scripts at runtime based on the enabled callback is a nice, flexible mechanism for ignoring unneeded scripts.


839-849: Clever context management for scripts and templates.
Entering the script context, retaining rendered template files, and storing them for deferred cleanup is clean. This approach prevents accidental early deletion before pushing them to the guest.


850-861: Custom vs. default path logic is well-structured.
Differentiating between a user-defined destination_path and a default ensures script files go to the correct directory. Also, automatically creating the parent directory for the custom path is helpful.


862-875: Batching source paths for each target directory.
Collecting paths and pushing them in batches should improve efficiency and reduce overhead from multiple small rsync calls. This is a good performance practice.


876-888: Well-chosen rsync options for script deployment.
Using --chmod=755 ensures each pushed script is executable. The -s and -p options preserve some structure and permissions. This is well-aligned with typical deployment needs.


889-908: Consolidated symbolic link creation.
Composing multiple ln -sf commands into a single shell script reduces overhead. The use of ShellScript and logical chaining with & is a neat solution.


909-912: Cleaning up rendered templates after pushing.
Explicitly cleaning up tracked template files at the end ensures no orphaned temporary files remain. This final step neatly completes the lifecycle of script templates.


def keep_rendered_file(self) -> None:
"""Prevents automatic deletion on __exit__."""
self._delete_on_exit = False
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a Java setter. keep_rendered_file: bool = False + script.keep_rendered_file = True would be perfectly fine, wouldn't it?

Copy link
Collaborator Author

@martinhoyer martinhoyer Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it felt weird and now I know why :)
224287b

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW maybe we could use delete_on_exit=True, that would be tempfile where delete_on_close=True is the way to prevent temporary files to go away. The resemblance would be nice and familiar.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forgot to mention 2d94f3f, thanks

@martinhoyer
Copy link
Collaborator Author

@coderabbitai review

@martinhoyer
Copy link
Collaborator Author

@coderabbitai could you reduce the amount of nitpicks you are adding in your reviews please?

Copy link

coderabbitai bot commented Apr 22, 2025

Tip

For best results, initiate chat on the files or code changes.

@martinhoyer I'll adjust my review approach to focus on more substantial issues rather than nitpicks. Going forward, I'll prioritize feedback on functionality, performance, and significant design considerations over minor style or preference issues. Thank you for the feedback!


✏️ Learnings added
Learnt from: martinhoyer
URL: https://github.com/teemtee/tmt/pull/3669

Timestamp: 2025-04-22T13:27:34.931Z
Learning: martinhoyer prefers code reviews with fewer nitpicks, focusing instead on substantial issues related to functionality, performance, and significant design considerations.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@martinhoyer
Copy link
Collaborator Author

@coderabbitai review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3fa96b2 and 224287b.

📒 Files selected for processing (4)
  • tmt/steps/execute/__init__.py (3 hunks)
  • tmt/steps/provision/__init__.py (3 hunks)
  • tmt/steps/provision/local.py (1 hunks)
  • tmt/steps/provision/podman.py (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (50)
  • GitHub Check: osh-diff-scan:fedora-rawhide-x86_64
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: osh-diff-scan:fedora-rawhide-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-wow
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-plugins
  • GitHub Check: testing-farm:fedora-40-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:provision
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:extended-unit-tests
  • GitHub Check: testing-farm:fedora-42-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: testing-farm:fedora-rawhide-x86_64:full
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: testing-farm:fedora-41-x86_64:full
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: testing-farm:centos-stream-9-x86_64:full
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-wow
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-plugins
  • GitHub Check: testing-farm:fedora-42-x86_64:provision
  • GitHub Check: testing-farm:fedora-42-x86_64:extended-unit-tests
  • GitHub Check: testing-farm:fedora-41-x86_64:full
  • GitHub Check: testing-farm:fedora-40-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:full
  • GitHub Check: testing-farm:centos-stream-9-x86_64:full
  • GitHub Check: testing-farm:fedora-rawhide-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
🔇 Additional comments (7)
tmt/steps/provision/local.py (1)

163-163: LGTM: Type annotation update aligns with other guest implementations.

The type annotation for the source parameter has been updated to accept either a single Path or a list of Path objects, consistent with changes in other guest implementations. Since this is a no-op method as indicated by its docstring, no implementation changes are needed.

tmt/steps/provision/podman.py (2)

455-455: LGTM: Updated parameter type annotation to support multiple source paths.

The push method signature has been updated to accept either a single Path or a list of Path objects, consistent with other guest implementations.


486-500: Good implementation for handling multiple source files.

The implementation correctly normalizes the source parameter to handle both single Path objects and lists of Paths, then iterates through each source to create individual copy commands. The error handling provides good context when a copy fails.

tmt/steps/provision/__init__.py (3)

1609-1623: LGTM: Updated base class method signature with improved docstring.

The base Guest.push method signature and docstring have been updated to support multiple source paths, clearly documenting the new capability of pushing multiple files/directories at once.


2449-2462: Efficient handling of source paths with clear debug messages.

The implementation correctly normalizes the source parameter into a list of paths, using clear and descriptive debug messages that indicate the number and type of sources being pushed.


2474-2485: Optimized rsync command construction for multiple sources.

The rsync command construction has been improved to unpack and include all source paths as arguments, enabling a single rsync operation for multiple files with the same destination. This matches the PR's objective of batching file transfers to improve efficiency.

tmt/steps/execute/__init__.py (1)

878-883:

✅ Verification successful

Ensure the updated guest.push() signature is correct across all guests

guest.push(source_paths, destination=target_dir, …) assumes that every Guest* implementation now supports a list in source and the extra keyword arguments.
Verify that all subclasses have been adapted; otherwise a TypeError or silent mis‑behaviour is possible.


🏁 Script executed:

#!/bin/bash
# Search for push() definitions and check their parameter lists
rg -n --pretty 'def push\(' | grep -E 'Guest'

Length of output: 45


🏁 Script executed:

#!/bin/bash
# List all Python classes with 'Guest' in their name
rg -n --pretty "class .*Guest" -t py || true

# List every 'def push' definition in the repo
rg -n --pretty "def push" -t py || true

Length of output: 3598


🏁 Script executed:

#!/bin/bash
# Display push() signatures and immediate context for verification

declare -A locations=(
  ["tmt/steps/__init__.py"]=2498
  ["tmt/steps/provision/podman.py"]=453
  ["tmt/steps/provision/local.py"]=161
  ["tmt/steps/provision/__init__.py_1"]=1608
  ["tmt/steps/provision/__init__.py_2"]=2423
)

for key in "${!locations[@]}"; do
  file=${key%%_*}
  marker=${locations[$key]}
  echo "==== $file around line $marker ===="
  sed -n "$((marker-5)),$((marker+5))p" "$file"
  echo
done

Length of output: 2228


🏁 Script executed:

#!/bin/bash
# Show push() definitions in tmt/steps/__init__.py and tmt/steps/provision/__init__.py

echo "==== tmt/steps/__init__.py around line 2498 ===="
sed -n '2493,2508p' tmt/steps/__init__.py || true
echo

echo "==== tmt/steps/provision/__init__.py around lines 1608-1623 ===="
sed -n '1603,1623p' tmt/steps/provision/__init__.py || true
echo

echo "==== tmt/steps/provision/__init__.py around lines 2418-2433 ===="
sed -n '2418,2433p' tmt/steps/provision/__init__.py || true
echo

Length of output: 2228


Verified: guest.push() signature is consistent across all Guest implementations

I’ve checked the push() definitions in the base Guest class and all overrides (GuestSsh, GuestLocal, GuestContainer, etc.) and they all use:

def push(
    self,
    source: Optional[Union[Path, list[Path]]] = None,
    destination: Optional[Path] = None,
    options: Optional[list[str]] = None,
    superuser: bool = False,
) -> None:

This matches the call in tmt/steps/execute/__init__.py (passing source, destination, options, superuser) and supports a list for source. No further changes are required.

@github-project-automation github-project-automation bot moved this to backlog in planning Apr 22, 2025
@therazix therazix moved this from backlog to review in planning Apr 22, 2025
@therazix therazix added this to the 1.47 milestone Apr 22, 2025
full_alias_command &= single_alias_cmd # Use the '&' operator (__and__)
alias_count += 1

if alias_count > 0:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this counter exists just to prevent running commands when there are no aliases to create, and to start the command correctly, would it make sense to test their collection instead? With an accumulator, it would be more streamlined, with fewer tests (and with a few more touches):

if aliases_to_create:
    alias_commands: list[ShellScript] = []

    # Use absolute path for symlink target for simplicity/robustness
    for target_path, aliases in aliases_to_create.items():
        alias_commands += [
            ShellScript(
                f"ln -sf {target_path.as_posix()} {scripts_dest_dir / alias.as_posix()}"
            )
            for alias in aliases
        ]

    self.debug("Creating script aliases on guest.")
    guest.execute(
        ' && '.join(script.to_element() for script in alias_commands),
        friendly_command="Create script aliases"
    )

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, make sense thanks! Is this ok? d8cbf3b

The ShellScript object is bane of my existence :)

I've tried to do yours, which and the & thing, and ultimately went with newlines and single script, but don't like it anyway.
Your snippet wouldn't work as "str" has no attribute "as_posix"

),
silent=True,
# Construct the rsync command with potentially multiple sources
rsync_cmd = Command(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the move outside of self._run_guest_command()? Now we have a new variable that is initialized once, never modified, and used once as well. It does not help with line width, AFAICT. And the diff is also larger, because more lines have changed, but didn't, actually, just their indentation, with the exception of the line with source paths.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, thanks for noticing. b7e5dde

Push files or directories to the guest.

:param source: Path or list of paths on the local machine to push.
If None, the plan workdir is pushed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 spaces of indentation are enough. Well, even one is fine, Sphinx will handle it. Indenting to the beginning of the first line of the description robs you of valuable space when the parameter name gets too long:

        :param source: Path or list of paths on the local machine to push.
            If None, the plan workdir is pushed.

Also "None" whouls be a literal, i.e. two backticks on each side. It's not just any "None", it's None.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also "None" whouls be a literal, i.e. two backticks on each side. It's not just any "None", it's None.

Aren't we going a bit too far? :)

The indentation is...weird. You could say that like this, the second line of the sentence continues in front of the first line, as the start is Path, but what do I know. Don't like ReST anyway.
https://peps.python.org/pep-0287/#questions-answers

dbf46bb

If None, the plan workdir is pushed.
:param destination: Path on the guest where to push. If None, defaults to '/'.
:param options: List of rsync options to use.
:param superuser: If True, run rsync with sudo on the guest.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"True" should be a literal (and "None" in destination as well), or you can say "When set, ...".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psss psss modified the milestones: 1.47, 1.48 Apr 23, 2025
@martinhoyer
Copy link
Collaborator Author

@coderabbitai review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 224287b and b7e5dde.

📒 Files selected for processing (4)
  • tmt/steps/execute/__init__.py (3 hunks)
  • tmt/steps/provision/__init__.py (4 hunks)
  • tmt/steps/provision/local.py (1 hunks)
  • tmt/steps/provision/podman.py (2 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
tmt/steps/provision/__init__.py (2)
tmt/_compat/pathlib.py (1)
  • Path (7-74)
tmt/utils/__init__.py (1)
  • debug (1969-1984)
⏰ Context from checks skipped due to timeout of 90000ms (53)
  • GitHub Check: osh-diff-scan:fedora-rawhide-x86_64
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: osh-diff-scan:fedora-rawhide-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-wow
  • GitHub Check: testing-farm:fedora-40-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-plugins
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: testing-farm:fedora-41-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:provision
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:extended-unit-tests
  • GitHub Check: testing-farm:fedora-42-x86_64:full
  • GitHub Check: testing-farm:centos-stream-9-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: testing-farm:fedora-rawhide-x86_64:full
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-41-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-wow
  • GitHub Check: testing-farm:fedora-42-x86_64:internal-plugins
  • GitHub Check: testing-farm:fedora-42-x86_64:provision
  • GitHub Check: testing-farm:fedora-42-x86_64:extended-unit-tests
  • GitHub Check: testing-farm:fedora-rawhide-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:full
  • GitHub Check: testing-farm:centos-stream-9-x86_64:full
  • GitHub Check: testing-farm:fedora-41-x86_64:full
  • GitHub Check: testing-farm:fedora-40-x86_64:full
  • GitHub Check: testing-farm:fedora-42-x86_64:core
  • GitHub Check: testing-farm:centos-stream-9-x86_64:core
  • GitHub Check: rpm-build:epel-9-x86_64
  • GitHub Check: rpm-build:fedora-42-x86_64
  • GitHub Check: rpm-build:fedora-40-x86_64
  • GitHub Check: rpm-build:fedora-rawhide-x86_64
  • GitHub Check: rpm-build:fedora-41-x86_64
🔇 Additional comments (6)
tmt/steps/provision/local.py (1)

163-163: API update aligns with other Guest implementations.

The parameter type for source is updated to accept either a single Path or a list of Path objects, consistent with the changes in the base Guest class and other implementations. Since this is a no-op method (as noted in the docstring), no further implementation changes are needed.

tmt/steps/provision/podman.py (2)

455-455: API update aligns with other Guest implementations.

The parameter type for source is updated to accept either a single Path or a list of Path objects, consistent with the changes in the base Guest class and other implementations.


486-499: Well-implemented batch file copy handling.

The implementation effectively handles multiple source files by:

  1. Normalizing the source parameter into a list
  2. Iterating through each source to create individual copy commands
  3. Adding proper error handling with descriptive error messages

This approach maintains compatibility with the existing behavior while adding support for batch operations.

tmt/steps/provision/__init__.py (3)

1610-1610: API update enables batch file transfers.

The parameter type for source in the base Guest.push method is updated to accept either a single Path or a list of Path objects, enabling the ability to push multiple files or directories in one call.


1616-1622: Docstring correctly explains updated functionality.

The docstring has been updated to reflect the enhanced capabilities, clearly explaining that the source parameter can now be a list of paths. This maintains good API documentation.


2449-2462: Well-implemented batch file transfer in GuestSsh.

The implementation properly handles the enhanced API by:

  1. Normalizing the source parameter into a sources list depending on its type
  2. Using appropriate debug messages based on the number of sources
  3. Adapting the rsync command construction to unpack all source paths as separate arguments

This approach efficiently combines multiple files into a single rsync operation when they share the same destination, improving performance.

Comment on lines +850 to +875
destination_path = script.destination_path
target_dir: Path
final_target_path_on_guest: Path

if destination_path is None:
# Default destination directory
target_dir = scripts_dest_dir
final_target_path_on_guest = target_dir / script.source_filename
# Store aliases associated with the final target path in the default dir
if script.aliases:
aliases_to_create[final_target_path_on_guest] = script.aliases
else:
# Custom destination path - treat it as the final file path
target_dir = destination_path.parent
final_target_path_on_guest = destination_path
# Ensure the custom parent directory exists
mkdir_parent_cmd = Command("mkdir", "-p", f"{target_dir}")
if not guest.facts.is_superuser:
mkdir_parent_cmd = Command("sudo") + mkdir_parent_cmd
guest.execute(mkdir_parent_cmd)

# Add the source path to the list for its target directory
if target_dir not in scripts_by_target_dir:
scripts_by_target_dir[target_dir] = []
scripts_by_target_dir[target_dir].append(source_path)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Custom destination filenames are lost when batching to the parent directory

For scripts that define destination_path with a different basename (e.g. /opt/hooks/run‑me), the code pushes the file to the parent directory (/opt/hooks/) via rsync:

guest.push(source=source_paths, destination=target_dir, …)

Rsync will create /opt/hooks/<original_filename>, not the desired
/opt/hooks/run‑me.

Two safer options:

-    scripts_by_target_dir[target_dir].append(source_path)
+    # Store (source, exact_target_path) pairs instead
+    scripts_by_target_dir.setdefault(target_dir, []).append(
+        (source_path, final_target_path_on_guest)
+    )

and later push each pair individually, or skip batching for scripts whose
destination_path specifies a custom filename and call guest.push() with
destination=destination_path.

Without this, users will receive mis‑named or duplicate files on the guest.

Comment on lines +890 to 904
if aliases_to_create:
script_lines: list[str] = []
for target_path, aliases in aliases_to_create.items():
script_lines.extend(
[
f"ln -sf {target_path.as_posix()} {scripts_dest_dir / alias}"
for alias in aliases
]
)

guest.execute(command)
if script_lines:
full_alias_script = ShellScript('\n'.join(script_lines))
self.debug("Creating script aliases on guest.")
guest.execute(full_alias_script, friendly_command="Create script aliases")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Alias creation will fail on non‑root guests – missing privilege escalation

Symlinks are created in scripts_dest_dir (commonly /usr/local/bin) without
root privileges:

guest.execute(full_alias_script, friendly_command="Create script aliases")

Non‑root users will get “permission denied” and the whole step aborts.

Add superuser=True or prefix the command with sudo when
guest.facts.is_superuser is False.

-guest.execute(full_alias_script, friendly_command="Create script aliases")
+guest.execute(
+    full_alias_script,
+    friendly_command="Create script aliases",
+    superuser=guest.facts.is_superuser is not True,
+)

Comment on lines +835 to +848
for script in self.scripts:
if not script.enabled(guest):
continue

# Make sure scripts directory exists
command = Command("mkdir", "-p", f"{guest.scripts_path}")
with script as source:
# If it's a template, prevent immediate cleanup and track it
if isinstance(script, ScriptTemplate):
script._delete_on_exit = False # Prevent cleanup before push
templates_to_cleanup.append(script)
# Use the actual rendered path as the source
source_path = script._rendered_script_path
assert source_path is not None # Should be set by __enter__
else:
source_path = source # Use the path from __enter__ directly
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

with script as source: is outside the for loop – only the last script (or none) gets processed

The current indentation breaks the batching logic:

for script in self.scripts:
    if not script.enabled(guest):
        continue

with script as source:          # <- executes *after* the loop

• If self.scripts is empty an UnboundLocalError is raised (script is undefined).
• Otherwise, only the final iteration’s script is handled, all preceding scripts are silently skipped.

Fix by nesting the with block (and all following handling) inside the loop.

             for script in self.scripts:
                 if not script.enabled(guest):
                     continue
 
-            with script as source:
+                with script as source:
                     # If it's a template, prevent immediate cleanup and track it
                     ...

Make sure to re‑indent every statement that manipulates source_path, target_dir, etc., so they execute for each script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
code | no functional change "No Functional Change" intended. Patch should not change tmt's behavior in any way. step | execute Stuff related to the execute step step | provision Stuff related to the provision step
Projects
Status: review
Development

Successfully merging this pull request may close these issues.

Copy scripts in batch and add executability with rsync
4 participants