pyinfra-dev / pyinfra

pyinfra turns Python code into shell commands and runs them on your servers. Execute ad-hoc commands and write declarative operations. Target SSH servers, local machine and Docker containers. Fast and scales from one server to thousands.
https://pyinfra.com
MIT License
3.91k stars 382 forks source link

Files operations are ignoring TEMP_DIR setting when SUDO = True #761

Closed Renerick closed 2 years ago

Renerick commented 2 years ago

Describe the bug

When trying to upload or template a file to remote host without /tmp access and when SUDO = True I receive an error that /tmp directory is not found even when other value of the TEMP_DIR setting is specified. Currently I'm experiencing this problem when trying to upload docker-compose files to Synology NAS where direct SFTP has no access to file system other then user home directory

To Reproduce

Steps to reproduce the behavior (include code & usage example):

from pyinfra import config
from pyinfra.operations import files

config.SUDO = True
config.TEMP_DIR = 'test'

files.put('synology/docker-compose/agh.yml', './tmp/agh/docker-composy.yml')
pyinfra ./inventory/inventory.py deploys/synology/agh.py --limit synology -vvvv --debug
    [pyinfra_cli.main] Checking potential directory: deploys/synology
    [pyinfra_cli.main] Checking potential directory: deploys
    [pyinfra_cli.main] Setting directory to: deploys
--> Loading config...
--> Loading inventory...
    [pyinfra_cli.inventory] Creating fake inventory...
    [pyinfra_cli.inventory] Looking for group data in: deploys/group_data/[REDACTED].py

--> Connecting to hosts...
    [pyinfra.api.connectors.ssh] Connecting to: synology ({'allow_agent': False, 'look_for_keys': False, 'hostname': '[REDACTED]', '_pyinfra_force_forward_agent': None, '_pyinfra_ssh_config_file': None, 'username': '[REDACTED]', 'port': [REDACTED], 'timeout': 10})
    [synology] Connected
    [pyinfra.api.state] Activating host: synology

--> Preparing operations...
    Loading: deploys/synology/agh.py
    [pyinfra.api.operation] Adding operation, {'Files/Put'}, opOrder=(8,), opHash=bd05d67789318d253923550b574ee43b70bbb955
    [pyinfra.api.facts] Getting fact: file (path=./tmp/agh/docker-compose.yml) (ensure_hosts: (Host(synology),))
    [pyinfra.api.connectors.ssh] Running command on synology: (pty=False) sh -c 'chmod +x pyinfra-sudo-askpass'
    [pyinfra.api.connectors.ssh] Waiting for exit status...
    [pyinfra.api.connectors.ssh] Command exit status: 0
    [pyinfra.api.connectors.ssh] Running command on synology: (pty=False) env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c '! (test -e ./tmp/agh/docker-compose.yml || test -L ./tmp/agh/docker-compose.yml ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/agh/docker-compose.yml 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/agh/docker-compose.yml )'
[synology] >>> env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c '! (test -e ./tmp/agh/docker-compose.yml || test -L ./tmp/agh/docker-compose.yml ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/agh/docker-compose.yml 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/agh/docker-compose.yml )'
    [pyinfra.api.connectors.ssh] Waiting for exit status...
    [pyinfra.api.connectors.ssh] Command exit status: 0
    Loaded fact file (path=./tmp/agh/docker-compose.yml)
    [pyinfra.api.facts] Getting fact: directory (path=./tmp/agh/docker-compose.yml) (ensure_hosts: (Host(synology),))
    [pyinfra.api.connectors.ssh] Running command on synology: (pty=False) env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c '! (test -e ./tmp/agh/docker-compose.yml || test -L ./tmp/agh/docker-compose.yml ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/agh/docker-compose.yml 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/agh/docker-compose.yml )'
[synology] >>> env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c '! (test -e ./tmp/agh/docker-compose.yml || test -L ./tmp/agh/docker-compose.yml ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/agh/docker-compose.yml 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/agh/docker-compose.yml )'
    [pyinfra.api.connectors.ssh] Waiting for exit status...
    [pyinfra.api.connectors.ssh] Command exit status: 0
    Loaded fact directory (path=./tmp/agh/docker-compose.yml)
    [pyinfra.api.facts] Getting fact: directory (path=./tmp/agh) (ensure_hosts: (Host(synology),))
    [pyinfra.api.connectors.ssh] Running command on synology: (pty=False) env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c '! (test -e ./tmp/agh || test -L ./tmp/agh ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/agh 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/agh )'
[synology] >>> env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c '! (test -e ./tmp/agh || test -L ./tmp/agh ) || ( stat -c '"'"'user=%U group=%G mode=%A atime=%X mtime=%Y ctime=%Z size=%s %N'"'"' ./tmp/agh 2> /dev/null || stat -f '"'"'user=%Su group=%Sg mode=%Sp atime=%a mtime=%m ctime=%c size=%z %N%SY'"'"' ./tmp/agh )'
[synology] user=ansible-admin group=users mode=drwx--x--x atime=1645292982 mtime=1645352207 ctime=1645352207 size=0 './tmp/agh'
    [pyinfra.api.connectors.ssh] Waiting for exit status...
    [pyinfra.api.connectors.ssh] Command exit status: 0
    Loaded fact directory (path=./tmp/agh)
    [pyinfra.api.facts] Getting fact: sha1_file (path=./tmp/agh/docker-compose.yml) (ensure_hosts: (Host(synology),))
    [pyinfra.api.connectors.ssh] Running command on synology: (pty=False) env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c 'test -e ./tmp/agh/docker-compose.yml && ( sha1sum ./tmp/agh/docker-compose.yml 2> /dev/null || shasum ./tmp/agh/docker-compose.yml 2> /dev/null || sha1 ./tmp/agh/docker-compose.yml ) || true'
[synology] >>> env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c 'test -e ./tmp/agh/docker-compose.yml && ( sha1sum ./tmp/agh/docker-compose.yml 2> /dev/null || shasum ./tmp/agh/docker-compose.yml 2> /dev/null || sha1 ./tmp/agh/docker-compose.yml ) || true'
    [pyinfra.api.connectors.ssh] Waiting for exit status...
    [pyinfra.api.connectors.ssh] Command exit status: 0
    Loaded fact sha1_file (path=./tmp/agh/docker-compose.yml)
    [pyinfra.api.facts] Getting fact: file (path=./tmp/agh/docker-compose.yml) (ensure_hosts: (Host(synology),))
    [synology] Ready: deploys/synology/agh.py

--> Proposed changes:
    Groups: inventory / infra
    [synology]   Operations: 1   Commands: 1   

--> Beginning operation run...
--> Starting operation: Files/Put (synology/docker-compose/agh.yml, ./tmp/agh/docker-compose.yml)
    [pyinfra.api.operations] Starting operation Files/Put on synology
    [pyinfra.api.connectors.ssh] Running command on synology: (pty=None) env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c 'cp /tmp/pyinfra-7dec97431f2113fbc8f45e59a9838c86c08274ef ./tmp/agh/docker-compose.yml'
[synology] >>> env SUDO_ASKPASS=pyinfra-sudo-askpass *** sudo -H -A -k sh -c 'cp /tmp/pyinfra-7dec97431f2113fbc8f45e59a9838c86c08274ef ./tmp/agh/docker-compose.yml'
[synology] cp: cannot stat '/tmp/pyinfra-7dec97431f2113fbc8f45e59a9838c86c08274ef': No such file or directory
    [pyinfra.api.connectors.ssh] Waiting for exit status...
    [pyinfra.api.connectors.ssh] Command exit status: 1
    File upload error: cp: cannot stat '/tmp/pyinfra-7dec97431f2113fbc8f45e59a9838c86c08274ef': No such file or directory
    [synology] Error
    [pyinfra.api.state] Failing hosts: synology
    [pyinfra.api.connectors.ssh] Running command on synology: (pty=False) sh -c 'rm -f pyinfra-sudo-askpass'
    [pyinfra.api.connectors.ssh] Waiting for exit status...
    [pyinfra.api.connectors.ssh] Command exit status: 0
--> pyinfra error: No hosts remaining!

Notice how pyinfra is trying to copy from /tmp directory.

This same code works properly without sudo, the source file is present on the pyinfra machine, so I came to a conclusion that this is a bug

I tried to debug it myself by adding the following lines here

    if sudo or su_user:
        # Get temp file location
        temp_file = state.get_temp_filename(remote_filename)
        print("INTERNAL DEBUG: temp_file", temp_file)
        print("INTERNAL DEBUG: TEMP_DIR", state.config.TEMP_DIR)
        print("INTERNAL DEBUG: SUDO", state.config.SUDO)
        _put_file(host, filename_or_io, temp_file)

This was the output

INTERNAL DEBUG: temp_file /tmp/pyinfra-7dec97431f2113fbc8f45e59a9838c86c08274ef
INTERNAL DEBUG: TEMP_DIR /tmp
INTERNAL DEBUG: SUDO False

Seems like configuration values are not being passed correctly to the connector

EDIT 1:

I went a bit further and added this to https://github.dev/Fizzadar/pyinfra/blob/233a7797dc556f01727b8887cbf0e39bdd9dc548/pyinfra/api/host.py#L1

    def put_file(self, *args, **kwargs):
        self._check_state()
        print('INTERNAL DEBUG state', self.state.config.TEMP_DIR)
        return self.executor.put_file(self.state, self, *args, **kwargs)

Here is the result

--> Loading config...
--> Loading inventory...

--> Connecting to hosts...
    [synology] Connected

--> Preparing operations...
    Loading: deploys/synology/agh.py
INTERNAL DEBUG state test
    [synology] Ready: deploys/synology/agh.py

--> Proposed changes:
    Groups: inventory / infra
    [synology]   Operations: 1   Commands: 1   

--> Beginning operation run...
--> Starting operation: Files/Put (synology/docker-compose/agh.yml, ./tmp/agh/docker-compose.yml)
INTERNAL DEBUG state /tmp
    File upload error: cp: cannot stat '/tmp/pyinfra-7dec97431f2113fbc8f45e59a9838c86c08274ef': No such file or directory
    [synology] Error
--> pyinfra error: No hosts remaining!

Seems like configs values are diverging between planning and execution stages

Expected behavior

Files are uploaded/templated without errors

Meta

Fizzadar commented 2 years ago

@Renerick thanks for reporting this, confirmed is happening on both 1.6.x and v2 WIP as well. Had a quick look but can't quite figure it out so needs a more thorough investigation.

Fizzadar commented 2 years ago

So the issue here was the config being used to generate the temp filename at execution time, not during operation generation. I've change how this works to pass through the temporary name in https://github.com/Fizzadar/pyinfra/commit/eec1c5d51bfd6e97331c878dcbd0458a856e5848 which should resolve this. Pending v1.7 release later today.

Fizzadar commented 2 years ago

Now released in v1.7!