How to communicate with /run/snapd.socket using Python?

Hi!

I can get a JSON response including the list of all snaps installed on a device by running the following command:

curl --unix-socket /run/snapd.socket http://localhost/v2/snaps

I would like to know if it’s possible to achieve a similar result using Python (and if possible, only the standard library).

Since /run/snapd.socket is a unix socket, I suppose I can connect to it with

import socket

s = socket.socket(socket.AF_UNIX)
s.settimeout(1)
s.connect("/run/snapd.socket")

But I don’t know how to send a request nor receive a response from this… I’ve tried using s.send("GET /v2/snaps"), but the send() function only returns the number of bytes sent, not the actual response…

Thanks in advance!

given that snapd speaks http over that socket, you probably want to make sure to send proper http protocol headers and such …

Thanks for the tip, @ogra! That’s what I was missing.

For the record:

import socket

s = socket.socket(socket.AF_UNIX)
s.settimeout(1)
s.connect("/run/snapd.socket")
s.send("GET /v2/snaps HTTP/1.1\r\nHost: localhost\r\n\r\n")
s.recv(1024)
# 'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nDate: Wed, 18 Jul 2018 10:05:59 GMT\r\nTransfer-Encoding: chunked\r\n\r\n17c3\r\n{"type":"sync","status-code":200,"status":"OK","result":[{"id":"","summary":"(...)

If you’re willing to move beyond the Python standard library, snapd-glib is probably the best option. On Ubuntu, you can install the necessary parts with:

sudo apt install python3-gi gir1.2-snapd-1

The binding uses gobject-introspection, so you can import it like so:

import gi
gi.require_version('Snapd', '1')
from gi.repository import Snapd

You can list available snaps with something like this:

c = Snapd.Client()
for snap in c.list_sync():
    print(snap.props.name)

You can poke around interactively with help() to discover how to use the API.

Thanks for the info!

I’m trying to stick to the standard lib for two reasons:

  • I need it for a snap that should be compatible with as many devices/platforms as possible
  • I’m stuck with Python 2 for the time being…

Plus, I only need to retrieve very basic info (snap names) and my tool is not meant at all to heavily use the snapd interface, so I guess the very basic socket communication will be enough for now :slight_smile:

However, I’ll keep the snapd-glib option in a corner of my head for later!

All of the above should function with Python 2 as well (just install python-gi instead of python3-gi).

As for accessing snapd from within a snap, note that you won’t be able to access that socket when running with strict confinement. The limit to what a strict confined snap is allowed to do is effectively the feature set of the snapctl command.

You can do this with classic confinement (which basically gives your app the same access as a traditional Linux app has), but you’ll need to provide a justification for that need to get it published on the store.

Note that the interface needed for a confined snap to talk to /run/snapd.socket is snapd-control, which a user cannot connect without an assertion.

What’re you wanting to list snaps for? There might be another way (which would need small changes on the snapd side), but it’d be interesting to understand the use case.

Thanks for the feedback.

I’m developing this tool for our QA team in order to report bugs on both Ubuntu Core and classic environments.

Up to now I was using the output of snap list to check for different things (like if a given snap was installed in order to go grab some logs from it if needed), but I recently got bitten by a UnicodeDecodeError because of the sometimes used in the output of snap list: lp:1782331.

A colleague mentioned talking to snapd socket directly, since relying on the output from snap command is not a good idea to begin with (since the output might change, etc.).

My use case is very basic:

  • I need to know the names of the snaps installed
  • I need to know if the tool itself is installed with --devmode or not

Hi!

After more than 2 years (!!!), I’ve revisited this topic and tried your method.

As long as I’m testing locally (i.e. without a snap), it works well \o/

In my snapcraft.yaml file, I added the two packages you mentioned in my program’s part:

  qabro:
    plugin: python
    source: .
    parse-info: [setup.py]
    ...
    stage-packages:
      ...
      - python3-gi
      - gir1.2-snapd-1
  ...

It seems both packages get pulled at build time:

$ snapcraft 
Launching a VM.
...
Get:1 gir1.2-snapd-1_1.49-0ubuntu0.18.04.2_amd64.deb [12.9 kB]                                                                                                                                                    
Fetched 12.9 kB in 0s (0 B/s)
...
Get:1 python3-gi_3.26.1-2ubuntu1_amd64.deb [153 kB]                                                                                                                                                               
Fetched 153 kB in 0s (0 B/s)
...
Snapping |                                                                                                                                                                                              
Snapped qabro_0.14dev_amd64.snap

But! my program fails to run.

If I investigate a bit:

$ snap run --shell qabro
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

pieq@cbrown:/home/pieq/dev/qabro$ python3
Python 3.6.9 (default, Jul 17 2020, 12:50:27) 
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import gi
>>> gi.require_version('Snapd', '1')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/snap/qabro/x1/usr/lib/python3/dist-packages/gi/__init__.py", line 130, in require_version
    raise ValueError('Namespace %s not available' % namespace)
ValueError: Namespace Snapd not available

$ ls /usr/lib/x86_64-linux-gnu/girepository-1.0/Snapd-1.typelib
ls: cannot access '/usr/lib/x86_64-linux-gnu/girepository-1.0/Snapd-1.typelib': No such file or directory

According to the Ubuntu package, installing gir1.2-snapd-1 should create /usr/lib/x86_64-linux-gnu/girepository-1.0/Snapd-1.typelib.

Any idea what’s wrong here?

More info:

I found the problem!

The typelib files are stored in $SNAP/usr/lib/x86_64-linux-gnu/girepository-1.0, so, following the advice found in this topic, I added GI_TYPELIB_PATH to my snap’s environment:

apps:
  qabro:
    command: qabro
    environment:
      GI_TYPELIB_PATH: $SNAP/usr/lib/x86_64-linux-gnu/girepository-1.0
      ...

and now it runs fine!

I’d probably change that to:

GI_TYPELIB_PATH: $SNAP/usr/lib/$SNAPCRAFT_ARCH_TRIPLET/girepository-1.0

So you’re not hard coding an amd64 path into your snap’s build rules. Snapcraft will replace the $SNAPCRAFT_ARCH_TRIPLET with the appropriate value for the architecture you’re building the snap for.

Note also that it is unlikely that you’ll be able to publish a snap to the main snap store that has the snapd-control plug needed to connect to /run/snapd.socket. This interface is heavily restricted because it is essentially equivalent to granting root access to the host system.

Thanks! Since my snap is used on armhf and arm64 archs, it’s gonna be useful. (although I couldn’t find any information online about this environment variable)

This snap is currently requiring --devmode at install time precisely because of this. I know it’s ugly, but it’s a bug reporting program that needs to gather a lot of logs from different parts of the system under test, so there is no workaround for the moment.