diff --git a/modules/cups/examples/simple.mcl b/modules/cups/examples/simple.mcl
new file mode 100644
index 000000000..20256fef5
--- /dev/null
+++ b/modules/cups/examples/simple.mcl
@@ -0,0 +1,51 @@
+import "git://github.com/purpleidea/mgmt/modules/cups/"
+
+$default_printer = {
+ "ws1" => "Foo-Laboratory-Brother",
+ "lappy2" => "Bar-Office-Canon",
+}
+$subset_printers = {
+ "Foo-Laboratory-Brother" => ["ws1",],
+}
+include cups.base() as printers
+
+# helper function
+# make sure you add your .ppd files
+class printer($name, $st) {
+ $default = $default_printer[$hostname] || ""
+ $subset = $subset_printers[$name] || []
+
+ $location = $st->location || ""
+ $makemodel = $st->makemodel || ""
+ $uri str = $st->uri
+
+ $comment = $st->comment || ""
+
+ # XXX: if we had a method that took a struct, and added a field and returned it, that would be helpful!
+ # XXX: this would need to have language sugar so that we guarantee the field name string is static.
+ # XXX: eg: $new_st = $old_st.foo => "bar"
+ # XXX: eg: $new_st = { $old_st with foo => "bar" }
+ if $subset == [] or $hostname in $subset {
+ include printers.printer($name, struct{
+ default => $name == $default,
+ info => $name, # since the name is descriptive
+ location => $location,
+ makemodel => $makemodel,
+ uri => $uri,
+ ppd => deploy.readfile("/files/ppd/${name}.ppd"),
+ comment => $comment,
+ })
+ }
+}
+
+include printer("Foo-Laboratory-Brother", struct{
+ location => "Foo's Office",
+ makemodel => "Brother Printer, driverless, 2.1b1",
+ uri => "lpd://192.168.201.108:515/PASSTHRU", # TODO: change me?
+})
+
+include printer("Bar-Office-Canon", struct{
+ location => "Bar's Office",
+ makemodel => "Canon iR-ADV C351 PPD",
+ uri => "lpd://192.168.201.120",
+})
diff --git a/modules/cups/files/printer.conf.tmpl b/modules/cups/files/printer.conf.tmpl
new file mode 100644
index 000000000..74f665672
--- /dev/null
+++ b/modules/cups/files/printer.conf.tmpl
@@ -0,0 +1,35 @@
+{{/*
+TODO: A lot of this could be templated, if we knew what it did.
+*/ -}}
+{{ if .default -}}
+
+{{ else -}}
+
+{{ end -}}
+PrinterId {{ .id }}
+UUID urn:uuid:{{ .uuid }}
+{{ if .info -}}
+Info {{ .info }}
+{{ end -}}
+{{ if .location -}}
+Location {{ .location }}
+{{ end -}}
+MakeModel {{ .makemodel }}
+DeviceURI {{ .uri }}
+State Idle
+StateTime 1735329279
+ConfigTime 1734305561
+Type 36884
+Accepting Yes
+Shared Yes
+JobSheets none none
+QuotaPeriod 0
+PageLimit 0
+KLimit 0
+OpPolicy default
+ErrorPolicy abort-job
+{{ if .default -}}
+
+{{ else -}}
+
+{{ end -}}
diff --git a/modules/cups/files/printers.conf.header b/modules/cups/files/printers.conf.header
new file mode 100644
index 000000000..3462efcbd
--- /dev/null
+++ b/modules/cups/files/printers.conf.header
@@ -0,0 +1,4 @@
+# Printer configuration file for CUPS
+# Written by mgmt config
+# DO NOT EDIT THIS FILE WHEN CUPSD IS RUNNING
+NextPrinterId 1024
diff --git a/modules/cups/main.mcl b/modules/cups/main.mcl
new file mode 100644
index 000000000..8e44428ca
--- /dev/null
+++ b/modules/cups/main.mcl
@@ -0,0 +1,167 @@
+# Mgmt
+# Copyright (C) James Shubin and the project contributors
+# Written by James Shubin and the project contributors
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+#
+# Additional permission under GNU GPL version 3 section 7
+#
+# If you modify this program, or any covered work, by linking or combining it
+# with embedded mcl code and modules (and that the embedded mcl code and
+# modules which link with this program, contain a copy of their source code in
+# the authoritative form) containing parts covered by the terms of any other
+# license, the licensors of this program grant you additional permission to
+# convey the resulting work. Furthermore, the licensors of this program grant
+# the original author, James Shubin, additional permission to update this
+# additional permission if he deems it necessary to achieve the goals of this
+# additional permission.
+
+import "deploy"
+import "golang"
+import "golang/strconv" as golang_strconv
+import "local"
+import "os"
+import "strings"
+
+class base() {
+ $vardir = local.vardir("cups/")
+
+ file "/etc/cups/" {
+ state => $const.res.file.state.exists,
+ recurse => false, # not completely managed!
+ purge => false, # not completely managed!
+ owner => "root",
+ group => "lp", # TODO: debian?
+ mode => "u=rwx,go=rx", # dir
+ }
+
+ file "/etc/cups/printers.conf" {
+ state => $const.res.file.state.exists,
+ fragments => [
+ "${vardir}printers.conf.header", # also pull this one file
+ "${vardir}printers.d/", # pull from this dir
+ ],
+ owner => "root",
+ group => "lp",
+ mode => "u=rw,go=",
+
+ Before => Exec["restorecon"],
+ Notify => Svc["cups"],
+ }
+
+ exec "restorecon" {
+ cmd => "/usr/sbin/restorecon -rv /etc/cups/",
+ # XXX: make some magic snippets which turn into ./mgmt snippet /etc/cups/ (for example) and get substituted in here!
+ # XXX: or even better, instead of snippets which exec mgmt stuff, they turn into pure golang equivalents...
+ ifcmd => "mgmt:changed /etc/cups/",
+ watchcmd => "mgmt:dir /etc/cups/",
+ }
+
+ svc "cups" {
+ # TODO: manage this
+ }
+}
+
+class base:printer_base() {
+ file "${vardir}printers.d/" {
+ state => $const.res.file.state.exists,
+ recurse => true,
+ purge => true,
+ owner => "root",
+ group => "root",
+ mode => "u=rwx,go=", # dir
+ }
+
+ file "${vardir}printers.conf.header" {
+ state => $const.res.file.state.exists,
+ content => deploy.readfile("/files/printers.conf.header"), # static, no template!
+ owner => "root",
+ group => "root",
+ mode => "u=rw,go=",
+ }
+}
+
+class base:printer($name, $st) {
+ $default = $st->default || false
+ $info = $st->info || ""
+ $location = $st->location || ""
+ $makemodel = $st->makemodel || ""
+ $uri str = $st->uri
+ $ppd str = $st->ppd
+
+ $comment = $st->comment || ""
+
+ include printer_base
+
+ $index = local.pool("cups-printer", $name) # the uid will always return the same int
+ # XXX: implement local.pool_max("cups-printer") to use for NextPrinterId
+
+ panic($index < 0 or $index > 65535) # 0xffffh is the maximum
+ $hex = strings.left_pad(golang_strconv.format_int($index, 16), "0", 4)
+ $tail = strings.substr($hex, 0, 4) # TODO: support $hex[0] or $hex[0:4] ?
+ $uuid = "01234567-89ab-cdef-0000-00000000${tail}"
+
+ $tmpl = struct{
+ name => $name,
+ id => $index, # it just has to be constant and unique
+ uuid => $uuid,
+ default => $default,
+ info => $info,
+ location => $location,
+ makemodel => $makemodel,
+ uri => $uri,
+ #ppd => $ppd,
+ comment => $comment,
+ }
+
+ $content = golang.template(deploy.readfile("/files/printer.conf.tmpl"), $tmpl)
+ #$f = os.readfile("${vardir}printers.d/${name}") # XXX: replace with something.stateok() ???
+ #if $f != $content {
+ # # XXX: stop the svc... then change the file, then start it up again...
+ # # XXX: might need a flag so that we stop the svc for multiples of these all in the same place...
+ #}
+
+ file "${vardir}printers.d/${name}" {
+ state => $const.res.file.state.exists,
+ content => $content,
+ owner => "root",
+ group => "root",
+ mode => "u=rw,go=",
+ }
+
+ file "/etc/cups/ppd/${name}.ppd" {
+ state => $const.res.file.state.exists,
+ # TODO: build a file "handle" system for bigger data
+ content => $ppd,
+ #source => $ppd,
+ owner => "root",
+ group => "lp",
+ mode => "u=rw,g=r,o=",
+
+ Before => Exec["restorecon"],
+ Notify => Svc["cups"],
+ }
+
+ if $default {
+ # there can only be one!
+ # if more than one of these is set on the same machine we'd error
+ file "${vardir}default" {
+ state => $const.res.file.state.exists,
+ content => "${name}",
+ owner => "root",
+ group => "root",
+ mode => "u=rw,go=",
+ }
+ }
+}
diff --git a/modules/cups/metadata.yaml b/modules/cups/metadata.yaml
new file mode 100644
index 000000000..e69de29bb