Merge pull request #55510 from florianjacob/declarative-printers
nixos/printers: declarative configuration
This commit is contained in:
commit
ad13ebe029
4 changed files with 242 additions and 81 deletions
|
@ -135,7 +135,17 @@
|
|||
<literal>./programs/dwm-status.nix</literal>
|
||||
</para>
|
||||
</listitem>
|
||||
<listitem>
|
||||
<para>
|
||||
The new <varname>hardware.printers</varname> module allows to declaratively configure CUPS printers
|
||||
via the <varname>ensurePrinters</varname> and
|
||||
<varname>ensureDefaultPrinter</varname> options.
|
||||
<varname>ensurePrinters</varname> will never delete existing printers,
|
||||
but will make sure that the given printers are configured as declared.
|
||||
</para>
|
||||
</listitem>
|
||||
</itemizedlist>
|
||||
|
||||
</section>
|
||||
|
||||
<section xmlns="http://docbook.org/ns/docbook"
|
||||
|
|
135
nixos/modules/hardware/printers.nix
Normal file
135
nixos/modules/hardware/printers.nix
Normal file
|
@ -0,0 +1,135 @@
|
|||
{ config, lib, pkgs, ... }:
|
||||
with lib;
|
||||
let
|
||||
cfg = config.hardware.printers;
|
||||
ppdOptionsString = options: optionalString (options != {})
|
||||
(concatStringsSep " "
|
||||
(mapAttrsToList (name: value: "-o '${name}'='${value}'") options)
|
||||
);
|
||||
ensurePrinter = p: ''
|
||||
${pkgs.cups}/bin/lpadmin -p '${p.name}' -E \
|
||||
${optionalString (p.location != null) "-L '${p.location}'"} \
|
||||
${optionalString (p.description != null) "-D '${p.description}'"} \
|
||||
-v '${p.deviceUri}' \
|
||||
-m '${p.model}' \
|
||||
${ppdOptionsString p.ppdOptions}
|
||||
'';
|
||||
ensureDefaultPrinter = name: ''
|
||||
${pkgs.cups}/bin/lpoptions -d '${name}'
|
||||
'';
|
||||
|
||||
# "graph but not # or /" can't be implemented as regex alone due to missing lookahead support
|
||||
noInvalidChars = str: all (c: c != "#" && c != "/") (stringToCharacters str);
|
||||
printerName = (types.addCheck (types.strMatching "[[:graph:]]+") noInvalidChars)
|
||||
// { description = "printable string without spaces, # and /"; };
|
||||
|
||||
|
||||
in {
|
||||
options = {
|
||||
hardware.printers = {
|
||||
ensureDefaultPrinter = mkOption {
|
||||
type = types.nullOr printerName;
|
||||
default = null;
|
||||
description = ''
|
||||
Ensures the named printer is the default CUPS printer / printer queue.
|
||||
'';
|
||||
};
|
||||
ensurePrinters = mkOption {
|
||||
description = ''
|
||||
Will regularly ensure that the given CUPS printers are configured as declared here.
|
||||
If a printer's options are manually changed afterwards, they will be overwritten eventually.
|
||||
This option will never delete any printer, even if removed from this list.
|
||||
You can check existing printers with <command>lpstat -s</command>
|
||||
and remove printers with <command>lpadmin -x <printer-name></command>.
|
||||
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.
|
||||
<command>lpinfo -v</command> 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.
|
||||
<command>lpinfo -m</command> 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.
|
||||
<command>lpoptions [-p printername] -l</command> 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);
|
||||
};
|
||||
};
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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 =
|
||||
''
|
||||
<Location />
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Location>
|
||||
'';
|
||||
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 =
|
||||
''
|
||||
<Location />
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Location>
|
||||
'';
|
||||
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);
|
||||
'';
|
||||
})
|
||||
|
|
Loading…
Reference in a new issue