1"""
2Manage LXD images.
3
4.. versionadded:: 2019.2.0
5
6.. link: https://github.com/lxc/pylxd/blob/master/doc/source/installation.rst
7
8.. note:
9
10    - :role:`pylxd <link>` version 2 is required to let this work,
11      currently only available via pip.
12
13        To install on Ubuntu:
14
15        $ apt-get install libssl-dev python-pip
16        $ pip install -U pylxd
17
18    - you need lxd installed on the minion
19      for the init() and version() methods.
20
21    - for the config_get() and config_get() methods
22      you need to have lxd-client installed.
23
24
25:maintainer: René Jochum <rene@jochums.at>
26:maturity: new
27:depends: python-pylxd
28:platform: Linux
29"""
30
31
32from salt.exceptions import CommandExecutionError, SaltInvocationError
33
34__docformat__ = "restructuredtext en"
35
36__virtualname__ = "lxd_image"
37
38
39def __virtual__():
40    """
41    Only load if the lxd module is available in __salt__
42    """
43    if "lxd.version" in __salt__:
44        return __virtualname__
45    return (False, "lxd module could not be loaded")
46
47
48def present(
49    name,
50    source,
51    aliases=None,
52    public=None,
53    auto_update=None,
54    remote_addr=None,
55    cert=None,
56    key=None,
57    verify_cert=True,
58):
59    """
60    Ensure an image exists, copy it else from source
61
62    name :
63        An alias of the image, this is used to check if the image exists and
64        it will be added as alias to the image on copy/create.
65
66    source :
67        Source dict.
68
69        For an LXD to LXD copy:
70
71        .. code-block:: yaml
72
73            source:
74                type: lxd
75                name: ubuntu/xenial/amd64  # This can also be a fingerprint.
76                remote_addr: https://images.linuxcontainers.org:8443
77                cert: ~/.config/lxd/client.crt
78                key: ~/.config/lxd/client.key
79                verify_cert: False
80
81        .. attention:
82
83            For this kind of remote you also need to provide:
84            - a https:// remote_addr
85            - a cert and key
86            - verify_cert
87
88        From file:
89
90        .. code-block:: yaml
91
92            source:
93                type: file
94                filename: salt://lxd/files/busybox.tar.xz
95                saltenv: base
96
97        From simplestreams:
98
99        .. code-block:: yaml
100
101            source:
102                type: simplestreams
103                server: https://cloud-images.ubuntu.com/releases
104                name: xenial/amd64
105
106        From an URL:
107
108        .. code-block:: yaml
109
110            source:
111                type: url
112                url: https://dl.stgraber.org/lxd
113
114    aliases :
115        List of aliases to append, can be empty.
116
117    public :
118        Make this image public available on this instance?
119            None on source_type LXD means copy source
120            None on source_type file means False
121
122    auto_update :
123        Try to auto-update from the original source?
124            None on source_type LXD means copy source
125            source_type file does not have auto-update.
126
127    remote_addr :
128        An URL to a remote Server, you also have to give cert and key if you
129        provide remote_addr!
130
131        Examples:
132            https://myserver.lan:8443
133            /var/lib/mysocket.sock
134
135    cert :
136        PEM Formatted SSL Zertifikate.
137
138        Examples:
139            ~/.config/lxc/client.crt
140
141    key :
142        PEM Formatted SSL Key.
143
144        Examples:
145            ~/.config/lxc/client.key
146
147    verify_cert : True
148        Wherever to verify the cert, this is by default True
149        but in the most cases you want to set it off as LXD
150        normally uses self-signed certificates.
151    """
152    if aliases is None:
153        aliases = []
154
155    # Create a copy of aliases, since we're modifying it here
156    aliases = aliases[:]
157    ret = {
158        "name": name,
159        "source": source,
160        "aliases": aliases,
161        "public": public,
162        "auto_update": auto_update,
163        "remote_addr": remote_addr,
164        "cert": cert,
165        "key": key,
166        "verify_cert": verify_cert,
167        "changes": {},
168    }
169
170    image = None
171    try:
172        image = __salt__["lxd.image_get_by_alias"](
173            name, remote_addr, cert, key, verify_cert, _raw=True
174        )
175    except CommandExecutionError as e:
176        return _error(ret, str(e))
177    except SaltInvocationError as e:
178        # Image not found
179        pass
180
181    if image is None:
182        if __opts__["test"]:
183            # Test is on, just return that we would create the image
184            msg = 'Would create the image "{}"'.format(name)
185            ret["changes"] = {"created": msg}
186            return _unchanged(ret, msg)
187
188        try:
189            if source["type"] == "lxd":
190                image = __salt__["lxd.image_copy_lxd"](
191                    source["name"],
192                    src_remote_addr=source["remote_addr"],
193                    src_cert=source["cert"],
194                    src_key=source["key"],
195                    src_verify_cert=source.get("verify_cert", True),
196                    remote_addr=remote_addr,
197                    cert=cert,
198                    key=key,
199                    verify_cert=verify_cert,
200                    aliases=aliases,
201                    public=public,
202                    auto_update=auto_update,
203                    _raw=True,
204                )
205
206            if source["type"] == "file":
207                if "saltenv" not in source:
208                    source["saltenv"] = __env__
209                image = __salt__["lxd.image_from_file"](
210                    source["filename"],
211                    remote_addr=remote_addr,
212                    cert=cert,
213                    key=key,
214                    verify_cert=verify_cert,
215                    aliases=aliases,
216                    public=False if public is None else public,
217                    saltenv=source["saltenv"],
218                    _raw=True,
219                )
220
221            if source["type"] == "simplestreams":
222                image = __salt__["lxd.image_from_simplestreams"](
223                    source["server"],
224                    source["name"],
225                    remote_addr=remote_addr,
226                    cert=cert,
227                    key=key,
228                    verify_cert=verify_cert,
229                    aliases=aliases,
230                    public=False if public is None else public,
231                    auto_update=False if auto_update is None else auto_update,
232                    _raw=True,
233                )
234
235            if source["type"] == "url":
236                image = __salt__["lxd.image_from_url"](
237                    source["url"],
238                    remote_addr=remote_addr,
239                    cert=cert,
240                    key=key,
241                    verify_cert=verify_cert,
242                    aliases=aliases,
243                    public=False if public is None else public,
244                    auto_update=False if auto_update is None else auto_update,
245                    _raw=True,
246                )
247        except CommandExecutionError as e:
248            return _error(ret, str(e))
249
250    # Sync aliases
251    if name not in aliases:
252        aliases.append(name)
253
254    old_aliases = {str(a["name"]) for a in image.aliases}
255    new_aliases = set(map(str, aliases))
256
257    alias_changes = []
258    # Removed aliases
259    for k in old_aliases.difference(new_aliases):
260        if not __opts__["test"]:
261            __salt__["lxd.image_alias_delete"](image, k)
262            alias_changes.append('Removed alias "{}"'.format(k))
263        else:
264            alias_changes.append('Would remove alias "{}"'.format(k))
265
266    # New aliases
267    for k in new_aliases.difference(old_aliases):
268        if not __opts__["test"]:
269            __salt__["lxd.image_alias_add"](image, k, "")
270            alias_changes.append('Added alias "{}"'.format(k))
271        else:
272            alias_changes.append('Would add alias "{}"'.format(k))
273
274    if alias_changes:
275        ret["changes"]["aliases"] = alias_changes
276
277    # Set public
278    if public is not None and image.public != public:
279        if not __opts__["test"]:
280            ret["changes"]["public"] = "Setting the image public to {!s}".format(public)
281            image.public = public
282            __salt__["lxd.pylxd_save_object"](image)
283        else:
284            ret["changes"]["public"] = "Would set public to {!s}".format(public)
285
286    if __opts__["test"] and ret["changes"]:
287        return _unchanged(ret, "Would do {} changes".format(len(ret["changes"].keys())))
288
289    return _success(ret, "{} changes".format(len(ret["changes"].keys())))
290
291
292def absent(name, remote_addr=None, cert=None, key=None, verify_cert=True):
293    """
294    name :
295        An alias or fingerprint of the image to check and delete.
296
297    remote_addr :
298        An URL to a remote Server, you also have to give cert and key if you
299        provide remote_addr!
300
301        Examples:
302            https://myserver.lan:8443
303            /var/lib/mysocket.sock
304
305    cert :
306        PEM Formatted SSL Zertifikate.
307
308        Examples:
309            ~/.config/lxc/client.crt
310
311    key :
312        PEM Formatted SSL Key.
313
314        Examples:
315            ~/.config/lxc/client.key
316
317    verify_cert : True
318        Wherever to verify the cert, this is by default True
319        but in the most cases you want to set it off as LXD
320        normally uses self-signed certificates.
321    """
322    ret = {
323        "name": name,
324        "remote_addr": remote_addr,
325        "cert": cert,
326        "key": key,
327        "verify_cert": verify_cert,
328        "changes": {},
329    }
330    image = None
331    try:
332        image = __salt__["lxd.image_get_by_alias"](
333            name, remote_addr, cert, key, verify_cert, _raw=True
334        )
335    except CommandExecutionError as e:
336        return _error(ret, str(e))
337    except SaltInvocationError as e:
338        try:
339            image = __salt__["lxd.image_get"](
340                name, remote_addr, cert, key, verify_cert, _raw=True
341            )
342        except CommandExecutionError as e:
343            return _error(ret, str(e))
344        except SaltInvocationError as e:
345            return _success(ret, 'Image "{}" not found.'.format(name))
346
347    if __opts__["test"]:
348        ret["changes"] = {"removed": 'Image "{}" would get deleted.'.format(name)}
349        return _success(ret, ret["changes"]["removed"])
350
351    __salt__["lxd.image_delete"](image)
352
353    ret["changes"] = {"removed": 'Image "{}" has been deleted.'.format(name)}
354    return _success(ret, ret["changes"]["removed"])
355
356
357def _success(ret, success_msg):
358    ret["result"] = True
359    ret["comment"] = success_msg
360    if "changes" not in ret:
361        ret["changes"] = {}
362    return ret
363
364
365def _unchanged(ret, msg):
366    ret["result"] = None
367    ret["comment"] = msg
368    if "changes" not in ret:
369        ret["changes"] = {}
370    return ret
371
372
373def _error(ret, err_msg):
374    ret["result"] = False
375    ret["comment"] = err_msg
376    if "changes" not in ret:
377        ret["changes"] = {}
378    return ret
379