Skip to content
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

incus_connection: Allow non-root users to connect to an instance #9743

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelogs/fragments/9743-incus_connection-nonroot-user.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- incus connection plugin - adds ``remote_user`` and ``incus_become_method`` parameters for allowing a non-root user to connect to an Incus instance (https://github.com/ansible-collections/community.general/pull/9743).
115 changes: 100 additions & 15 deletions plugins/connection/incus.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,15 @@
vars:
- name: ansible_executable
- name: ansible_incus_executable
incus_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: incus_become_method
version_added: 10.4.0
remote:
description:
- The name of the Incus remote to use (per C(incus remote list)).
Expand All @@ -40,6 +49,22 @@
default: local
vars:
- name: ansible_incus_remote
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:
- The name of the Incus project to use (per C(incus project list)).
Expand All @@ -64,7 +89,6 @@ class Connection(ConnectionBase):

transport = "incus"
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)
Expand All @@ -79,10 +103,34 @@ def _connect(self):
super(Connection, self)._connect()

if not self._connected:
self._display.vvv("ESTABLISH Incus CONNECTION FOR USER: root",
self._display.vvv(f"ESTABLISH Incus CONNECTION FOR USER: {self.get_option('remote_user')}",
host=self._instance())
self._connected = True

def _build_command(self, cmd) -> str:
"""build the command to execute on the incus host"""

exec_cmd = [
self._incus_cmd,
"--project", self.get_option("project"),
"exec",
f"{self.get_option('remote')}:{self._instance()}",
"--"]

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 'incus exec' with become method: {self.get_option('incus_become_method')}",
host=self._instance(),
)
exec_cmd.extend(
[self.get_option("incus_become_method"), self.get_option("remote_user"), "-c"]
)

exec_cmd.extend([self.get_option("executable"), "-c", cmd])

return exec_cmd

def _instance(self):
# Return only the leading part of the FQDN as the instance name
# as Incus instance names cannot be a FQDN.
Expand All @@ -95,13 +143,8 @@ def exec_command(self, cmd, in_data=None, sudoable=True):
self._display.vvv(f"EXEC {cmd}",
host=self._instance())

local_cmd = [
self._incus_cmd,
"--project", self.get_option("project"),
"exec",
f"{self.get_option('remote')}:{self._instance()}",
"--",
self._play_context.executable, "-c", cmd]
local_cmd = self._build_command(cmd)
self._display.vvvvv(f"EXEC {local_cmd}", host=self._instance())

local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]
in_data = to_bytes(in_data, errors='surrogate_or_strict', nonstring='passthru')
Expand All @@ -120,6 +163,25 @@ def exec_command(self, cmd, in_data=None, sudoable=True):

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 Incus """
super(Connection, self).put_file(in_path, out_path)
Expand All @@ -130,12 +192,35 @@ def put_file(self, in_path, out_path):
if not os.path.isfile(to_bytes(in_path, errors='surrogate_or_strict')):
raise AnsibleFileNotFound(f"input path is not a file: {in_path}")

local_cmd = [
self._incus_cmd,
"--project", self.get_option("project"),
"file", "push", "--quiet",
in_path,
f"{self.get_option('remote')}:{self._instance()}/{out_path}"]
if self.get_option("remote_user") != "root":
uid, gid = self._get_remote_uid_gid()
local_cmd = [
self._incus_cmd,
"--project",
self.get_option("project"),
"file",
"push",
"--uid",
str(uid),
"--gid",
str(gid),
"--quiet",
in_path,
f"{self.get_option('remote')}:{self._instance()}/{out_path}",
]
else:
local_cmd = [
self._incus_cmd,
"--project",
self.get_option("project"),
"file",
"push",
"--quiet",
in_path,
f"{self.get_option('remote')}:{self._instance()}/{out_path}",
]

self._display.vvvvv(f"PUT {local_cmd}", host=self._instance())

local_cmd = [to_bytes(i, errors='surrogate_or_strict') for i in local_cmd]

Expand Down