nixos/printers: declarative configuration
This commit is contained in:
parent
d4283f47dc
commit
18a5d23b55
|
@ -77,7 +77,17 @@
|
||||||
<literal>./programs/dwm-status.nix</literal>
|
<literal>./programs/dwm-status.nix</literal>
|
||||||
</para>
|
</para>
|
||||||
</listitem>
|
</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>
|
</itemizedlist>
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section xmlns="http://docbook.org/ns/docbook"
|
<section xmlns="http://docbook.org/ns/docbook"
|
||||||
|
|
|
@ -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/nitrokey.nix
|
||||||
./hardware/opengl.nix
|
./hardware/opengl.nix
|
||||||
./hardware/pcmcia.nix
|
./hardware/pcmcia.nix
|
||||||
|
./hardware/printers.nix
|
||||||
./hardware/raid/hpsa.nix
|
./hardware/raid/hpsa.nix
|
||||||
./hardware/steam-hardware.nix
|
./hardware/steam-hardware.nix
|
||||||
./hardware/usb-wwan.nix
|
./hardware/usb-wwan.nix
|
||||||
|
|
|
@ -1,99 +1,114 @@
|
||||||
# Test printing via CUPS.
|
# 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";
|
name = "printing";
|
||||||
meta = with pkgs.stdenv.lib.maintainers; {
|
meta = with pkgs.stdenv.lib.maintainers; {
|
||||||
maintainers = [ domenkozar eelco matthewbauer ];
|
maintainers = [ domenkozar eelco matthewbauer ];
|
||||||
};
|
};
|
||||||
|
|
||||||
nodes = {
|
nodes = {
|
||||||
|
socketActivatedServer = { ... }: (printingServer true);
|
||||||
|
serviceServer = { ... }: (printingServer false);
|
||||||
|
|
||||||
server =
|
socketActivatedClient = { ... }: (printingClient true);
|
||||||
{ ... }:
|
serviceClient = { ... }: (printingClient false);
|
||||||
{ 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;
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
testScript =
|
testScript =
|
||||||
''
|
''
|
||||||
startAll;
|
startAll;
|
||||||
|
|
||||||
$client->succeed("lpstat -r") =~ /scheduler is running/ or die;
|
# Make sure that cups is up on both sides.
|
||||||
# check local encrypted connections work without error
|
$serviceServer->waitForUnit("cups.service");
|
||||||
$client->succeed("lpstat -E -r") =~ /scheduler is running/ or die;
|
$serviceClient->waitForUnit("cups.service");
|
||||||
# Test that UNIX socket is used for connections.
|
# wait until cups is fully initialized and ensure-printers has executed with 10s delay
|
||||||
$client->succeed("lpstat -H") =~ "/run/cups/cups.sock" or die;
|
$serviceClient->sleep(20);
|
||||||
# Test that HTTP server is available too.
|
$socketActivatedClient->waitUntilSucceeds("systemctl status ensure-printers | grep -q -E 'code=exited, status=0/SUCCESS'");
|
||||||
$client->succeed("curl --fail http://localhost:631/");
|
sub testPrinting {
|
||||||
$client->succeed("curl --fail http://server:631/");
|
my ($client, $server) = (@_);
|
||||||
$server->fail("curl --fail --connect-timeout 2 http://client:631/");
|
my $clientHostname = $client->name();
|
||||||
|
my $serverHostname = $server->name();
|
||||||
# Add a HP Deskjet printer connected via USB to the server.
|
$client->succeed("lpstat -r") =~ /scheduler is running/ or die;
|
||||||
$server->succeed("lpadmin -p DeskjetLocal -E -v usb://foobar/printers/foobar");
|
# Test that UNIX socket is used for connections.
|
||||||
|
$client->succeed("lpstat -H") =~ "/var/run/cups/cups.sock" or die;
|
||||||
# Add it to the client as well via IPP.
|
# Test that HTTP server is available too.
|
||||||
$client->succeed("lpadmin -p DeskjetRemote -E -v ipp://server/printers/DeskjetLocal");
|
$client->succeed("curl --fail http://localhost:631/");
|
||||||
$client->succeed("lpadmin -d DeskjetRemote");
|
$client->succeed("curl --fail http://$serverHostname:631/");
|
||||||
|
$server->fail("curl --fail --connect-timeout 2 http://$clientHostname:631/");
|
||||||
# Do some status checks.
|
# Do some status checks.
|
||||||
$client->succeed("lpstat -a") =~ /DeskjetRemote accepting requests/ or die;
|
$client->succeed("lpstat -a") =~ /DeskjetRemote accepting requests/ or die;
|
||||||
$client->succeed("lpstat -h server:631 -a") =~ /DeskjetLocal accepting requests/ or die;
|
$client->succeed("lpstat -h $serverHostname:631 -a") =~ /DeskjetLocal accepting requests/ or die;
|
||||||
$client->succeed("cupsdisable DeskjetRemote");
|
$client->succeed("cupsdisable DeskjetRemote");
|
||||||
$client->succeed("lpq") =~ /DeskjetRemote is not ready.*no entries/s or die;
|
$client->succeed("lpq") =~ /DeskjetRemote is not ready.*no entries/s or die;
|
||||||
$client->succeed("cupsenable DeskjetRemote");
|
$client->succeed("cupsenable DeskjetRemote");
|
||||||
$client->succeed("lpq") =~ /DeskjetRemote is ready.*no entries/s or die;
|
$client->succeed("lpq") =~ /DeskjetRemote is ready.*no entries/s or die;
|
||||||
|
# Test printing various file types.
|
||||||
# Test printing various file types.
|
foreach my $file ("${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
|
||||||
foreach my $file ("${pkgs.groff.doc}/share/doc/*/examples/mom/penguin.pdf",
|
"${pkgs.groff.doc}/share/doc/*/meref.ps",
|
||||||
"${pkgs.groff.doc}/share/doc/*/meref.ps",
|
"${pkgs.cups.out}/share/doc/cups/images/cups.png",
|
||||||
"${pkgs.cups.out}/share/doc/cups/images/cups.png",
|
"${pkgs.pcre.doc}/share/doc/pcre/pcre.txt")
|
||||||
"${pkgs.pcre.doc}/share/doc/pcre/pcre.txt")
|
{
|
||||||
{
|
$file =~ /([^\/]*)$/; my $fn = $1;
|
||||||
$file =~ /([^\/]*)$/; my $fn = $1;
|
subtest "print $fn", sub {
|
||||||
|
# Print the file on the client.
|
||||||
subtest "print $fn", sub {
|
$client->succeed("lp $file");
|
||||||
|
$client->waitUntilSucceeds("lpq | grep -q -E 'active.*root.*$fn'");
|
||||||
# Print the file on the client.
|
# Ensure that a raw PCL file appeared in the server's queue
|
||||||
$client->succeed("lp $file");
|
# (showing that the right filters have been applied). Of
|
||||||
$client->sleep(10);
|
# course, since there is no actual USB printer attached, the
|
||||||
$client->succeed("lpq") =~ /active.*root.*$fn/ or die;
|
# file will stay in the queue forever.
|
||||||
|
$server->waitForFile("/var/spool/cups/d*-001");
|
||||||
# Ensure that a raw PCL file appeared in the server's queue
|
$server->waitUntilSucceeds("lpq -a | grep -q -E '$fn'");
|
||||||
# (showing that the right filters have been applied). Of
|
# Delete the job on the client. It should disappear on the
|
||||||
# course, since there is no actual USB printer attached, the
|
# server as well.
|
||||||
# file will stay in the queue forever.
|
$client->succeed("lprm");
|
||||||
$server->waitForFile("/var/spool/cups/d*-001");
|
$client->waitUntilSucceeds("lpq -a | grep -q -E 'no entries'");
|
||||||
$server->sleep(10);
|
Machine::retry sub {
|
||||||
$server->succeed("lpq -a") =~ /$fn/ or die;
|
return 1 if $server->succeed("lpq -a") =~ /no entries/;
|
||||||
|
};
|
||||||
# Delete the job on the client. It should disappear on the
|
# The queue is empty already, so this should be safe.
|
||||||
# server as well.
|
# Otherwise, pairs of "c*"-"d*-001" files might persist.
|
||||||
$client->succeed("lprm");
|
$server->execute("rm /var/spool/cups/*");
|
||||||
$client->sleep(10);
|
|
||||||
$client->succeed("lpq -a") =~ /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/*");
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
'';
|
testPrinting($serviceClient, $serviceServer);
|
||||||
|
testPrinting($socketActivatedClient, $socketActivatedServer);
|
||||||
|
'';
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue