Skip to content

Commit 5312ff2

Browse files
committed
fixed connection plugin
1 parent 43f9189 commit 5312ff2

File tree

1 file changed

+92
-64
lines changed

1 file changed

+92
-64
lines changed

plugins/connection/proxmox_qm_remote.py

Lines changed: 92 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,13 @@
234234
type: int
235235
vars:
236236
- 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
237244
proxmox_become_method:
238245
description:
239246
- Become command used in proxmox
@@ -266,7 +273,7 @@
266273
- >
267274
When NOT using this plugin as root, you need to have a become mechanism,
268275
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)
270277
available in the C(PATH) for this plugin to work with file transfers.
271278
- >
272279
The VM must have QEMU guest agent installed and running.
@@ -307,12 +314,12 @@
307314
tasks:
308315
- name: Ping VM
309316
ansible.builtin.ping:
310-
317+
311318
- name: Copy file to VM
312319
ansible.builtin.copy:
313320
src: ./local_file.txt
314321
dest: /tmp/remote_file.txt
315-
322+
316323
- name: Fetch file from VM
317324
ansible.builtin.fetch:
318325
src: /tmp/remote_file.txt
@@ -343,6 +350,12 @@
343350
from ansible.utils.path import makedirs_safe
344351
from binascii import hexlify
345352

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+
346359
try:
347360
import paramiko
348361
PARAMIKO_IMPORT_ERR = None
@@ -496,7 +509,7 @@ def _connect(self) -> Connection:
496509

497510
ssh.connect(
498511
self.get_option('remote_addr').lower(),
499-
username=self.get_option('remote_user'),
512+
username=self.get_option('proxmox_ssh_user'),
500513
allow_agent=allow_agent,
501514
look_for_keys=self.get_option('look_for_keys'),
502515
key_filename=key_filename,
@@ -521,7 +534,7 @@ def _connect(self) -> Connection:
521534
raise AnsibleConnectionFailure(msg)
522535
else:
523536
raise AnsibleConnectionFailure(msg)
524-
537+
525538
self.ssh = ssh
526539
self._connected = True
527540
return self
@@ -558,9 +571,9 @@ def _save_ssh_host_keys(self, filename: str) -> None:
558571
def _build_qm_command(self, cmd: str) -> str:
559572
"""Build qm guest exec command"""
560573
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':
562575
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: ' +
564577
f'{self.get_option("proxmox_become_method")}',
565578
host=self.get_option('remote_addr'))
566579
return ' '.join(qm_cmd)
@@ -569,59 +582,60 @@ def _qm_exec(self, cmd: list[str], data_in: bytes | None = None, timeout: int |
569582
"""Execute command inside VM via qm guest exec and return output"""
570583
if timeout is None:
571584
timeout = self.get_option('qm_timeout')
572-
585+
573586
qm_cmd = ['/usr/sbin/qm', 'guest', 'exec', str(self.get_option('vmid'))]
574-
587+
575588
if data_in:
576589
qm_cmd += ['--pass-stdin', '1']
577-
590+
578591
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':
581594
qm_cmd = [self.get_option('proxmox_become_method')] + qm_cmd
582595

583596
try:
584597
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+
587601
if data_in:
588602
chan.sendall(data_in)
589603
chan.shutdown_write()
590-
604+
591605
stdout = b''.join(chan.makefile('rb', 4096))
592606
stderr = b''.join(chan.makefile_stderr('rb', 4096))
593607
returncode = chan.recv_exit_status()
594-
608+
595609
if returncode != 0:
596610
raise AnsibleError(f'qm command failed: {stderr.decode()}')
597-
611+
598612
if not stdout:
599613
return None
600-
614+
601615
stdout_json = json.loads(stdout.decode())
602-
616+
603617
if stdout_json.get('exitcode') != 0 or stdout_json.get('exited') != 1:
604618
raise AnsibleError(f'VM command failed: {stdout_json}')
605-
619+
606620
return stdout_json.get('out-data')
607-
621+
608622
except Exception as e:
609623
raise AnsibleError(f'qm execution failed: {to_text(e)}')
610624

611625
def _check_guest_agent(self) -> None:
612626
"""Check if guest agent is available"""
613627
try:
614628
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':
616630
qm_cmd = [self.get_option('proxmox_become_method')] + qm_cmd
617-
631+
618632
chan = self.ssh.get_transport().open_session()
619633
chan.exec_command(' '.join(qm_cmd))
620634
returncode = chan.recv_exit_status()
621-
635+
622636
if returncode != 0:
623637
raise AnsibleError('Guest agent is not installed or not responding')
624-
638+
625639
except Exception as e:
626640
raise AnsibleError(f'Guest agent check failed: {to_text(e)}')
627641

@@ -630,7 +644,7 @@ def _check_required_commands(self) -> None:
630644
required_commands = ["cat", "dd", "stat", "base64", "sha256sum"]
631645
for cmd in required_commands:
632646
try:
633-
result = self._qm_exec(['sh', '-c', f'which {cmd}'])
647+
result = self._qm_exec(['sh', '-c', f"'which {cmd}'"])
634648
if not result:
635649
raise AnsibleError(f"Command '{cmd}' is not available on the VM")
636650
except Exception:
@@ -722,135 +736,149 @@ def exec_command(self, cmd: str, in_data: bytes | None = None, sudoable: bool =
722736
if 'qm: not found' in stderr.decode('utf-8'):
723737
raise AnsibleError(f'qm not found in path of host: {to_text(self.get_option("remote_addr"))}')
724738

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+
725753
return (returncode, no_prompt_out + stdout, no_prompt_out + stderr)
726754

727755
def put_file(self, in_path: str, out_path: str) -> None:
728756
""" transfer a file from local to VM using chunked transfer """
729757

730758
display.vvv(f'PUT {in_path} TO {out_path}', host=self.get_option('remote_addr'))
731-
759+
732760
try:
733761
# Check guest agent and required commands
734762
self._check_guest_agent()
735763
self._check_required_commands()
736-
764+
737765
file_size = os.path.getsize(in_path)
738766
chunk_size = self.get_option('qm_file_chunk_size_put')
739767
total_chunks = (file_size + chunk_size - 1) // chunk_size
740-
768+
741769
display.vvv(f'File size: {file_size} bytes. Transferring in {total_chunks} chunks.')
742-
770+
743771
operator = '>'
744-
772+
745773
with open(in_path, 'rb') as f:
746774
for chunk_num in range(total_chunks):
747775
chunk = f.read(chunk_size)
748776
if not chunk:
749777
break
750-
778+
751779
display.vvv(f'Transferring chunk {chunk_num + 1}/{total_chunks} ({len(chunk)} bytes)')
752-
780+
753781
# 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)
755783
operator = '>>' # After first chunk, append
756-
784+
757785
# Verify file transfer
758786
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')
760788
if remote_size != file_size:
761789
raise AnsibleError(f'File size mismatch: local={file_size}, remote={remote_size}')
762-
790+
763791
# Calculate checksums for verification
764792
local_hash = hashlib.sha256()
765793
with open(in_path, 'rb') as f:
766794
for chunk in iter(lambda: f.read(8192), b""):
767795
local_hash.update(chunk)
768796
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+
772800
if local_checksum != remote_checksum:
773801
raise AnsibleError(f'Checksum mismatch: local={local_checksum}, remote={remote_checksum}')
774-
802+
775803
except Exception as e:
776804
display.warning(f'File verification failed: {to_text(e)}')
777-
805+
778806
except Exception as e:
779807
raise AnsibleError(f'error occurred while putting file from {in_path} to {out_path}!\n{to_text(e)}')
780808

781809
def fetch_file(self, in_path: str, out_path: str) -> None:
782810
""" fetch a file from VM using chunked transfer """
783811

784812
display.vvv(f'FETCH {in_path} TO {out_path}', host=self.get_option('remote_addr'))
785-
813+
786814
try:
787815
# Check guest agent and required commands
788816
self._check_guest_agent()
789817
self._check_required_commands()
790-
818+
791819
# 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')
793821
if file_size == 0:
794822
raise AnsibleError(f'File {in_path} does not exist or is empty')
795-
823+
796824
chunk_size = self.get_option('qm_file_chunk_size_fetch')
797825
blocksize = 4096
798826
count = int(chunk_size / blocksize)
799827
total_chunks = (file_size + chunk_size - 1) // chunk_size
800-
828+
801829
display.vvv(f'File size: {file_size} bytes. Fetching in {total_chunks} chunks.')
802-
830+
803831
transferred_bytes = 0
804-
832+
805833
with open(out_path, 'wb') as f:
806834
for chunk_num in range(total_chunks):
807835
display.vvv(f'Fetching chunk {chunk_num + 1}/{total_chunks}')
808-
836+
809837
# Calculate remaining bytes to transfer
810838
remaining_bytes = file_size - transferred_bytes
811839
current_chunk_size = min(chunk_size, remaining_bytes)
812-
840+
813841
# 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'"
815843
chunk_data_b64 = self._qm_exec(['sh', '-c', cmd])
816-
844+
817845
if not chunk_data_b64:
818846
break
819-
847+
820848
# Decode base64 data
821849
chunk_data = base64.standard_b64decode(chunk_data_b64)
822-
850+
823851
# Trim chunk to actual remaining file size
824852
if len(chunk_data) > remaining_bytes:
825853
chunk_data = chunk_data[:remaining_bytes]
826-
854+
827855
f.write(chunk_data)
828856
transferred_bytes += len(chunk_data)
829-
857+
830858
if transferred_bytes >= file_size:
831859
break
832-
860+
833861
# Verify file transfer
834862
try:
835863
local_size = os.path.getsize(out_path)
836864
if local_size != file_size:
837865
raise AnsibleError(f'File size mismatch: local={local_size}, remote={file_size}')
838-
866+
839867
# Calculate checksums for verification
840868
local_hash = hashlib.sha256()
841869
with open(out_path, 'rb') as f:
842870
for chunk in iter(lambda: f.read(8192), b""):
843871
local_hash.update(chunk)
844872
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+
848876
if local_checksum != remote_checksum:
849877
raise AnsibleError(f'Checksum mismatch: local={local_checksum}, remote={remote_checksum}')
850-
878+
851879
except Exception as e:
852880
display.warning(f'File verification failed: {to_text(e)}')
853-
881+
854882
except Exception as e:
855883
raise AnsibleError(f'error occurred while fetching file from {in_path} to {out_path}!\n{to_text(e)}')
856884

0 commit comments

Comments
 (0)