234
234
type: int
235
235
vars:
236
236
- name: proxmox_vmid
237
+ proxmox_ssh_user:
238
+ description:
239
+ - Become command used in proxmox
240
+ type: str
241
+ default: root
242
+ vars:
243
+ - name: proxmox_ssh_user
237
244
proxmox_become_method:
238
245
description:
239
246
- Become command used in proxmox
266
273
- >
267
274
When NOT using this plugin as root, you need to have a become mechanism,
268
275
e.g. C(sudo), installed on Proxmox and setup so we can run it without prompting for the password.
269
- Inside the VM, we need a shell and commands like C(cat), C(dd), C(stat), C(base64), and C(sha256sum)
276
+ Inside the VM, we need a shell and commands like C(cat), C(dd), C(stat), C(base64), and C(sha256sum)
270
277
available in the C(PATH) for this plugin to work with file transfers.
271
278
- >
272
279
The VM must have QEMU guest agent installed and running.
307
314
tasks:
308
315
- name: Ping VM
309
316
ansible.builtin.ping:
310
-
317
+
311
318
- name: Copy file to VM
312
319
ansible.builtin.copy:
313
320
src: ./local_file.txt
314
321
dest: /tmp/remote_file.txt
315
-
322
+
316
323
- name: Fetch file from VM
317
324
ansible.builtin.fetch:
318
325
src: /tmp/remote_file.txt
343
350
from ansible .utils .path import makedirs_safe
344
351
from binascii import hexlify
345
352
353
+ import os
354
+ if os .getenv ("ANSIBLE_DEBUGPY" ) == "1" :
355
+ import debugpy
356
+ debugpy .listen (("0.0.0.0" , 5678 ))
357
+ debugpy .wait_for_client ()
358
+
346
359
try :
347
360
import paramiko
348
361
PARAMIKO_IMPORT_ERR = None
@@ -496,7 +509,7 @@ def _connect(self) -> Connection:
496
509
497
510
ssh .connect (
498
511
self .get_option ('remote_addr' ).lower (),
499
- username = self .get_option ('remote_user ' ),
512
+ username = self .get_option ('proxmox_ssh_user ' ),
500
513
allow_agent = allow_agent ,
501
514
look_for_keys = self .get_option ('look_for_keys' ),
502
515
key_filename = key_filename ,
@@ -521,7 +534,7 @@ def _connect(self) -> Connection:
521
534
raise AnsibleConnectionFailure (msg )
522
535
else :
523
536
raise AnsibleConnectionFailure (msg )
524
-
537
+
525
538
self .ssh = ssh
526
539
self ._connected = True
527
540
return self
@@ -558,9 +571,9 @@ def _save_ssh_host_keys(self, filename: str) -> None:
558
571
def _build_qm_command (self , cmd : str ) -> str :
559
572
"""Build qm guest exec command"""
560
573
qm_cmd = ['/usr/sbin/qm' , 'guest' , 'exec' , str (self .get_option ('vmid' )), '--' , cmd ]
561
- if self .get_option ('remote_user ' ) != 'root' :
574
+ if self .get_option ('proxmox_ssh_user ' ) != 'root' :
562
575
qm_cmd = [self .get_option ('proxmox_become_method' )] + qm_cmd
563
- display .vvv (f'INFO Running as non root user: { self .get_option ("remote_user " )} , trying to run qm with become method: ' +
576
+ display .vvv (f'INFO Running as non root user: { self .get_option ("proxmox_ssh_user " )} , trying to run qm with become method: ' +
564
577
f'{ self .get_option ("proxmox_become_method" )} ' ,
565
578
host = self .get_option ('remote_addr' ))
566
579
return ' ' .join (qm_cmd )
@@ -569,59 +582,60 @@ def _qm_exec(self, cmd: list[str], data_in: bytes | None = None, timeout: int |
569
582
"""Execute command inside VM via qm guest exec and return output"""
570
583
if timeout is None :
571
584
timeout = self .get_option ('qm_timeout' )
572
-
585
+
573
586
qm_cmd = ['/usr/sbin/qm' , 'guest' , 'exec' , str (self .get_option ('vmid' ))]
574
-
587
+
575
588
if data_in :
576
589
qm_cmd += ['--pass-stdin' , '1' ]
577
-
590
+
578
591
qm_cmd += ['--timeout' , str (timeout ), '--' ] + cmd
579
-
580
- if self .get_option ('remote_user ' ) != 'root' :
592
+
593
+ if self .get_option ('proxmox_ssh_user ' ) != 'root' :
581
594
qm_cmd = [self .get_option ('proxmox_become_method' )] + qm_cmd
582
595
583
596
try :
584
597
chan = self .ssh .get_transport ().open_session ()
585
- chan .exec_command (' ' .join (qm_cmd ))
586
-
598
+ command = ' ' .join (qm_cmd )
599
+ chan .exec_command (command )
600
+
587
601
if data_in :
588
602
chan .sendall (data_in )
589
603
chan .shutdown_write ()
590
-
604
+
591
605
stdout = b'' .join (chan .makefile ('rb' , 4096 ))
592
606
stderr = b'' .join (chan .makefile_stderr ('rb' , 4096 ))
593
607
returncode = chan .recv_exit_status ()
594
-
608
+
595
609
if returncode != 0 :
596
610
raise AnsibleError (f'qm command failed: { stderr .decode ()} ' )
597
-
611
+
598
612
if not stdout :
599
613
return None
600
-
614
+
601
615
stdout_json = json .loads (stdout .decode ())
602
-
616
+
603
617
if stdout_json .get ('exitcode' ) != 0 or stdout_json .get ('exited' ) != 1 :
604
618
raise AnsibleError (f'VM command failed: { stdout_json } ' )
605
-
619
+
606
620
return stdout_json .get ('out-data' )
607
-
621
+
608
622
except Exception as e :
609
623
raise AnsibleError (f'qm execution failed: { to_text (e )} ' )
610
624
611
625
def _check_guest_agent (self ) -> None :
612
626
"""Check if guest agent is available"""
613
627
try :
614
628
qm_cmd = ['/usr/sbin/qm' , 'guest' , 'cmd' , str (self .get_option ('vmid' )), 'ping' ]
615
- if self .get_option ('remote_user ' ) != 'root' :
629
+ if self .get_option ('proxmox_ssh_user ' ) != 'root' :
616
630
qm_cmd = [self .get_option ('proxmox_become_method' )] + qm_cmd
617
-
631
+
618
632
chan = self .ssh .get_transport ().open_session ()
619
633
chan .exec_command (' ' .join (qm_cmd ))
620
634
returncode = chan .recv_exit_status ()
621
-
635
+
622
636
if returncode != 0 :
623
637
raise AnsibleError ('Guest agent is not installed or not responding' )
624
-
638
+
625
639
except Exception as e :
626
640
raise AnsibleError (f'Guest agent check failed: { to_text (e )} ' )
627
641
@@ -630,7 +644,7 @@ def _check_required_commands(self) -> None:
630
644
required_commands = ["cat" , "dd" , "stat" , "base64" , "sha256sum" ]
631
645
for cmd in required_commands :
632
646
try :
633
- result = self ._qm_exec (['sh' , '-c' , f'which { cmd } ' ])
647
+ result = self ._qm_exec (['sh' , '-c' , f" 'which { cmd } '" ])
634
648
if not result :
635
649
raise AnsibleError (f"Command '{ cmd } ' is not available on the VM" )
636
650
except Exception :
@@ -722,135 +736,149 @@ def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool =
722
736
if 'qm: not found' in stderr .decode ('utf-8' ):
723
737
raise AnsibleError (f'qm not found in path of host: { to_text (self .get_option ("remote_addr" ))} ' )
724
738
739
+ # Check proxmox qm binary return code:
740
+ if returncode == 0 :
741
+ # Parse results of command executed inside of the vm
742
+ stdout_json = json .loads (stdout .decode ())
743
+ # Check if command inside of vm failed
744
+ if stdout_json .get ('exitcode' ) != 0 or stdout_json .get ('exited' ) != 1 :
745
+ raise AnsibleError (f'VM command failed: { stdout_json } ' )
746
+ returncode = stdout_json .get ('exitcode' )
747
+ # Extract output from command executed inside of vm
748
+ if stdout_json .get ('out-data' ):
749
+ stdout = stdout_json .get ('out-data' ).encode ()
750
+ else :
751
+ stdout = b''
752
+
725
753
return (returncode , no_prompt_out + stdout , no_prompt_out + stderr )
726
754
727
755
def put_file (self , in_path : str , out_path : str ) -> None :
728
756
""" transfer a file from local to VM using chunked transfer """
729
757
730
758
display .vvv (f'PUT { in_path } TO { out_path } ' , host = self .get_option ('remote_addr' ))
731
-
759
+
732
760
try :
733
761
# Check guest agent and required commands
734
762
self ._check_guest_agent ()
735
763
self ._check_required_commands ()
736
-
764
+
737
765
file_size = os .path .getsize (in_path )
738
766
chunk_size = self .get_option ('qm_file_chunk_size_put' )
739
767
total_chunks = (file_size + chunk_size - 1 ) // chunk_size
740
-
768
+
741
769
display .vvv (f'File size: { file_size } bytes. Transferring in { total_chunks } chunks.' )
742
-
770
+
743
771
operator = '>'
744
-
772
+
745
773
with open (in_path , 'rb' ) as f :
746
774
for chunk_num in range (total_chunks ):
747
775
chunk = f .read (chunk_size )
748
776
if not chunk :
749
777
break
750
-
778
+
751
779
display .vvv (f'Transferring chunk { chunk_num + 1 } /{ total_chunks } ({ len (chunk )} bytes)' )
752
-
780
+
753
781
# Transfer chunk using qm guest exec
754
- self ._qm_exec (['sh' , '-c' , f'cat { operator } { out_path } ' ], data_in = chunk )
782
+ self ._qm_exec (['sh' , '-c' , f" 'cat { operator } { out_path } '" ], data_in = chunk )
755
783
operator = '>>' # After first chunk, append
756
-
784
+
757
785
# Verify file transfer
758
786
try :
759
- remote_size = int (self ._qm_exec (['sh' , '-c' , f'stat --printf="%s" { out_path } ' ]) or '0' )
787
+ remote_size = int (self ._qm_exec (['sh' , '-c' , f" 'stat --printf=\ " %s\ " { out_path } '" ]) or '0' )
760
788
if remote_size != file_size :
761
789
raise AnsibleError (f'File size mismatch: local={ file_size } , remote={ remote_size } ' )
762
-
790
+
763
791
# Calculate checksums for verification
764
792
local_hash = hashlib .sha256 ()
765
793
with open (in_path , 'rb' ) as f :
766
794
for chunk in iter (lambda : f .read (8192 ), b"" ):
767
795
local_hash .update (chunk )
768
796
local_checksum = local_hash .hexdigest ()
769
-
770
- remote_checksum = self ._qm_exec (['sh' , '-c' , f'sha256sum { out_path } | cut -d " " -f 1' ]).strip ()
771
-
797
+
798
+ remote_checksum = self ._qm_exec (['sh' , '-c' , f" 'sha256sum { out_path } | cut -d \" \ " -f 1'" ]).strip ()
799
+
772
800
if local_checksum != remote_checksum :
773
801
raise AnsibleError (f'Checksum mismatch: local={ local_checksum } , remote={ remote_checksum } ' )
774
-
802
+
775
803
except Exception as e :
776
804
display .warning (f'File verification failed: { to_text (e )} ' )
777
-
805
+
778
806
except Exception as e :
779
807
raise AnsibleError (f'error occurred while putting file from { in_path } to { out_path } !\n { to_text (e )} ' )
780
808
781
809
def fetch_file (self , in_path : str , out_path : str ) -> None :
782
810
""" fetch a file from VM using chunked transfer """
783
811
784
812
display .vvv (f'FETCH { in_path } TO { out_path } ' , host = self .get_option ('remote_addr' ))
785
-
813
+
786
814
try :
787
815
# Check guest agent and required commands
788
816
self ._check_guest_agent ()
789
817
self ._check_required_commands ()
790
-
818
+
791
819
# Get file size
792
- file_size = int (self ._qm_exec (['sh' , '-c' , f'stat --printf="%s" { in_path } ' ]) or '0' )
820
+ file_size = int (self ._qm_exec (['sh' , '-c' , f" 'stat --printf=\ " %s\ " { in_path } '" ]) or '0' )
793
821
if file_size == 0 :
794
822
raise AnsibleError (f'File { in_path } does not exist or is empty' )
795
-
823
+
796
824
chunk_size = self .get_option ('qm_file_chunk_size_fetch' )
797
825
blocksize = 4096
798
826
count = int (chunk_size / blocksize )
799
827
total_chunks = (file_size + chunk_size - 1 ) // chunk_size
800
-
828
+
801
829
display .vvv (f'File size: { file_size } bytes. Fetching in { total_chunks } chunks.' )
802
-
830
+
803
831
transferred_bytes = 0
804
-
832
+
805
833
with open (out_path , 'wb' ) as f :
806
834
for chunk_num in range (total_chunks ):
807
835
display .vvv (f'Fetching chunk { chunk_num + 1 } /{ total_chunks } ' )
808
-
836
+
809
837
# Calculate remaining bytes to transfer
810
838
remaining_bytes = file_size - transferred_bytes
811
839
current_chunk_size = min (chunk_size , remaining_bytes )
812
-
840
+
813
841
# Fetch chunk using dd + base64
814
- cmd = f'dd if={ in_path } bs={ blocksize } count={ count } skip={ count * chunk_num } 2>/dev/null | base64 -w0'
842
+ cmd = f" 'dd if={ in_path } bs={ blocksize } count={ count } skip={ count * chunk_num } 2>/dev/null | base64 -w0'"
815
843
chunk_data_b64 = self ._qm_exec (['sh' , '-c' , cmd ])
816
-
844
+
817
845
if not chunk_data_b64 :
818
846
break
819
-
847
+
820
848
# Decode base64 data
821
849
chunk_data = base64 .standard_b64decode (chunk_data_b64 )
822
-
850
+
823
851
# Trim chunk to actual remaining file size
824
852
if len (chunk_data ) > remaining_bytes :
825
853
chunk_data = chunk_data [:remaining_bytes ]
826
-
854
+
827
855
f .write (chunk_data )
828
856
transferred_bytes += len (chunk_data )
829
-
857
+
830
858
if transferred_bytes >= file_size :
831
859
break
832
-
860
+
833
861
# Verify file transfer
834
862
try :
835
863
local_size = os .path .getsize (out_path )
836
864
if local_size != file_size :
837
865
raise AnsibleError (f'File size mismatch: local={ local_size } , remote={ file_size } ' )
838
-
866
+
839
867
# Calculate checksums for verification
840
868
local_hash = hashlib .sha256 ()
841
869
with open (out_path , 'rb' ) as f :
842
870
for chunk in iter (lambda : f .read (8192 ), b"" ):
843
871
local_hash .update (chunk )
844
872
local_checksum = local_hash .hexdigest ()
845
-
846
- remote_checksum = self ._qm_exec (['sh' , '-c' , f'sha256sum { in_path } | cut -d " " -f 1' ]).strip ()
847
-
873
+
874
+ remote_checksum = self ._qm_exec (['sh' , '-c' , f" 'sha256sum { in_path } | cut -d \" \ " -f 1'" ]).strip ()
875
+
848
876
if local_checksum != remote_checksum :
849
877
raise AnsibleError (f'Checksum mismatch: local={ local_checksum } , remote={ remote_checksum } ' )
850
-
878
+
851
879
except Exception as e :
852
880
display .warning (f'File verification failed: { to_text (e )} ' )
853
-
881
+
854
882
except Exception as e :
855
883
raise AnsibleError (f'error occurred while fetching file from { in_path } to { out_path } !\n { to_text (e )} ' )
856
884
0 commit comments