diff --git a/nixos/doc/manual/release-notes/rl-1909.xml b/nixos/doc/manual/release-notes/rl-1909.xml
index f831cfcdc57..2b5ae929ece 100644
--- a/nixos/doc/manual/release-notes/rl-1909.xml
+++ b/nixos/doc/manual/release-notes/rl-1909.xml
@@ -77,7 +77,17 @@
./programs/dwm-status.nix
+
+
+ The new hardware.printers module allows to declaratively configure CUPS printers
+ via the ensurePrinters and
+ ensureDefaultPrinter options.
+ ensurePrinters will never delete existing printers,
+ but will make sure that the given printers are configured as declared.
+
+
+
lpstat -s
+ and remove printers with lpadmin -x <printer-name>.
+ Printers not listed here can still be manually configured.
+ '';
+ default = [];
+ type = types.listOf (types.submodule {
+ options = {
+ name = mkOption {
+ type = printerName;
+ example = "BrotherHL_Workroom";
+ description = ''
+ Name of the printer / printer queue.
+ May contain any printable characters except "/", "#", and space.
+ '';
+ };
+ location = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "Workroom";
+ description = ''
+ Optional human-readable location.
+ '';
+ };
+ description = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ example = "Brother HL-5140";
+ description = ''
+ Optional human-readable description.
+ '';
+ };
+ deviceUri = mkOption {
+ type = types.str;
+ example = [
+ "ipp://printserver.local/printers/BrotherHL_Workroom"
+ "usb://HP/DESKJET%20940C?serial=CN16E6C364BH"
+ ];
+ description = ''
+ How to reach the printer.
+ lpinfo -v shows a list of supported device URIs and schemes.
+ '';
+ };
+ model = mkOption {
+ type = types.str;
+ example = literalExample ''
+ gutenprint.''${lib.version.majorMinor (lib.getVersion pkgs.cups)}://brother-hl-5140/expert
+ '';
+ description = ''
+ Location of the ppd driver file for the printer.
+ lpinfo -m shows a list of supported models.
+ '';
+ };
+ ppdOptions = mkOption {
+ type = types.attrsOf types.str;
+ example = {
+ "PageSize" = "A4";
+ "Duplex" = "DuplexNoTumble";
+ };
+ default = {};
+ description = ''
+ Sets PPD options for the printer.
+ lpoptions [-p printername] -l shows suported PPD options for the given printer.
+ '';
+ };
+ };
+ });
+ };
+ };
+ };
+
+ config = mkIf (cfg.ensurePrinters != [] && config.services.printing.enable) {
+ systemd.services."ensure-printers" = let
+ cupsUnit = if config.services.printing.startWhenNeeded then "cups.socket" else "cups.service";
+ in {
+ description = "Ensure NixOS-configured CUPS printers";
+ wantedBy = [ "multi-user.target" ];
+ requires = [ cupsUnit ];
+ # in contrast to cups.socket, for cups.service, this is actually not enough,
+ # as the cups service reports its activation before clients can actually interact with it.
+ # Because of this, commands like `lpinfo -v` will report a bad file descriptor
+ # due to the missing UNIX socket without sufficient sleep time.
+ after = [ cupsUnit ];
+
+ serviceConfig = {
+ Type = "oneshot";
+ };
+
+ # sleep 10 is required to wait until cups.service is actually initialized and has created its UNIX socket file
+ script = (optionalString (!config.services.printing.startWhenNeeded) "sleep 10\n")
+ + (concatMapStringsSep "\n" ensurePrinter cfg.ensurePrinters)
+ + optionalString (cfg.ensureDefaultPrinter != null) (ensureDefaultPrinter cfg.ensureDefaultPrinter);
+ };
+ };
+}
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index c84ef3d6d9b..12fb67d279d 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -59,6 +59,7 @@
./hardware/nitrokey.nix
./hardware/opengl.nix
./hardware/pcmcia.nix
+ ./hardware/printers.nix
./hardware/raid/hpsa.nix
./hardware/steam-hardware.nix
./hardware/usb-wwan.nix
diff --git a/nixos/tests/printing.nix b/nixos/tests/printing.nix
index 74583ae5562..4d0df289cf7 100644
--- a/nixos/tests/printing.nix
+++ b/nixos/tests/printing.nix
@@ -1,99 +1,114 @@
# Test printing via CUPS.
-import ./make-test.nix ({pkgs, ... }: {
+import ./make-test.nix ({pkgs, ... }:
+let
+ printingServer = startWhenNeeded: {
+ services.printing.enable = true;
+ services.printing.startWhenNeeded = startWhenNeeded;
+ services.printing.listenAddresses = [ "*:631" ];
+ services.printing.defaultShared = true;
+ services.printing.extraConf =
+ ''
+
+ Order allow,deny
+ Allow from all
+
+ '';
+ networking.firewall.allowedTCPPorts = [ 631 ];
+ # Add a HP Deskjet printer connected via USB to the server.
+ hardware.printers.ensurePrinters = [{
+ name = "DeskjetLocal";
+ deviceUri = "usb://foobar/printers/foobar";
+ model = "drv:///sample.drv/deskjet.ppd";
+ }];
+ };
+ printingClient = startWhenNeeded: {
+ services.printing.enable = true;
+ services.printing.startWhenNeeded = startWhenNeeded;
+ # Add printer to the client as well, via IPP.
+ hardware.printers.ensurePrinters = [{
+ name = "DeskjetRemote";
+ deviceUri = "ipp://${if startWhenNeeded then "socketActivatedServer" else "serviceServer"}/printers/DeskjetLocal";
+ model = "drv:///sample.drv/deskjet.ppd";
+ }];
+ hardware.printers.ensureDefaultPrinter = "DeskjetRemote";
+ };
+
+in
+
+{
name = "printing";
meta = with pkgs.stdenv.lib.maintainers; {
maintainers = [ domenkozar eelco matthewbauer ];
};
nodes = {
+ socketActivatedServer = { ... }: (printingServer true);
+ serviceServer = { ... }: (printingServer false);
- server =
- { ... }:
- { services.printing.enable = true;
- services.printing.listenAddresses = [ "*:631" ];
- services.printing.defaultShared = true;
- services.printing.extraConf =
- ''
-
- Order allow,deny
- Allow from all
-
- '';
- networking.firewall.allowedTCPPorts = [ 631 ];
- };
-
- client =
- { ... }:
- { services.printing.enable = true;
- };
-
+ socketActivatedClient = { ... }: (printingClient true);
+ serviceClient = { ... }: (printingClient false);
};
testScript =
''
startAll;
- $client->succeed("lpstat -r") =~ /scheduler is running/ or die;
- # check local encrypted connections work without error
- $client->succeed("lpstat -E -r") =~ /scheduler is running/ or die;
- # Test that UNIX socket is used for connections.
- $client->succeed("lpstat -H") =~ "/run/cups/cups.sock" or die;
- # Test that HTTP server is available too.
- $client->succeed("curl --fail http://localhost:631/");
- $client->succeed("curl --fail http://server:631/");
- $server->fail("curl --fail --connect-timeout 2 http://client:631/");
-
- # Add a HP Deskjet printer connected via USB to the server.
- $server->succeed("lpadmin -p DeskjetLocal -E -v usb://foobar/printers/foobar");
-
- # Add it to the client as well via IPP.
- $client->succeed("lpadmin -p DeskjetRemote -E -v ipp://server/printers/DeskjetLocal");
- $client->succeed("lpadmin -d DeskjetRemote");
-
- # Do some status checks.
- $client->succeed("lpstat -a") =~ /DeskjetRemote accepting requests/ or die;
- $client->succeed("lpstat -h server:631 -a") =~ /DeskjetLocal accepting requests/ or die;
- $client->succeed("cupsdisable DeskjetRemote");
- $client->succeed("lpq") =~ /DeskjetRemote is not ready.*no entries/s or die;
- $client->succeed("cupsenable DeskjetRemote");
- $client->succeed("lpq") =~ /DeskjetRemote is ready.*no entries/s or die;
-
- # Test printing various file types.
- foreach my $file ("${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
- "${pkgs.groff.doc}/share/doc/*/meref.ps",
- "${pkgs.cups.out}/share/doc/cups/images/cups.png",
- "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt")
- {
- $file =~ /([^\/]*)$/; my $fn = $1;
-
- subtest "print $fn", sub {
-
- # Print the file on the client.
- $client->succeed("lp $file");
- $client->sleep(10);
- $client->succeed("lpq") =~ /active.*root.*$fn/ or die;
-
- # Ensure that a raw PCL file appeared in the server's queue
- # (showing that the right filters have been applied). Of
- # course, since there is no actual USB printer attached, the
- # file will stay in the queue forever.
- $server->waitForFile("/var/spool/cups/d*-001");
- $server->sleep(10);
- $server->succeed("lpq -a") =~ /$fn/ or die;
-
- # Delete the job on the client. It should disappear on the
- # server as well.
- $client->succeed("lprm");
- $client->sleep(10);
- $client->succeed("lpq -a") =~ /no entries/;
- Machine::retry sub {
- return 1 if $server->succeed("lpq -a") =~ /no entries/;
+ # Make sure that cups is up on both sides.
+ $serviceServer->waitForUnit("cups.service");
+ $serviceClient->waitForUnit("cups.service");
+ # wait until cups is fully initialized and ensure-printers has executed with 10s delay
+ $serviceClient->sleep(20);
+ $socketActivatedClient->waitUntilSucceeds("systemctl status ensure-printers | grep -q -E 'code=exited, status=0/SUCCESS'");
+ sub testPrinting {
+ my ($client, $server) = (@_);
+ my $clientHostname = $client->name();
+ my $serverHostname = $server->name();
+ $client->succeed("lpstat -r") =~ /scheduler is running/ or die;
+ # Test that UNIX socket is used for connections.
+ $client->succeed("lpstat -H") =~ "/var/run/cups/cups.sock" or die;
+ # Test that HTTP server is available too.
+ $client->succeed("curl --fail http://localhost:631/");
+ $client->succeed("curl --fail http://$serverHostname:631/");
+ $server->fail("curl --fail --connect-timeout 2 http://$clientHostname:631/");
+ # Do some status checks.
+ $client->succeed("lpstat -a") =~ /DeskjetRemote accepting requests/ or die;
+ $client->succeed("lpstat -h $serverHostname:631 -a") =~ /DeskjetLocal accepting requests/ or die;
+ $client->succeed("cupsdisable DeskjetRemote");
+ $client->succeed("lpq") =~ /DeskjetRemote is not ready.*no entries/s or die;
+ $client->succeed("cupsenable DeskjetRemote");
+ $client->succeed("lpq") =~ /DeskjetRemote is ready.*no entries/s or die;
+ # Test printing various file types.
+ foreach my $file ("${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
+ "${pkgs.groff.doc}/share/doc/*/meref.ps",
+ "${pkgs.cups.out}/share/doc/cups/images/cups.png",
+ "${pkgs.pcre.doc}/share/doc/pcre/pcre.txt")
+ {
+ $file =~ /([^\/]*)$/; my $fn = $1;
+ subtest "print $fn", sub {
+ # Print the file on the client.
+ $client->succeed("lp $file");
+ $client->waitUntilSucceeds("lpq | grep -q -E 'active.*root.*$fn'");
+ # Ensure that a raw PCL file appeared in the server's queue
+ # (showing that the right filters have been applied). Of
+ # course, since there is no actual USB printer attached, the
+ # file will stay in the queue forever.
+ $server->waitForFile("/var/spool/cups/d*-001");
+ $server->waitUntilSucceeds("lpq -a | grep -q -E '$fn'");
+ # Delete the job on the client. It should disappear on the
+ # server as well.
+ $client->succeed("lprm");
+ $client->waitUntilSucceeds("lpq -a | grep -q -E 'no entries'");
+ Machine::retry sub {
+ return 1 if $server->succeed("lpq -a") =~ /no entries/;
+ };
+ # The queue is empty already, so this should be safe.
+ # Otherwise, pairs of "c*"-"d*-001" files might persist.
+ $server->execute("rm /var/spool/cups/*");
};
- # The queue is empty already, so this should be safe.
- # Otherwise, pairs of "c*"-"d*-001" files might persist.
- $server->execute("rm /var/spool/cups/*");
- };
+ }
}
- '';
+ testPrinting($serviceClient, $serviceServer);
+ testPrinting($socketActivatedClient, $socketActivatedServer);
+ '';
})