`cups-control` interface needs to work with CUPS Debs, CUPS Snap, and on Ubuntu Core

Up to now we have installed snapped applications on standard Ubuntu. If they plug the cups-control interface, they get access to the socket of the installed Debian packages of CUPS, /run/cups/cups.sock and to the D-Bus notification facility of the same Debian packages of CUPS.

Now we get 2 new scenarios where these applications should also work. The first is Ubuntu 23.10, where we will switch from the Debian packages of CUPS to the CUPS Snap, meaning that such applications would need to access the snapped CUPS daemon instead of a classically installed one. The other scenario is Ubuntu Core, which does not contain a CUPS printing stack in its immutable image and the user is supposed to install the CUPS Snap to get printing functionality. Dabian packages cannot get installed here.

The original scenario using the Debian packages of CUPS on classic Ubuntu is well established and works.

The other scenarios can get made to work by manually connecting the cups-control plug to CUPS Snap’s cups:cups-control slot, but they fail if the user grabs an application from the Snap Store for which cups-control auto-connection got granted and the application fails to access CUPS due to the auto-connection being done to the wrong slot or not at all. Also manual connection not supplying the slot on the command line (expecting auto-selection) fails.

This thread is about to find a fix for this. An application which plugs cups-control and has auto-connection for it granted by the Snap Store team should always auto-connect to the correct xxx:cups-control slot so that it get full administrative access to the CUPS daemon in use, independent whether this daemon is provided by classic packages or the CUPS Snap, and independent whether we are running a classic distro or Ubuntu Core.

Tests show success and failure in the different scenarios:

1. Classic Ubuntu with CUPS from Debian packages (SUCCESS)

This is the well-tested “standard” case for which the :cups-control slot of of snapd got originally designed and this just works. Auto-connection or manual connection without specifiying the slot

$ sudo snap connect client-snap:cups-control

connects to snapd’s :cups-control slot and this just works using the CUPS installed via Debian packages and taking commands through the standard socket /run/cups/cups.sock.

2. Classic Ubuntu with CUPS from CUPS Snap (FAIL)

Now I have tested the scenario which we want to have in Ubuntu 23.10, the CUPS Snap being the print environment of the system, no Debian packages of CUPS installed.

For that I have stopped and disabled the Debian CUPS via systemctl, installed the CUPS Snap and set its no-proxy flag so that it runs in its standalone mode, see my call for testing for detailed instructions.

When I install a Snap which auto-connects cups-control or manually connect cups-control without specifying the slot

$ sudo snap connect client-snap:cups-control

the Snap’s cups-control plug gets always connected to snapd’s :cups-control slot, independent whether the slot is backed by a CUPS daemon or not. The slot which would be needed here is the CUPS Snap’s cups:cups-control slot.

Now, when you try it out, simply by printing from Firefox (Firefox uses the cups-control plug and not cups as an app which prints should use) you will not see any failure as printing just works in this case, but if you try with a Snap which actually does administrative operations on CUPS (like modifying queues) these operations will fail. The non-administrative operations work due to the fact that the connection to :cups-control gives access to /run/cups/cups.sock and when the CUPS Snap is in standalone mode (no classically installed CUPS in the way) it also listens via /run/cups/cups.sock. The administrative operations do not work (you get a Forbidden) due to the snapped CUPS daemon’s Snap mediation. It allows administrative operations from external client Snaps only if they plug the CUPS Snap’s cups:cups-control slot, plugging any cups-control slot somewhere around is not good enough!

3. Ubuntu Core (FAIL)

This scenario I did not test by myself, but a user who has created a Snap of an application doing administrative actions on CUPS and wants to use this Snap on Ubuntu Core has done so and I have helped them making it work. See this thread, especially start reading from this post on.

In Ubuntu Core we do not even have a :cups-control slot, so snapd seems to identify on startup that it is under Ubuntu Core where a classically installed CUPS does not exist and so does not open the slot. Manually connecting cups-control without specifying the slot (or auto-connection) fails as follows:

$ sudo snap connect client-snap:cups-control
error: snap "snapd" has no "cups-control" interface slots

And, as with the CUIPS Snap being used on a classic distro, manually connecting explicitly to the CUPS Snap’s slot works:

$ sudo snap connect client-snap:cups-control cups:cups-control

Do you want to test it?

See my call for testing upstream to get scenarios 2 and 3.

Use the CUPS Snap from the --edge channel:

$ sudo snap refresh --edge cups

Install Snaps which plug cups-control from the Snap Store (like Firefox).

Check connections of application snaps:

$ snap connections client-snap

with client-snap replaced by the name of the actual Snap you are trying out. and of CUPS:

$ snap connections cups

Here always both plugs and slots are shown. Slots without Snap name (starting with :) are from snapd.

Connect cups-control manually, without specifting a slot, it always connects to :cups-control (of snpad):

$ sudo snap connect client-snap:cups-control

Connect to the cups-control slot of the CUPS Snap:

$ sudo snap connect client-snap:cups-control cups:cups-control

I am not aware of a printer setup tool in the Snap Store which we could use as an example for testing here. So do the following if you want to test: Copy the lines below into an editor and save the file as snapcraft.yaml:

name: cups-admin-test
base: core22 # The base snap is the execution environment for this snap
version: 0.1.0
summary: CUPS admin and non-admin tasks out of a Snap 
description: Testing interfaces for the CUPS Snap
grade: stable
confinement: strict

apps:
  lpinfo:
    command: usr/sbin/lpinfo
    plugs: [network, cups-control]
  lpadmin:
    command: usr/sbin/lpadmin
    plugs: [network, avahi-observe, home, cups-control]
  lpstat:
    command: usr/bin/lpstat
    plugs: [network, avahi-observe, cups-control]
  lpoptions:
    command: usr/bin/lpoptions
    plugs: [network, home, cups-control]
  lp:
    command: usr/bin/lp
    plugs: [network, home, cups-control]
  cancel:
    command: usr/bin/cancel
    plugs: [network, cups-control]
  lpmove:
    command: usr/sbin/lpmove
    plugs: [network, cups-control]
  cupsenable:
    command: usr/sbin/cupsenable
    plugs: [network, cups-control]
  cupsdisable:
    command: usr/sbin/cupsdisable
    plugs: [network, cups-control]
  cupsaccept:
    command: usr/sbin/cupsaccept
    plugs: [network, cups-control]
  cupsreject:
    command: usr/sbin/cupsreject
    plugs: [network, cups-control]
  accept:
    command: usr/sbin/cupsaccept
    plugs: [network, cups-control]
  reject:
    command: usr/sbin/cupsreject
    plugs: [network, cups-control]
  cupsctl:
    command: usr/sbin/cupsctl
    plugs: [network, cups-control]

parts:
  cups-client:
    plugin: dump
    source: .
    stage-packages:
        - cups-client
        - libcups2
    prime:
        - usr/bin/*
        - usr/sbin/*
        - usr/lib/*

Then run the command

snapcraft pack

and when this has finished, run

sudo snap install --dangerous cups-admin-test_0.1.0_amd64.snap

and use the commands shown above with cups-admin-test as client Snap.

A non-invasive administrative operation for testing is lpinfo -v. So run

cups-admin-test.lpinfo -v

to see whether the test Snap has admin permissions on CUPS.

Summary of the problem

Automatic connecting of the cups-control plug of a Snap, regardless whether by installing a Snap with appropriate permissions or by manually connecting without specifying the slot, always connects to snapd’s :cups-control slot, independently whether this actually works or whether the slot even exists.

As an awkward workaround one can manually connect to the CUPS Snap’s cups:cups-control by explicitly specifying the slot and then everything works as expected.

This is an inconsistent behavior for users. They cannot install a Snap from the Snap Store which has auto-connection permission for cups-control and it just works. It only just works on a classic distro wioth classic CUPS installation. It will fail and require manual connection on Ubuntu 23.10 (which uses CUPS Snap) or Ubuntu Core (also uses CUPS Snap).

Request for a fix

The fix is needed to be implemented in snapd. snapd provides the :cups-control slot to make it easy to snap printer setup tools or generally software which does administrative tasks on CUPS. As Snap is all about distribution-independent packaging, it must be possible to snap such applications without needint to care about the destination system, neither in the upstream code, the snapping, or the documentation for the Snap. Therefore an app plugging cups-control must always be able to admin the CUPS which the destination system provides for its users to be able to print.

So snapd needs to always handle the request to plug cups-control of a client Snap to the correct slot, without needing any manual action of the user and without requiring from the user to know about the 3 mentioned scenarios.

snapd would somehow need to check whether the socket file /run/cups/cups.sock is backed by an unsnapped CUPS daemon (cupsd). There are probably tools to determine the executable name with its path for what backs a socket file. If the path does not contain /snap/ one could assume that the cupsd is unsnapped. In this case, and only in this case, snapd should behave as before. If /run/cups/cups.sock does not exist (for example as it could not get created by the CUPS Snap running under the immutable Ubuntu Core) or the cupsd backing it is snapped, it should connect the plug to cups:cups-control or somehow mirror cups:cups-control into :cups-control but in a way that the Snap mediation of cupsd does not fail (I would modify/extend it if needed).

snapd should not simply check for the presence of the CUPS Snap here, as it could run in parallel to a classic CUPS installation (“proxy” mode and “parallel” mode of the CUPS Snap).

Discussion and further thoughts

I have discussed this with both @jamesh and @mvo. Especially there were the follwing thoughts:

  • snapd developers say that snapd does not need to care of special cases like printing: This is wrong, as snapd provides the :cups-control slot. This slot must always work and snappers of applications rely (and users) on it.
  • The CUPS Snap should get replaced by a an actual CUPS Snap and a CUPS Proxy Snap. This does not eliminate this problem. It only eases plug and slot naming a bit, and simplifies the startup scripts of the CUPS Snap somewhat, but it makes migration from classic CUPS to the CUPS Snap unnecessarily complex.
  • Applications which only print but do not manage CUPS (like Firefox) should use the cups plug and not the cups-control plug, but this also does not eliminate our problem here.

I hope we can solve this before Ubuntu 23.10 to create a smooth user experience.

1 Like

As previously discussed with you, to have auto-connect work there needs to be a single connection candidate for the application snap’s cups-control plug: if there are two candidates, then the plug won’t auto-connect to either of them.

While we can’t make the system:cups-control slot appear or disappear based on presence of the CUPS snap, @pedronis made the suggestion that we could use the interface’s AutoConnect method to remove one of the slots as a connection candidate. All we need to do is determine what that code should do.

What would be helpful is if you could provide some guidance over what we should do in the various configurations. I’d imagine this would somewhat mirror how you decide whether to run the CUPS proxy within your snap.

On Ubuntu Core systems, I don’t think we need to do anything special since there is no implicit system:cups-control slot. For classic systems, there seems to be 4 configurations:

  1. host system does not have cupsd, and the cups snap is not installed
  2. host system does have cupsd, and the cups snap is not installed
  3. host system does not have cupsd, and the cups snap is installed
  4. host system does not have cupsd, and the cups snap is installed

Cases (3) and (4) are the cases where we have two connection candidates (system:cups-control and cups:cups-server). Further more, in (3) we require the cups:cups-control plug to connect to system:cups-control so the snap can forward jobs to the system print spooler.

I think the following pseudo-code would

if host_system_has_cupsd {
    allow auto-connect to implicit cups-control slots and block to application snap cups-control slots
} else {
    allow auto-connect to application snap cups-control slots, and block implicit cups-control slots
}

That leaves open the question of what we should look for to determine whether the host system has a print spooler. We can’t look for /run/cups/cups.sock, since the snapped cups might create that. Would presence of /usr/sbin/cupsd be reliable? Or presence of a particular named systemd unit?

First to not confuse the further development by renaming plugs and slots of the CUPS Snap in the middle of coding the needed changes, I have promoted current Edge to Stable as after the last release to Stable I have done the planned renamings to be able to test them. I tested them without any bad observations in the last days and the initial posting of this thread is already based on the renaming.

I did the following renamings:

  • Slot cups:cups-server -> cups:cups-control
  • Plug cups:cups-control -> cups:cups-host

A Snap cannot have a plug and a slot with the same name, so I had to decide where I use cups-control or whether I do not use this one at all.

I settled on naming the slot cups-control as it is the same name as the system (snapd) uses and the slot is actually user-facing. Users sometimes have to manually connect an application’s plug which is named cups-control to a corresponding slot, which is ideally named cups-control, too.

But the plug of the CUPS Snap to plug into the system’s cups-control so that the snapped cupsd can access the system’s classically installed cupsd in “proxy” mode connects automatically and therefore is invisible for the user, so its name is not that important.

The exact names of the plugs and slots are not particularly important here.

What I really need is some guidance on what the host_system_has_cupsd check from my pseudo-code should actually be checking. This is something that would need to work across all platforms that snapd works on that have CUPS installed.

The general rule is that if a working classically installed CUPS is there that it should be used, as in such a case an also installed CUPS Snap is usually running in the “proxy” mode, being a firewall for the classically installed CUPS.

The CUPS Snap can run in a “parallel” mode though, meaning that the CUPS Snap’s and the classically installed CUPS daemon’s run independently, but this is an experimental mode, for development and debugging, only to be used by users who know what they are doing and so also know how to manually connect Snaps where needed.

So then we can proceed as follows:

  • Are we running Ubuntu Core (generally an all-Snap, immutable distribution)? If yes, there is never a classically installed CUPS, so if CUPS Snap is there, connect to cups:cups-control. DONE.
  • We are running a classic (DEB, RPM, …, non-immutable) distribution or a (Flatpak-based?) immutable distribution with CUPS in its immutable core (I do not recommend such a configuration):
    • Check whether there is an active, classically installed CUPS, usually whether a classically installed cupsd is running (executable path does not contain /snap/), AND/OR /run/cups/cups.sock is connected with a classically installed cupsd executable (needs tool find it out; ss? lsof? Or eqivalent library API functions, which one?). If classically installed, active CUPS daemon is there, connect to :cups-control. DONE.
    • No classically installed active CUPS cdaemon there, but CUPS Snap? Connect to cups:cups-control. DONE.
  • No CUPS at all? Do not connect. DONE.

So this is more or less as you have proposed @jamesh What we need to find out now is how to best determine whether we have an active classically installed CUPS daemon.

There are principally 2 ways:

  • cupsd process running, path of the executable does not contain /snap/.
  • Find out process attached to /run/cups/cups.sock. Is it a cupsd without /snap/ in its path or without Snap confinement?

I would opt for the second, it is more accurate than the first.

I think, the command line tools ss and lsof are capable of finding a process behind a socket. Having found the ID by this, ps reveals the command line to check the path and snapctl (or equivalent library function) could perhaps tell whether the process is Snap-confined (and therefore NOT a classically installed cupsd).

I’d prefer something that can be deduced from file paths rather than checking for running processes: both because it’ll be faster and because it will remove startup order dependence.

Also: I think we really think the check needs to be looking for the presence of the host system cupsd rather than presence of the cups snap. This code might run during the install of the cups snap, so I wouldn’t want to rely on any files that snap might create.

So you mean that one simply checks for presence of /usr/sbin/cupsd (plus its executable bit perhaps)? Generally it would work on most distros using packaging like DEB or RPM, would not work if user installs CUPS from source, as then often we get /usr/local/sbin/cupsd. We could check both, or whether we have cupsd in the $PATH (but then check whether there is a cupsd whose path does not contain /snap/).

This naturally does not cover if the user has disabled the classically installed cupsd via systend (no other PID 1 system needs to get considered as Snap requires systemd). One could check that if it is not too time/resource-consuming …

I’m not sure whether self-built installs are much of a concern.

I guess a related question is: how do you plan to decide whether the cups snap runs in proxy mode or full stack mode? The answer to that question is likely to be the same as to which cups-control slot should be used. So it’d be reasonable for them both to use the same test.

The CUPS Snap tests the presence and read bit of /etc/cups/cupsd.conf to determine that there is a classic CUPS installation. If the file is present and readable, it starts into “proxy” mode.

So would it be reasonable for snapd to use that file to decide which slot to make auto-connectable?

And as a related question: if a future cups-daemon Ubuntu package was released that transitioned users to the CUPS snap (similar to what we have done for Chromium and Firefox), would you expect it to delete that file? (after copying it to the location the cups snap uses, of course).

I am thinking about changing the check in the CUPS Snap to /usr/sbin/cupsd, as cupsd.conf is a configuration file which does not go away by a simple uninstall of CUPS (dpkg -r or apt remove). Also a user perhaps wants to keep the file for migrating back. cupsd definitely goes away when uninstalling CUPS and can esily be put back by installing CUPS again.

So I will change the CUPS Snap appropriately …

I will check via if [ -x /usr/sbin/cupsd ]; then ... …

Remember that /usr/sbin inside the snap sandbox will not be /usr/sbin of the host system.

james@lrrr:~$ ls -l /usr/sbin/cupsd
-rwxr-xr-x 1 root root 482144 Jun 13 20:15 /usr/sbin/cupsd
james@lrrr:~$ snap run --shell cups.lpinfo
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

james@lrrr:/home/james$ ls -l /usr/sbin/cupsd
ls: cannot access '/usr/sbin/cupsd': No such file or directory

Your check for the configuration file in /etc works because the host system /etc is mounted within the snap sandbox.

Sorry, now I remember: I have used

plugs:
  etc-cups:
    interface: system-files
    read:
      - /etc/cups

in snacraft.yaml for possible migration scripts already many years ago, and when I introduced the “proxy” mode I used this peephole to the outside to discover the presence of a classic CUPS installation.

Now the question is to stay with it or to create another peephole to /usr/sbin/cupsd. Probably stayi g with it is the easiest …

Perhaps it is simply enough to extend this to

plugs:
  etc-cups:
    interface: system-files
    read:
      - /etc/cups
      - /usr/sbin/cupsd

to have access to non-config file for the check whether there is a classically installed CUPS or not. WDYT?

@jamesh, I have tried your Pull Request #12965 on snapd now, via the snap-files.zip artifact from this test run. I installed the contained Snap via

$ sudo snap install --dangerous snapd_2.60.1+gitb055bee.b055bee_amd64.snap

and right after that I did:

$ snap list snapd
Name   Version                    Rev  Tracking       Publisher  Notes
snapd  2.60.1+gitb055bee.b055bee  x1   latest/stable  -          snapd
$ snap list cups
Name  Version  Rev  Tracking       Publisher  Notes
cups  2.4.6-1  x23  latest/stable  -          -
$ ls -l /etc/cups/cupsd.conf
-rw-r--r-- 1 root lp 4568 Mar 30 22:27 /etc/cups/cupsd.conf
$ sudo snap disconnect cups-admin-test:cups-control
Disconnect cups-admin-test:cups-control from snapd:cups-control
$ snap connections cups-admin-test
Interface      Plug                           Slot            Notes
avahi-observe  cups-admin-test:avahi-observe  :avahi-observe  manual
cups-control   cups-admin-test:cups-control   -               -
home           cups-admin-test:home           :home           -
network        cups-admin-test:network        :network        -
$ sudo snap connect cups-admin-test:cups-control
$ snap connections cups-admin-test
Interface      Plug                           Slot            Notes
avahi-observe  cups-admin-test:avahi-observe  :avahi-observe  manual
cups-control   cups-admin-test:cups-control   :cups-control   manual
home           cups-admin-test:home           :home           -
network        cups-admin-test:network        :network        -
$

This is expected. CUPS’ config file is there, so the app’s cups-control gets plugged to the system. now we remove the read bit from the cupsd.conf file:

$ sudo chmod -r /etc/cups/cupsd.conf
$ ls -l /etc/cups/cupsd.conf
--w------- 1 root lp 4568 Mar 30 22:27 /etc/cups/cupsd.conf
$ sudo snap disconnect cups-admin-test:cups-control
Disconnect cups-admin-test:cups-control from snapd:cups-control
$ snap connections cups-admin-test
Interface      Plug                           Slot            Notes
avahi-observe  cups-admin-test:avahi-observe  :avahi-observe  manual
cups-control   cups-admin-test:cups-control   -               -
home           cups-admin-test:home           :home           -
network        cups-admin-test:network        :network        -
$ sudo snap connect cups-admin-test:cups-control
$ snap connections cups-admin-test
Interface      Plug                           Slot            Notes
avahi-observe  cups-admin-test:avahi-observe  :avahi-observe  manual
cups-control   cups-admin-test:cups-control   :cups-control   manual
home           cups-admin-test:home           :home           -
network        cups-admin-test:network        :network        -
$

and we get still a connection to the system’s cups-control. Seems that the change on snapd is checking the mere presence of the file, not its readability. So let us try to move it out of the way:

$ sudo mv /etc/cups/cupsd.conf /etc/cups/cupsd.conf.orig
$ ls -l /etc/cups/cupsd.conf
ls: cannot access '/etc/cups/cupsd.conf': No such file or directory
$ sudo snap disconnect cups-admin-test:cups-control
Disconnect cups-admin-test:cups-control from snapd:cups-control
$ snap connections cups-admin-test
Interface      Plug                           Slot            Notes
avahi-observe  cups-admin-test:avahi-observe  :avahi-observe  manual
cups-control   cups-admin-test:cups-control   -               -
home           cups-admin-test:home           :home           -
network        cups-admin-test:network        :network        -
$ sudo snap connect cups-admin-test:cups-control
$ snap connections cups-admin-test
Interface      Plug                           Slot            Notes
avahi-observe  cups-admin-test:avahi-observe  :avahi-observe  manual
cups-control   cups-admin-test:cups-control   :cups-control   manual
home           cups-admin-test:home           :home           -
network        cups-admin-test:network        :network        -
$

Still connection to the system’s cups-control.

@jamesh, what is going wrong here?

The changes in that snapd build will only affect auto-connect behaviour. You’ll still be able to manually connect to either slot. So if you see “manual” anywhere in the snap connections output, you’re not testing auto-connect behaviour. Auto-connect happens during install or refresh of a snap: provided you haven’t overridden it via a manual connection/disconnection.

Also note that running snap connect cups-admin-test:cups-control is equivalent to snap connect cups-admin-test:cups-control system:cups-control – it won’t ever try to connect to an non-implicit slot.

A better test plan would be something like this:

  1. Install the test build of snapd.
  2. Set up printing in the config you want to test (e.g. cupsd as deb, cupsd as snap, or both).
  3. Install a snap with a cups-control plug
  4. Check whether the plug has been connected to the correct slot.

If the plug remains disconnected or is connected to the wrong slot, that would be a failure and mean we need to adjust the snapd patch.

One other wrinkle: the cups-control interface disallows auto-connection by default, so you’ll need to test with a published snap whose snap-declaration allows auto-connection. On my system, I can see that the Firefox snap fits this bill. You probably know better than me about others.