summaryrefslogtreecommitdiffstats
path: root/ansible_collections/community/general/plugins/connection/incus.py
blob: 81d6f971c708cd2914effa9deaf2b05926ce8868 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# -*- coding: utf-8 -*-
# Based on lxd.py (c) 2016, Matt Clay <matt@mystile.com>
# (c) 2023, Stephane Graber <stgraber@stgraber.org>
# Copyright (c) 2023 Ansible Project
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = """
    author: Stéphane Graber (@stgraber)
    name: incus
    short_description: Run tasks in Incus instances via the Incus CLI.
    description:
        - Run commands or put/fetch files to an existing Incus instance using Incus CLI.
    version_added: "8.2.0"
    options:
      remote_addr:
        description:
            - The instance identifier.
        default: inventory_hostname
        vars:
            - name: inventory_hostname
            - name: ansible_host
            - name: ansible_incus_host
      executable:
        description:
            - The shell to use for execution inside the instance.
        default: /bin/sh
        vars:
            - name: ansible_executable
            - name: ansible_incus_executable
      remote:
        description:
            - The name of the Incus remote to use (per C(incus remote list)).
            - Remotes are used to access multiple servers from a single client.
        default: local
        vars:
            - name: ansible_incus_remote
      project:
        description:
            - The name of the Incus project to use (per C(incus project list)).
            - Projects are used to divide the instances running on a server.
        default: default
        vars:
            - name: ansible_incus_project
"""

import os
from subprocess import call, Popen, PIPE

from ansible.errors import AnsibleError, AnsibleConnectionFailure, AnsibleFileNotFound
from ansible.module_utils.common.process import get_bin_path
from ansible.module_utils._text import to_bytes, to_text
from ansible.plugins.connection import ConnectionBase


class Connection(ConnectionBase):
    """ Incus based connections """

    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)

        self._incus_cmd = get_bin_path("incus")

        if not self._incus_cmd:
            raise AnsibleError("incus command not found in PATH")

    def _connect(self):
        """connect to Incus (nothing to do here) """
        super(Connection, self)._connect()

        if not self._connected:
            self._display.vvv(u"ESTABLISH Incus CONNECTION FOR USER: root",
                              host=self._instance())
            self._connected = True

    def _instance(self):
        # Return only the leading part of the FQDN as the instance name
        # as Incus instance names cannot be a FQDN.
        return self.get_option('remote_addr').split(".")[0]

    def exec_command(self, cmd, in_data=None, sudoable=True):
        """ execute a command on the Incus host """
        super(Connection, self).exec_command(cmd, in_data=in_data, sudoable=sudoable)

        self._display.vvv(u"EXEC {0}".format(cmd),
                          host=self._instance())

        local_cmd = [
            self._incus_cmd,
            "--project", self.get_option("project"),
            "exec",
            "%s:%s" % (self.get_option("remote"), self._instance()),
            "--",
            self._play_context.executable, "-c", cmd]

        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')

        process = Popen(local_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
        stdout, stderr = process.communicate(in_data)

        stdout = to_text(stdout)
        stderr = to_text(stderr)

        if stderr == "Error: Instance is not running.\n":
            raise AnsibleConnectionFailure("instance not running: %s" %
                                           self._instance())

        if stderr == "Error: Instance not found\n":
            raise AnsibleConnectionFailure("instance not found: %s" %
                                           self._instance())

        return process.returncode, stdout, stderr

    def put_file(self, in_path, out_path):
        """ put a file from local to Incus """
        super(Connection, self).put_file(in_path, out_path)

        self._display.vvv(u"PUT {0} TO {1}".format(in_path, out_path),
                          host=self._instance())

        if not os.path.isfile(to_bytes(in_path, errors='surrogate_or_strict')):
            raise AnsibleFileNotFound("input path is not a file: %s" % in_path)

        local_cmd = [
            self._incus_cmd,
            "--project", self.get_option("project"),
            "file", "push", "--quiet",
            in_path,
            "%s:%s/%s" % (self.get_option("remote"),
                          self._instance(),
                          out_path)]

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

        call(local_cmd)

    def fetch_file(self, in_path, out_path):
        """ fetch a file from Incus to local """
        super(Connection, self).fetch_file(in_path, out_path)

        self._display.vvv(u"FETCH {0} TO {1}".format(in_path, out_path),
                          host=self._instance())

        local_cmd = [
            self._incus_cmd,
            "--project", self.get_option("project"),
            "file", "pull", "--quiet",
            "%s:%s/%s" % (self.get_option("remote"),
                          self._instance(),
                          in_path),
            out_path]

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

        call(local_cmd)

    def close(self):
        """ close the connection (nothing to do here) """
        super(Connection, self).close()

        self._connected = False