Moving to Incus for users of pylxd

For LXD users who were also using the python module pylxd, I guess there will not be a “pyincus” python module to switch to.

So in order to move to Incus I guess we need to change our python code to either:

I’ve been planning on moving away from pylxd for a while now due to a bug that has been fixed but not made it into a release yet. So moving to Incus will be a good reason to do this.

I would expect pylxd to actually work fine against the Incus API.
You may need to tweak the connection logic a bit to force it to talk to the correct unix socket path when not using the network (TLS) API, but other than that, most of the functions will work just the same.

Incus is going to get features that LXD doesn’t have (and possibly vice-versa), but pylxd has been lagging behind a LOT over recent years, so that’s unlikely to really be visible.

Upstream Incus folks don’t have a lot of python knowledge so we’re not likely to create a pyincus of our own. However if someone wants to start such a project and maintain it, we’d happily host it.

Note that any such project would probably want to start from scratch as the pylxd design seems quite odd and hard to keep up to date with server-side changes. A lot of the design dates back from its integration with OpenStack which isn’t a thing anymore and so should allow for a more modern and easier to use module be developed.

This is good to know, I’ll try it in the short term.

But eventually I think I’ll replace it with a thin wrapper around the Incus REST API using python requests. If I come up with anything useful I’ll share it.

As there is a rest-api.yml, which is supposed to follow openapi specifications, it can be used to auto-generate clients. In that case, everyone can just generate it for their intended language.

Currently, it seem like the specification isn’t working too well, it’s not passing the openapi validators. Maybe that could be a starting point.

We generate the swagger metadata as a way to get API documentation.

It’s very unlikely to work to generate a functional client due to our API doing a fair few things that swagger cannot represent (mix of REST & websocket, TLS based authentication, …).

I tried it today, I use a default pylxd.client.Client() in my python code and initially got a unix socket exception:

pylxd.exceptions.ClientConnectionFailed: ('Connection aborted.', FileNotFoundError(2, 'No such file or directory'))

Had a quick peek at the code and worked out by default it uses /var/lib/lxd/unix.socket unless the LXD_DIR environment variable is set, in which case it will use $LXD_DIR/unix.socket. So I set the environment variable in the shell as follows:

export LXD_DIR=/var/lib/incus

Then ran the python again and hit the following exception at the point where it is creating a new instance using an image:


conftest.py:305: in new_container
    container = client.containers.create(config, wait=True)
../../../../.local/share/virtualenvs/test.1-hxo-P72l/lib/python3.10/site-packages/pylxd/models/instance.py:323: in create
    response = client.api[cls._endpoint].post(json=config, target=target)
../../../../.local/share/virtualenvs/test.1-hxo-P72l/lib/python3.10/site-packages/pylxd/client.py:216: in post
    self._assert_response(response, allowed_status_codes=(200, 201, 202))
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <pylxd.client._APINode object at 0x7f9a039801c0>, response = <Response [404]>, allowed_status_codes = (200, 201, 202)
stream = False, is_api = True

    def _assert_response(
        self, response, allowed_status_codes=(200,), stream=False, is_api=True
    ):
        """Assert properties of the response.

        LXD's API clearly defines specific responses. If the API
        response is something unexpected (i.e. an error), then
        we need to raise an exception and have the call points
        handle the errors or just let the issue be raised to the
        user.
        """
        if response.status_code not in allowed_status_codes:
            if response.status_code == 404:
>               raise exceptions.NotFound(response)
E               pylxd.exceptions.NotFound: not found

../../../../.local/share/virtualenvs/test.1-hxo-P72l/lib/python3.10/site-packages/pylxd/client.py:144: NotFound

Ran again with the debugger I see what was in the request attached to this 404 response:

(Pdb) print(response.request)
<PreparedRequest [POST]>
(Pdb) print(response.request.url)
http+unix://%2Fvar%2Flib%2Fincus%2Funix.socket/1.0/containers
(Pdb) print(response.request.path_url)
/1.0/containers
Pdb) print(response.request.headers)
{'User-Agent': 'python-requests/2.31.0', 'Accept-Encoding': 'gzip, deflate, br', 'Accept': '*/*', 'Connection': 'keep-alive', 'Content-Length': '87', 'Content-Type': 'application/json'}
(Pdb) print(response.request.body)
b'{"name": "psp-dbmaster", "source": {"type": "image", "alias": "centos/7/psp/dbmaster"}}'
(Pdb)

Looking at the Incus API spec there is no method named containers. The LXD API spec also does not contain a containers method. I’m assuming that in LXD this method was renamed from containers to instances (see Incus API POST instances) and in LXD there is a redirect, but in Incus there is not?

On a machine with both incus and lxd installed I can run the following curl commands:

curl --unix-socket /var/lib/lxd/unix.socket http://localhost/1.0/containers
200
curl --unix-socket /var/lib/lxd/unix.socket http://localhost/1.0/instances
200
curl --unix-socket /var/lib/incus/unix.socket http://localhost/1.0/containers
404
curl --unix-socket /var/lib/incus/unix.socket http://localhost/1.0/instances
200

So I will need to patch pylxd client.py to replace the containers method name with instances. To be continued…

Oh, so that’s bad because even in LXD those URLs are deprecated :slight_smile:
A patch to make pylxd use /instances/ instead shouldn’t be controversial at all as it’s the right thing to do even for LXD.

1 Like

I have it working now. In my code (which I wrote about 5 years ago) I was using pylxd.models.Container instead of pylxd.models.Instance. After correcting my code to use the latter everywhere, it’s now all working.

For example I changed things like this:

-    if(client.containers.exists(name)):
-        container = client.containers.get(name)
+    if(client.instances.exists(name)):
+        container = client.instances.get(name)

The advice to use instances changed in the pylxd docs progressively between 2.2.11, 2.3.0 and 2.3.1.

In summary, as long as you are using instances instead of containers in your python code and also setting the LXD_DIR environment variable to /var/lib/incus, it all works fine :slight_smile: