From 75e35bfa6cfc7fb60e61bb0d789c9b5fded39523 Mon Sep 17 00:00:00 2001 From: "patchback[bot]" <45432694+patchback[bot]@users.noreply.github.com> Date: Sat, 15 Feb 2025 13:41:24 +0100 Subject: [PATCH] [PR #9659/06df717b backport][stable-10] lxd_connection: Allow non-root users to connect to an instance (#9751) lxd_connection: Allow non-root users to connect to an instance (#9659) * fix: add support for non-root user * fix: show correct info for connection * fix: use build_exec_command to execute as nonroot * unset default user * feat: add options for setting remote user and become method * fix: add root as default remote_user * fix: remove ansible_ssh_user from remote_user vars * fix: use single quotes inside f-string * fix: ensure lxc exec comes first * fix: line length * fix: use -c flag with su * Update plugins/connection/lxd.py Co-authored-by: Felix Fontein * Update plugins/connection/lxd.py Co-authored-by: Felix Fontein * Update plugins/connection/lxd.py Co-authored-by: Felix Fontein * doc: add changelog fragment * fix: use underscore for module name in fragment * Update 9659-lxd_connection-nonroot-user.yml Co-authored-by: Felix Fontein * fix: add put command * feat: add get_remote_uid_gid placeholder function * feat: complete placeholder _get_remote_uid_gid function * fix: better logging * fix: ensure default values are of type str * fix: use ints for uid and gid * fix: print put command * fix: format * fix: display msg for PUT * fix: add comment about defaults * fix: format * fix: use os module to get uid and gid * Revert "fix: use os module to get uid and gid" This reverts commit bb2ba14b8fde1639f1053fc0e0a52349b22deee3. * Update plugins/connection/lxd.py Co-authored-by: Felix Fontein * fix: omit uid, gid args in lxd file push if root --------- Co-authored-by: Felix Fontein (cherry picked from commit 06df717bc62fe3371acb2462b7694828b8e8196e) Co-authored-by: Peter Siegel <33677897+yeetypete@users.noreply.github.com> --- .../9659-lxd_connection-nonroot-user.yml | 2 + plugins/connection/lxd.py | 116 +++++++++++++++--- 2 files changed, 98 insertions(+), 20 deletions(-) create mode 100644 changelogs/fragments/9659-lxd_connection-nonroot-user.yml diff --git a/changelogs/fragments/9659-lxd_connection-nonroot-user.yml b/changelogs/fragments/9659-lxd_connection-nonroot-user.yml new file mode 100644 index 0000000000..760921f054 --- /dev/null +++ b/changelogs/fragments/9659-lxd_connection-nonroot-user.yml @@ -0,0 +1,2 @@ +minor_changes: + - lxd connection plugin - adds ``remote_user`` and ``lxd_become_method`` parameters for allowing a non-root user to connect to an LXD instance (https://github.com/ansible-collections/community.general/pull/9659). diff --git a/plugins/connection/lxd.py b/plugins/connection/lxd.py index fc8b4ae474..2670ed1b5f 100644 --- a/plugins/connection/lxd.py +++ b/plugins/connection/lxd.py @@ -32,6 +32,15 @@ options: vars: - name: ansible_executable - name: ansible_lxd_executable + lxd_become_method: + description: + - Become command used to switch to a non-root user. + - Is only used when O(remote_user) is not V(root). + type: str + default: /bin/su + vars: + - name: lxd_become_method + version_added: 10.4.0 remote: description: - Name of the LXD remote to use. @@ -40,6 +49,22 @@ options: vars: - name: ansible_lxd_remote version_added: 2.0.0 + remote_user: + description: + - User to login/authenticate as. + - Can be set from the CLI via the C(--user) or C(-u) options. + type: string + default: root + vars: + - name: ansible_user + env: + - name: ANSIBLE_REMOTE_USER + ini: + - section: defaults + key: remote_user + keyword: + - name: remote_user + version_added: 10.4.0 project: description: - Name of the LXD project to use. @@ -63,7 +88,6 @@ class Connection(ConnectionBase): transport = 'community.general.lxd' has_pipelining = True - default_user = 'root' def __init__(self, play_context, new_stdin, *args, **kwargs): super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) @@ -73,9 +97,6 @@ class Connection(ConnectionBase): except ValueError: raise AnsibleError("lxc command not found in PATH") - if self._play_context.remote_user is not None and self._play_context.remote_user != 'root': - self._display.warning('lxd does not support remote_user, using default: root') - def _host(self): """ translate remote_addr to lxd (short) hostname """ return self.get_option("remote_addr").split(".", 1)[0] @@ -85,25 +106,40 @@ class Connection(ConnectionBase): super(Connection, self)._connect() if not self._connected: - self._display.vvv("ESTABLISH LXD CONNECTION FOR USER: root", host=self._host()) + self._display.vvv(f"ESTABLISH LXD CONNECTION FOR USER: {self.get_option('remote_user')}", host=self._host()) self._connected = True + def _build_command(self, cmd) -> str: + """build the command to execute on the lxd host""" + + exec_cmd = [self._lxc_cmd] + + if self.get_option("project"): + exec_cmd.extend(["--project", self.get_option("project")]) + + exec_cmd.extend(["exec", f"{self.get_option('remote')}:{self._host()}", "--"]) + + if self.get_option("remote_user") != "root": + self._display.vvv( + f"INFO: Running as non-root user: {self.get_option('remote_user')}, \ + trying to run 'lxc exec' with become method: {self.get_option('lxd_become_method')}", + host=self._host(), + ) + exec_cmd.extend( + [self.get_option("lxd_become_method"), self.get_option("remote_user"), "-c"] + ) + + exec_cmd.extend([self.get_option("executable"), "-c", cmd]) + + return exec_cmd + def exec_command(self, cmd, in_data=None, sudoable=True): """ execute a command on the lxd host """ super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable) self._display.vvv(f"EXEC {cmd}", host=self._host()) - local_cmd = [self._lxc_cmd] - if self.get_option("project"): - local_cmd.extend(["--project", self.get_option("project")]) - local_cmd.extend([ - "exec", - f"{self.get_option('remote')}:{self._host()}", - "--", - self.get_option("executable"), "-c", cmd - ]) - + local_cmd = self._build_command(cmd) self._display.vvvvv(f"EXEC {local_cmd}", host=self._host()) local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd] @@ -125,6 +161,25 @@ class Connection(ConnectionBase): return process.returncode, stdout, stderr + def _get_remote_uid_gid(self) -> tuple[int, int]: + """Get the user and group ID of 'remote_user' from the instance.""" + + rc, uid_out, err = self.exec_command("/bin/id -u") + if rc != 0: + raise AnsibleError( + f"Failed to get remote uid for user {self.get_option('remote_user')}: {err}" + ) + uid = uid_out.strip() + + rc, gid_out, err = self.exec_command("/bin/id -g") + if rc != 0: + raise AnsibleError( + f"Failed to get remote gid for user {self.get_option('remote_user')}: {err}" + ) + gid = gid_out.strip() + + return int(uid), int(gid) + def put_file(self, in_path, out_path): """ put a file from local to lxd """ super(Connection, self).put_file(in_path, out_path) @@ -137,11 +192,32 @@ class Connection(ConnectionBase): local_cmd = [self._lxc_cmd] if self.get_option("project"): local_cmd.extend(["--project", self.get_option("project")]) - local_cmd.extend([ - "file", "push", - in_path, - f"{self.get_option('remote')}:{self._host()}/{out_path}" - ]) + + if self.get_option("remote_user") != "root": + uid, gid = self._get_remote_uid_gid() + local_cmd.extend( + [ + "file", + "push", + "--uid", + str(uid), + "--gid", + str(gid), + in_path, + f"{self.get_option('remote')}:{self._host()}/{out_path}", + ] + ) + else: + local_cmd.extend( + [ + "file", + "push", + in_path, + f"{self.get_option('remote')}:{self._host()}/{out_path}", + ] + ) + + self._display.vvvvv(f"PUT {local_cmd}", host=self._host()) local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]