canonical / pylxd

Python module for LXD
https://pylxd.readthedocs.io/en/latest/
Apache License 2.0
251 stars 133 forks source link

Remove dependency on `requests_unixsocket` #583

Open simondeziel opened 3 weeks ago

simondeziel commented 3 weeks ago

The requests_unixsocket is abandoned and we have very little hope that https://github.com/msabramo/requests-unixsocket/pull/72 will ever be merged.

cloud-init folks have found a way to talk to /dev/lxd/sock using requests (without using requests_unixsocket) to implement the LXD datasource. This could be taken as a source of inspiration for our replacement implementation.

simondeziel commented 3 weeks ago

Related issues #579 and #581.

simondeziel commented 2 weeks ago

Here's a toy PoC:

#!/usr/bin/python3

"""Basic interaction with the LXD API over the Unix socket."""

# Python implementation of:
# $ curl -s --unix /var/snap/lxd/common/lxd/unix.socket http://unix.socket/1.0 | jq -r '.metadata.environment'
#{
#  "addresses": [],
#  "architectures": [
#    "x86_64",
#    "i686"
#  ],
#...
#}

# Heavily inspired from snapd's api-client.py:
# https://github.com/snapcore/snapd/blob/master/tests/main/theme-install/api-client/bin/api-client.py

import http.client
import json
import socket
import sys

# This class is a subclass of http.client.HTTPConnection that connects to a Unix socket instead of a TCP socket.
class UnixSocketHTTPConnection(http.client.HTTPConnection):
    def __init__(self, socket_path):
        super().__init__("pylxd")
        self._socket_path = socket_path

    def connect(self):
        s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
        s.connect(self._socket_path)
        self.sock = s

# This function connects to the Unix socket and requests the environment part of the metadata returned by LXD.
# Returns the environment content of the metadata, or an empty string if an error occurs.
def main():
    # Try to connect to the Unix socket and request the environment part of the metadata returned by LXD.
    # If an exception occurs, print an error message and return an empty string.
    try:
        conn = UnixSocketHTTPConnection("/var/snap/lxd/common/lxd/unix.socket")
        conn.request("GET", "/1.0")
        response = conn.getresponse()
        body = response.read().decode()
    except FileNotFoundError:
        print("missing socket", file=sys.stderr)
        return ""
    except http.client.HTTPException as e:
        print("HTTP exception:", e, file=sys.stderr)
        return ""
    finally:
        conn.close()

    # If the response status is not 200, print an error message and return an empty string.
    if response.status != 200:
        print("HTTP error:", response.status, file=sys.stderr)
        return ""

    # If the response body is missing or empty, print an error message and return an empty string.
    if not body:
        print("Missing/empty body", file=sys.stderr)
        return ""

    # Try to parse the response body as JSON, and extract the environment part of the metadata returned by LXD.
    try:
        data = json.loads(body)
    except json.JSONDecodeError as e:
        print("JSON decode exception:", e, file=sys.stderr)
        return ""

    if "metadata" in data:
        return data["metadata"].get("environment", "")

    return ""

if __name__ == "__main__":
    print(main())