-
-
Notifications
You must be signed in to change notification settings - Fork 15.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
nixos/printers: declarative configuration #55510
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ]; | ||
florianjacob marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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); | ||
}; | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also here:
|
||
# 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); | ||
''; | ||
}) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we drop the "ensure" part? It seems redundant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@edolstra You see a conceptual difference between
ensurePrinters
andensureDefaultPrinter
here, and therefore it's redundant fordefaultPrinter
but not forensurePrinters
, is that correct? I'm at your service. Maybe you could provide some general guidance on this, though?I started to use those
ensure
prefixes to mark options that can only be enforced on a best effort basis on each service reload. In this concrete case, bothensurePrinters
andensureDefaultPrinter
are translated intolpadmin
command. I can't stop the user from modifying printers / change default printer, but the state will be restored. In case ofensurePrinters
, I deliberately do not try to delete non-listed printers that the user might have added manually. Similar options in NixOS exist though which don't have that prefix.There's a discussion going on about that, sparked off by similar mysql / postgresql options
I explained my point there as well.
EDIT: corrected link to discussion