[bug#75528,v3] services: Add power.

Message ID 376f456f8d49b825ddb1969734cc2aab4c99d780.1740345021.git.~@wolfsden.cz
State New
Headers
Series [bug#75528,v3] services: Add power. |

Commit Message

Tomas Volf Feb. 23, 2025, 9:10 p.m. UTC
  * gnu/services/power.scm: New file.
* gnu/local.mk (GNU_SYSTEM_MODULES): Add it.
* doc/guix.texi (Power Management Services): Document service and data types.

Change-Id: If205d19bea1d20a99309626e28521a2d6fe6702f
---
 doc/guix.texi          | 382 ++++++++++++++++++++++-
 gnu/local.mk           |   1 +
 gnu/services/power.scm | 672 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 1052 insertions(+), 3 deletions(-)
 create mode 100644 gnu/services/power.scm
  

Comments

Maxim Cournoyer Feb. 28, 2025, 2:11 a.m. UTC | #1
Hi Tomas,

I was about to merge this, but I have one last (I promise!) question, see
below.

Tomas Volf <~@wolfsden.cz> writes:

[...]

> +(define apcupsd-service-type
> +  (service-type
> +   (name 'apcupsd)
> +   (description "Configure and optionally start the apcupsd.")
> +   (extensions (list (service-extension activation-service-type
> +                                        apcupsd-activation)
> +                     (service-extension shepherd-root-service-type
> +                                        apcupsd-shepherd-services)
> +                     (service-extension pam-root-service-type
> +                                        apcupsd-pam-extensions)))
> +   (compose identity)
> +   (extend (lambda (cfg lst)
> +             (fold (cut <> <>) cfg lst)))

What does the above extend does?  I find the (cut <> <>) particularly
cryptic, as it's typically used to specialize procedures, which there
are none above.
  
Tomas Volf March 1, 2025, 11:46 p.m. UTC | #2
Maxim Cournoyer <maxim.cournoyer@gmail.com> writes:

> Hi Tomas,
>
> I was about to merge this, but I have one last (I promise!) question, see
> below.

No problem, I appreciate the review, it lead to higher code quality of
this series.

>
> Tomas Volf <~@wolfsden.cz> writes:
>
> [...]
>
>> +(define apcupsd-service-type
>> +  (service-type
>> +   (name 'apcupsd)
>> +   (description "Configure and optionally start the apcupsd.")
>> +   (extensions (list (service-extension activation-service-type
>> +                                        apcupsd-activation)
>> +                     (service-extension shepherd-root-service-type
>> +                                        apcupsd-shepherd-services)
>> +                     (service-extension pam-root-service-type
>> +                                        apcupsd-pam-extensions)))
>> +   (compose identity)
>> +   (extend (lambda (cfg lst)
>> +             (fold (cut <> <>) cfg lst)))
>
> What does the above extend does?  I find the (cut <> <>) particularly
> cryptic, as it's typically used to specialize procedures, which there
> are none above.

I consider the behavior of the default #f value for extend field to be
unfortunate, so I prefer to specify extend for all my services.  Yet in
this case there is no obvious thing to extend, so I opted for the
extension to be a procedure, taking in the configuration and returning,
possibly modified, configuration.

The (cut <> <>) is just a handy, more concise way to write
(lambda (proc arg) (proc arg)).

What are your thoughts here?  Should I just drop both compose and extend
from here?  Or is it fine like this?

Tomas
  
Maxim Cournoyer March 2, 2025, 5:26 a.m. UTC | #3
Hi Tomas,

Tomas Volf <~@wolfsden.cz> writes:

> Maxim Cournoyer <maxim.cournoyer@gmail.com> writes:
>
>> Hi Tomas,
>>
>> I was about to merge this, but I have one last (I promise!) question, see
>> below.
>
> No problem, I appreciate the review, it lead to higher code quality of
> this series.

:-)

>>
>> Tomas Volf <~@wolfsden.cz> writes:
>>
>> [...]
>>
>>> +(define apcupsd-service-type
>>> +  (service-type
>>> +   (name 'apcupsd)
>>> +   (description "Configure and optionally start the apcupsd.")
>>> +   (extensions (list (service-extension activation-service-type
>>> +                                        apcupsd-activation)
>>> +                     (service-extension shepherd-root-service-type
>>> +                                        apcupsd-shepherd-services)
>>> +                     (service-extension pam-root-service-type
>>> +                                        apcupsd-pam-extensions)))
>>> +   (compose identity)
>>> +   (extend (lambda (cfg lst)
>>> +             (fold (cut <> <>) cfg lst)))
>>
>> What does the above extend does?  I find the (cut <> <>) particularly
>> cryptic, as it's typically used to specialize procedures, which there
>> are none above.
>
> I consider the behavior of the default #f value for extend field to be
> unfortunate, so I prefer to specify extend for all my services.  Yet in
> this case there is no obvious thing to extend, so I opted for the
> extension to be a procedure, taking in the configuration and returning,
> possibly modified, configuration.

I see.  That's wasn't obvious to me.  If there's nothing to be
composed/extended, I think leaving the compose and extend fields to
their default values communicate this better.

> The (cut <> <>) is just a handy, more concise way to write
> (lambda (proc arg) (proc arg)).

I see.  I think my confusion also stemmed from the fact I can never
fully remember how extend/compose work until I look it up again :-).

> What are your thoughts here?  Should I just drop both compose and extend
> from here?  Or is it fine like this?

I'd just drop them.  If we discover a need later, we can add them back
in, ideally with users (other services) as self-documenting code.
  
Tomas Volf March 2, 2025, 1:47 p.m. UTC | #4
Maxim Cournoyer <maxim.cournoyer@gmail.com> writes:

> [..]
>
>> What are your thoughts here?  Should I just drop both compose and extend
>> from here?  Or is it fine like this?
>
> I'd just drop them.  If we discover a need later, we can add them back
> in, ideally with users (other services) as self-documenting code.

Makes sense, I have sent v4.

Have a nice day,
Tomas
  
Maxim Cournoyer March 2, 2025, 3:46 p.m. UTC | #5
Hi,

Tomas Volf <~@wolfsden.cz> writes:

[...]

> What are your thoughts here?  Should I just drop both compose and extend
> from here?  Or is it fine like this?

This was the last small change, so I've adjusted on my side and pushed
as commit d0e46a0003.

Thank you for your patience!
  
Tomas Volf March 2, 2025, 4:54 p.m. UTC | #6
Maxim Cournoyer <maxim.cournoyer@gmail.com> writes:

> Hi,
>
> Tomas Volf <~@wolfsden.cz> writes:
>
> [...]
>
>> What are your thoughts here?  Should I just drop both compose and extend
>> from here?  Or is it fine like this?
>
> This was the last small change, so I've adjusted on my side and pushed
> as commit d0e46a0003.
>
> Thank you for your patience!

And thank you for working with me through this. :)
  

Patch

diff --git a/doc/guix.texi b/doc/guix.texi
index 83ba0f3292..a9eb6e4da3 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -123,7 +123,7 @@ 
 Copyright @copyright{} 2023 Thomas Ieong@*
 Copyright @copyright{} 2023 Saku Laesvuori@*
 Copyright @copyright{} 2023 Graham James Addis@*
-Copyright @copyright{} 2023, 2024 Tomas Volf@*
+Copyright @copyright{} 2023-2025 Tomas Volf@*
 Copyright @copyright{} 2024, 2025 Herman Rimm@*
 Copyright @copyright{} 2024 Matthew Trzcinski@*
 Copyright @copyright{} 2024 Richard Sent@*
@@ -422,7 +422,7 @@  Top
 * Network File System::         NFS related services.
 * Samba Services::              Samba services.
 * Continuous Integration::      Cuirass and Laminar services.
-* Power Management Services::   Extending battery life.
+* Power Management Services::   Extending battery life, etc.
 * Audio Services::              The MPD.
 * Virtualization Services::     Virtualization services.
 * Version Control Services::    Providing remote access to Git repositories.
@@ -19270,7 +19270,7 @@  Services
 * Network File System::         NFS related services.
 * Samba Services::              Samba services.
 * Continuous Integration::      Cuirass and Laminar services.
-* Power Management Services::   Extending battery life.
+* Power Management Services::   Extending battery life, etc.
 * Audio Services::              The MPD.
 * Virtualization Services::     Virtualization services.
 * Version Control Services::    Providing remote access to Git repositories.
@@ -36849,6 +36849,382 @@  Power Management Services
 @end table
 @end deftp
 
+The @code{(gnu services power)} module provides a service definition for
+@uref{http://www.apcupsd.org/, apcupsd}, a utility to interact with
+@acronym{APC, APC by Schneider Electric or formerly American Power
+Conversion Corporation} @acronym{UPS, Uninterruptible Power Supply}
+devices.  Apcupsd also works with some @acronym{OEM, Original Equipment
+Manufacturer}-branded products manufactured by APC.
+
+@defvar apcupsd-service-type
+The service type for apcupsd.  For USB UPSes no configuration is
+necessary, however tweaking some fields to better suit your needs might
+be desirable.  The defaults are taken from the upstream configuration
+and they are not very conservative (for example, the default
+@code{battery-level} of 5% may be considered too low by some).
+
+The default event handlers do send emails, read more in
+@ref{apcupsd-event-handlers}.
+
+@lisp
+(service apcupsd-service-type)
+@end lisp
+@end defvar
+
+@deftp {Data Type} apcupsd-configuration
+
+Available @code{apcupsd-configuration} fields are:
+
+@table @asis
+@item @code{apcupsd} (default: @code{apcupsd}) (type: package)
+The @code{apcupsd} package to use.
+
+@item @code{shepherd-service-name} (default: @code{apcupsd}) (type: symbol)
+The name of the shepherd service.  You can add the service multiple
+times with different names to manage multiple UPSes.
+
+@item @code{auto-start?} (default: @code{#t}) (type: boolean)
+Should the shepherd service auto-start?
+
+@item @code{pid-file} (default: @code{"/var/run/apcupsd.pid"}) (type: string)
+The file name of the pid file.
+
+@item @code{debug-level} (default: @code{0}) (type: integer)
+The logging verbosity.  Bigger number means more logs.  The source code
+uses up to @code{300} as debug level value, so a value of @code{999}
+seems reasonable to enable all the logs.
+
+@item @code{run-dir} (default: @code{"/var/run/apcupsd"}) (type: string)
+The directory containing runtime information.  You need to change this
+if you desire to run multiple instances of the daemon.
+
+@item @code{name} (type: maybe-string)
+Use this to give your UPS a name in log files and such.  This is
+particularly useful if you have multiple UPSes.  This does not set the
+EEPROM.  It should be 8 characters or less.
+
+@item @code{cable} (default: @code{usb}) (type: enum-cable)
+The type of a cable connecting the UPS to your computer.  Possible
+generic choices are @code{'simple}, @code{'smart}, @code{'ether} and
+@code{'usb}.
+
+Alternatively, a specific cable model number may be used:
+@code{'940-0119A}, @code{'940-0127A}, @code{'940-0128A},
+@code{'940-0020B}, @code{'940-0020C}, @code{'940-0023A},
+@code{'940-0024B}, @code{'940-0024C}, @code{'940-1524C},
+@code{'940-0024G}, @code{'940-0095A}, @code{'940-0095B},
+@code{'940-0095C}, @code{'940-0625A}, @code{'M-04-02-2000}.
+
+@item @code{type} (default: @code{usb}) (type: enum-type)
+The type of the UPS you have.
+
+@table @code
+@item apcsmart
+Newer serial character device, appropriate for SmartUPS models using a
+serial cable (not an USB).
+
+@item usb
+Most new UPSes are an USB.
+
+@item net
+Network link to a master apcupsd through apcupsd's Network Information
+Server.  This is used if the UPS powering your computer is connected to
+a different computer for monitoring.
+
+@item snmp
+SNMP network link to an SNMP-enabled UPS device.
+
+@item netsnmp
+Same as the SNMP above but requires use of the net-snmp library.  Unless
+you have a specific need for this old driver, you should use the
+@code{'snmp} instead.
+
+@item dumb
+An old serial character device for use with simple-signaling UPSes.
+
+@item pcnet
+A PowerChute Network Shutdown protocol which can be used as an
+alternative to an SNMP with the AP9617 family of smart slot cards.
+
+@item modbus
+A serial device for use with newest SmartUPS models supporting the
+MODBUS protocol.
+
+@end table
+
+@item @code{device} (default: @code{""}) (type: string)
+For USB UPSes, usually you want to set this to an empty string (the
+default).  For other UPS types, you must specify an appropriate port or
+address.
+
+@table @code
+@item apcsmart
+Set to the appropriate @file{/dev/tty**} device.
+
+@item usb
+A null string setting enables auto-detection, which is the best choice
+for most installations.
+
+@item net
+Set to @code{@var{hostname}:@var{port}}.
+
+@item snmp
+Set to @code{@var{hostname}:@var{port}:@var{vendor}:@var{community}}.
+The @var{hostname} is the ip address or hostname of the UPS on the
+network.  The @var{vendor} can be can be "APC" or "APC_NOTRAP".
+"APC_NOTRAP" will disable SNMP trap catching; you usually want "APC".
+The @var{port} is usually 161.  The @var{community} is usually
+"private".
+
+@item netsnmp
+Same as the @code{'snmp}.
+
+@item dumb
+Set to the appropriate @file{/dev/tty**} device.
+
+@item pcnet
+Set to @code{@var{ipaddr}:@var{username}:@var{passphrase}:@var{port}}.
+The @var{ipaddr} is the IP address of the UPS management card.  The
+@var{username} and the @var{passphrase} are the credentials for which
+the card has been configured.  The @var{port} is the port number on
+which to listen for messages from the UPS, normally 3052.  If this
+parameter is empty or missing, the default of 3052 will be used.
+
+@item modbus
+Set to the appropriate @file{/dev/tty**} device.  You can also leave it
+empty for MODBUS over USB or set to the serial number of the UPS.
+
+@end table
+
+@item @code{poll-time} (default: @code{60}) (type: integer)
+The interval (in seconds) at which apcupsd polls the UPS for status.
+This setting applies both to directly-attached UPSes (apcsmart, usb,
+dumb) and networked UPSes (net, snmp).  Lowering this setting will
+improve the apcupsd's responsiveness to certain events at the cost of
+higher CPU utilization.
+
+@item @code{on-batery-delay} (default: @code{6}) (type: integer)
+The time in seconds from when a power failure is detected until we react
+to it with an onbattery event.  The @code{'powerout} event will be
+triggered immediately when a power failure is detected.  However, the
+@code{'onbattery} event will be trigger only after this delay.
+
+@item @code{battery-level} (default: @code{5}) (type: integer)
+If during a power failure, the remaining battery percentage (as reported
+by the UPS) is below or equal to this value, the apcupsd will initiate a
+system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation
+
+@item @code{remaining-minutes} (default: @code{3}) (type: integer)
+If during a power failure, the remaining runtime in minutes (as
+calculated internally by the UPS) is below or equal to this value,
+apcupsd will initiate a system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation
+
+@item @code{timeout} (default: @code{0}) (type: integer)
+If during a power failure, the UPS has run on batteries for this many
+seconds or longer, apcupsd will initiate a system shutdown.  The value
+of 0 disables this timer.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation
+
+@item @code{annoy-interval} (default: @code{300}) (type: integer)
+The time in seconds between annoying users (via the @code{'annoyme}
+event) to sign off prior to system shutdown.  0 disables.
+
+@item @code{annoy-delay} (default: @code{60}) (type: integer)
+The initial delay in seconds after a power failure before warning users
+to get off the system.
+
+@item @code{no-logon} (default: @code{disable}) (type: enum-no-logon)
+The condition which determines when users are prevented from logging in
+during a power failure.
+
+@item @code{kill-delay} (default: @code{0}) (type: integer)
+If this is non-zero, the apcupsd will continue running after a shutdown
+has been requested, and after the specified time in seconds attempt to
+kill the power.  This is for use on systems where apcupsd cannot regain
+control after a shutdown.
+
+@item @code{net-server} (default: @code{#f}) (type: boolean)
+If enabled, a network information server process will be started.
+
+@item @code{net-server-ip} (default: @code{"127.0.0.1"}) (type: string)
+An IP address on which the NIS server will listen for incoming
+connections.
+
+@item @code{net-server-port} (default: @code{3551}) (type: integer)
+An IP port on which the NIS server will listen for incoming connections.
+
+@item @code{net-server-events-file} (type: maybe-string)
+If you want the last few EVENTS to be available over the network by the
+network information server, you must set this to a file name.
+
+@item @code{net-server-events-file-max-size} (default: @code{10}) (type: integer)
+The maximum size of the events file in kilobytes.
+
+@item @code{class} (default: @code{standalone}) (type: enum-class)
+Normally standalone unless you share an UPS using an APC ShareUPS card.
+
+@item @code{mode} (default: @code{disable}) (type: enum-mode)
+Normally disable unless you share an UPS using an APC ShareUPS card.
+
+@item @code{stat-time} (default: @code{0}) (type: integer)
+The time interval in seconds between writing the status file, 0
+disables.
+
+@item @code{log-stats} (default: @code{#f}) (type: boolean)
+Also write the stats as a logs.  This generates a lot of output.
+
+@item @code{data-time} (default: @code{0}) (type: integer)
+The time interval in seconds between writing the data records to the log
+file, 0 disables.
+
+@item @code{facility} (type: maybe-string)
+The logging facility for the syslog.
+
+@item @code{event-handlers} (type: apcupsd-event-handlers)
+Handlers for events produced by apcupsd.
+
+@end table
+@end deftp
+
+@anchor{apcupsd-event-handlers}
+@deftp {Data Type} apcupsd-event-handlers
+
+For a description of the events please refer to @samp{man 8 apccontrol}.
+
+Each handler shall be a gexp.  It is spliced into the control script for
+the daemon.  In addition to the standard Guile programming environment,
+the following procedures and variables are also available:
+
+@table @code
+@item conf
+Variable containing the file name of the configuration file.
+
+@item powerfail-file
+Variable containing the file name of the powerfail file.
+
+@item cmd
+The event currently being handled.
+
+@item name
+The name of the UPS as specified in the configuration file.
+
+@item connected?
+Is @code{#t} if @command{apcupsd} is connected to the UPS via a serial
+port (or a USB port).  In most configurations, this will be the case.
+In the case of a Slave machine where apcupsd is not directly connected
+to the UPS, this value will be @code{#f}.
+
+@item powered?
+Is @code{#t} if the computer on which @command{apcupsd} is running is
+powered by the UPS and @code{#f} if not.  At the moment, this value is
+unimplemented and always @code{#f}.
+
+@item (err @var{fmt} @var{args...})
+Wrapper around @code{format} outputting to @code{(current-error-port)}.
+
+@item (wall @var{fmt} @var{args...})
+Wrapper around @code{format} outputting via @command{wall}.
+
+@item (apcupsd @var{args...})
+Call @command{apcupsd} while passing the correct configuration file and
+all the arguments.
+
+@item (mail-to-root @var{subject} @var{body})
+Send an email to the local administrator.  This procedure assumes the
+@command{sendmail} is located at @command{/run/privileged/bin/sendmail}
+(as would be the case with @code{opensmtpd-service-type}).
+
+@end table
+
+Available @code{apcupsd-event-handlers} fields are:
+
+@table @asis
+@item @code{modules} (type: gexp)
+Additional modules to import into the generated handler script.
+
+@item @code{killpower} (type: gexp)
+The handler for the killpower event.
+
+@item @code{commfailure} (type: gexp)
+The handler for the commfailure event.
+
+@item @code{commok} (type: gexp)
+The handler for the commfailure event.
+
+@item @code{powerout} (type: gexp)
+The handler for the powerout event.
+
+@item @code{onbattery} (type: gexp)
+The handler for the onbattery event.
+
+@item @code{offbattery} (type: gexp)
+The handler for the offbattery event.
+
+@item @code{mainsback} (type: gexp)
+The handler for the mainsback event.
+
+@item @code{failing} (type: gexp)
+The handler for the failing event.
+
+@item @code{timeout} (type: gexp)
+The handler for the timeout event.
+
+@item @code{loadlimit} (type: gexp)
+The handler for the loadlimit event.
+
+@item @code{runlimit} (type: gexp)
+The handler for the runlimit event.
+
+@item @code{doreboot} (type: gexp)
+The handler for the doreboot event.
+
+@item @code{doshutdown} (type: gexp)
+The handler for the doshutdown event.
+
+@item @code{annoyme} (type: gexp)
+The handler for the annoyme event.
+
+@item @code{emergency} (type: gexp)
+The handler for the emergency event.
+
+@item @code{changeme} (type: gexp)
+The handler for the changeme event.
+
+@item @code{remotedown} (type: gexp)
+The handler for the remotedown event.
+
+@item @code{startselftest} (type: gexp)
+The handler for the startselftest event.
+
+@item @code{endselftest} (type: gexp)
+The handler for the endselftest event.
+
+@item @code{battdetach} (type: gexp)
+The handler for the battdetach event.
+
+@item @code{battattach} (type: gexp)
+The handler for the battattach event.
+
+@end table
+@end deftp
+
 @node Audio Services
 @subsection Audio Services
 
diff --git a/gnu/local.mk b/gnu/local.mk
index 257b66d1c2..bf82ec1c43 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -764,6 +764,7 @@  GNU_SYSTEM_MODULES =				\
   %D%/services/nix.scm				\
   %D%/services/nfs.scm			\
   %D%/services/pam-mount.scm			\
+  %D%/services/power.scm			\
   %D%/services/science.scm			\
   %D%/services/security.scm			\
   %D%/services/security-token.scm		\
diff --git a/gnu/services/power.scm b/gnu/services/power.scm
new file mode 100644
index 0000000000..7e30955f83
--- /dev/null
+++ b/gnu/services/power.scm
@@ -0,0 +1,672 @@ 
+;;; Copyright © 2025 Tomas Volf <~@wolfsden.cz>
+
+;;;; Commentary:
+
+;;; Power-related services.
+
+;;;; Code:
+
+(define-module (gnu services power)
+  #:use-module (gnu)
+  #:use-module (gnu packages admin)
+  #:use-module (gnu packages linux)
+  #:use-module (gnu packages power)
+  #:use-module (gnu services configuration)
+  #:use-module (gnu services shepherd)
+  #:use-module (gnu services)
+  #:use-module (guix packages)
+  #:use-module (guix records)
+  #:use-module (ice-9 match)
+  #:use-module (srfi srfi-1)
+  #:use-module (srfi srfi-26)
+  #:export (apcupsd-service-type
+
+            apcupsd-configuration
+            apcupsd-configuration-apcupsd
+            apcupsd-configuration-shepherd-service-name
+            apcupsd-configuration-auto-start?
+            apcupsd-configuration-pid-file
+            apcupsd-configuration-debug-level
+            apcupsd-configuration-run-dir
+            apcupsd-configuration-name
+            apcupsd-configuration-cable
+            apcupsd-configuration-type
+            apcupsd-configuration-device
+            apcupsd-configuration-poll-time
+            apcupsd-configuration-on-batery-delay
+            apcupsd-configuration-battery-level
+            apcupsd-configuration-remaining-minutes
+            apcupsd-configuration-timeout
+            apcupsd-configuration-annoy-interval
+            apcupsd-configuration-annoy-delay
+            apcupsd-configuration-no-logon
+            apcupsd-configuration-kill-delay
+            apcupsd-configuration-net-server
+            apcupsd-configuration-net-server-ip
+            apcupsd-configuration-net-server-port
+            apcupsd-configuration-net-server-events-file
+            apcupsd-configuration-net-server-events-file-max-size
+            apcupsd-configuration-class
+            apcupsd-configuration-mode
+            apcupsd-configuration-stat-time
+            apcupsd-configuration-log-stats
+            apcupsd-configuration-data-time
+            apcupsd-configuration-facility
+            apcupsd-configuration-event-handlers
+
+            apcupsd-event-handlers
+            apcupsd-event-handlers-modules
+            apcupsd-event-handlers-annoyme
+            apcupsd-event-handlers-battattach
+            apcupsd-event-handlers-battdetach
+            apcupsd-event-handlers-changeme
+            apcupsd-event-handlers-commfailure
+            apcupsd-event-handlers-commok
+            apcupsd-event-handlers-doreboot
+            apcupsd-event-handlers-doshutdown
+            apcupsd-event-handlers-emergency
+            apcupsd-event-handlers-endselftest
+            apcupsd-event-handlers-failing
+            apcupsd-event-handlers-killpower
+            apcupsd-event-handlers-loadlimit
+            apcupsd-event-handlers-mainsback
+            apcupsd-event-handlers-offbattery
+            apcupsd-event-handlers-onbattery
+            apcupsd-event-handlers-powerout
+            apcupsd-event-handlers-remotedown
+            apcupsd-event-handlers-runlimit
+            apcupsd-event-handlers-startselftest
+            apcupsd-event-handlers-timeout))
+
+(define-configuration/no-serialization apcupsd-event-handlers
+  (modules
+   (gexp #~())
+   "Additional modules to import into the generated handler script.")
+  (killpower
+   (gexp
+    #~((wall "Apccontrol doing: apcupsd --killpower on UPS ~a" name)
+       (sleep 10)
+       (apcupsd "--killpower")
+       (wall "Apccontrol has done: apcupsd --killpower on UPS ~a" name)))
+   "The handler for the killpower event.")
+  (commfailure
+   (gexp
+    #~((let ((msg (format #f "~a Communications with UPS ~a lost."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Warning: communications lost with UPS ~a" name)))
+   "The handler for the commfailure event.")
+  (commok
+   (gexp
+    #~((let ((msg (format #f "~a Communications with UPS ~a restored."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Communications restored with UPS ~a" name)))
+   "The handler for the commfailure event.")
+  (powerout
+   (gexp
+    #~(#t))
+   "The handler for the powerout event.")
+  (onbattery
+   (gexp
+    #~((let ((msg (format #f "~a UPS ~a Power Failure !!!"
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Power failure on UPS ~a.  Running on batteries." name)))
+   "The handler for the onbattery event.")
+  (offbattery
+   (gexp
+    #~((let ((msg (format #f "~a UPS ~a Power has returned."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Power has returned on UPS ~a..." name)))
+   "The handler for the offbattery event.")
+  (mainsback
+   (gexp
+    #~((when (file-exists? powerfail-file)
+         (wall "Continuing with shutdown."))))
+   "The handler for the mainsback event.")
+  (failing
+   (gexp
+    #~((wall "Battery power exhausted on UPS ~a.  Doing shutdown." name)))
+   "The handler for the failing event.")
+  (timeout
+   (gexp
+    #~((wall "Battery time limit exceeded on UPS ~a.  Doing shutdown." name)))
+   "The handler for the timeout event.")
+  (loadlimit
+   (gexp
+    #~((wall "Remaining battery charge below limit on UPS ~a.  Doing shutdown." name)))
+   "The handler for the loadlimit event.")
+  (runlimit
+   (gexp
+    #~((wall "Remaining battery runtime below limit on UPS ~a.  Doing shutdown." name)))
+   "The handler for the runlimit event.")
+  (doreboot
+   (gexp
+    #~((wall "UPS ~a initiating Reboot Sequence" name)
+       (system* #$(file-append shepherd "/sbin/reboot"))))
+   "The handler for the doreboot event.")
+  (doshutdown
+   (gexp
+    #~((wall "UPS ~a initiated Shutdown Sequence" name)
+       (system* #$(file-append shepherd "/sbin/halt"))))
+   "The handler for the doshutdown event.")
+  (annoyme
+   (gexp
+    #~((wall "Power problems with UPS ~a.  Please logoff." name)))
+   "The handler for the annoyme event.")
+  (emergency
+   (gexp
+    #~((wall "Emergency Shutdown.  Possible battery failure on UPS ~a." name)))
+   "The handler for the emergency event.")
+  (changeme
+   (gexp
+    #~((let ((msg (format #f "~a UPS ~a battery needs changing NOW."
+                          (gethostname) name)))
+         (mail-to-root msg msg))
+       (wall "Emergency!  Batteries have failed on UPS ~a.  Change them NOW." name)))
+   "The handler for the changeme event.")
+  (remotedown
+   (gexp
+    #~((wall "Remote Shutdown.  Beginning Shutdown Sequence.")))
+   "The handler for the remotedown event.")
+  (startselftest
+   (gexp
+    #~(#t))
+   "The handler for the startselftest event.")
+  (endselftest
+   (gexp
+    #~(#t))
+   "The handler for the endselftest event.")
+  (battdetach
+   (gexp
+    #~(#t))
+   "The handler for the battdetach event.")
+  (battattach
+   (gexp
+    #~(#t))
+   "The handler for the battattach event."))
+
+(define-syntax define-enum
+  (lambda (x)
+    (syntax-case x ()
+      ((_ name values)
+       (let* ((datum/name (syntax->datum #'name))
+              (datum/predicate (string->symbol
+                                (format #f "enum-~a?" datum/name)))
+              (datum/serialize (string->symbol
+                                (format #f "serialize-enum-~a" datum/name))))
+         (with-syntax
+             ((predicate (datum->syntax x datum/predicate))
+              (serialize (datum->syntax x datum/serialize)))
+           #'(begin
+               (define (predicate value)
+                 (memq value values))
+               (define serialize serialize-symbol))))))))
+
+(define mangle-field-name
+  (match-lambda
+    ('name                            "UPSNAME")
+    ('cable                           "UPSCABLE")
+    ('type                            "UPSTYPE")
+    ('device                          "DEVICE")
+    ('poll-time                       "POLLTIME")
+    ('lock-dir                        "LOCKFILE")
+    ('power-fail-dir                  "PWRFAILDIR")
+    ('no-login-dir                    "NOLOGINDIR")
+    ('on-batery-delay                 "ONBATTERYDELAY")
+    ('battery-level                   "BATTERYLEVEL")
+    ('remaining-minutes               "MINUTES")
+    ('timeout                         "TIMEOUT")
+    ('annoy-interval                  "ANNOY")
+    ('annoy-delay                     "ANNOYDELAY")
+    ('no-logon                        "NOLOGON")
+    ('kill-delay                      "KILLDELAY")
+    ('net-server                      "NETSERVER")
+    ('net-server-ip                   "NISIP")
+    ('net-server-port                 "NISPORT")
+    ('net-server-events-file          "EVENTSFILE")
+    ('net-server-events-file-max-size "EVENTSFILEMAX")
+    ('class                           "UPSCLASS")
+    ('mode                            "UPSMODE")
+    ('stat-time                       "STATTIME")
+    ('stat-file                       "STATFILE")
+    ('log-stats                       "LOGSTATS")
+    ('data-time                       "DATATIME")
+    ('facility                        "FACILITY")))
+
+(define (serialize-string field-name value)
+  #~(format #f "~a ~a\n" #$(mangle-field-name field-name) '#$value))
+(define serialize-symbol serialize-string)
+(define serialize-integer serialize-string)
+(define (serialize-boolean field-name value)
+  #~(format #f "~a ~a\n"
+            #$(mangle-field-name field-name)
+            #$(if value "on" "off")))
+
+(define-maybe string)
+
+(define-enum cable '( simple smart ether usb
+                      940-0119A 940-0127A 940-0128A 940-0020B 940-0020C
+                      940-0023A 940-0024B 940-0024C 940-1524C 940-0024G
+                      940-0095A 940-0095B 940-0095C 940-0625A MAM-04-02-2000))
+(define-enum type '(apcsmart usb net snmp netsnmp dumb pcnet modbus test))
+(define-enum no-logon '(disable timeout percent minutes always))
+(define-enum class '(standalone shareslave sharemaster))
+(define-enum mode '(disable share))
+
+(define-configuration apcupsd-configuration
+  (apcupsd (package apcupsd) "The @code{apcupsd} package to use.")
+
+  (shepherd-service-name
+   (symbol 'apcupsd)
+   "The name of the shepherd service.  You can add the service multiple times
+with different names to manage multiple UPSes."
+   empty-serializer)
+  (auto-start?
+   (boolean #t)
+   "Should the shepherd service auto-start?"
+   empty-serializer)
+  (pid-file
+   (string "/run/apcupsd.pid")
+   "The file name of the PID file."
+   empty-serializer)
+  (debug-level
+   (integer 0)
+   "The logging verbosity.  Bigger number means more logs.  The source code
+uses up to @code{300} as debug level value, so a value of @code{999} seems
+reasonable to enable all the logs."
+   empty-serializer)
+
+  (run-dir
+   (string "/run/apcupsd")
+   "The directory containing runtime information.  You need to change this if
+you desire to run multiple instances of the daemon."
+   empty-serializer)
+
+  ;; General configuration parameters
+  (name
+   maybe-string
+   "Use this to give your UPS a name in log files and such.  This is
+particularly useful if you have multiple UPSes.  This does not set the EEPROM.
+It should be 8 characters or less.")
+  (cable
+   (enum-cable 'usb)
+   "The type of a cable connecting the UPS to your computer.  Possible generic
+choices are @code{'simple}, @code{'smart}, @code{'ether} and
+@code{'usb}.
+
+Alternatively, a specific cable model number may be used: @code{'940-0119A},
+@code{'940-0127A}, @code{'940-0128A}, @code{'940-0020B}, @code{'940-0020C},
+@code{'940-0023A}, @code{'940-0024B}, @code{'940-0024C}, @code{'940-1524C},
+@code{'940-0024G}, @code{'940-0095A}, @code{'940-0095B}, @code{'940-0095C},
+@code{'940-0625A}, @code{'M-04-02-2000}.")
+  (type
+   (enum-type 'usb)
+   "The type of the UPS you have.
+
+@table @code
+@item apcsmart
+Newer serial character device, appropriate for SmartUPS models using a serial
+cable (not an USB).
+
+@item usb
+Most new UPSes are an USB.
+
+@item net
+Network link to a master apcupsd through apcupsd's Network Information Server.
+This is used if the UPS powering your computer is connected to a different
+computer for monitoring.
+
+@item snmp
+SNMP network link to an SNMP-enabled UPS device.
+
+@item netsnmp
+Same as the SNMP above but requires use of the net-snmp library.  Unless you
+have a specific need for this old driver, you should use the @code{'snmp}
+instead.
+
+@item dumb
+An old serial character device for use with simple-signaling UPSes.
+
+@item pcnet
+A PowerChute Network Shutdown protocol which can be used as an alternative to
+an SNMP with the AP9617 family of smart slot cards.
+
+@item modbus
+A serial device for use with newest SmartUPS models supporting the MODBUS
+protocol.
+
+@end table")
+  (device
+   (string "")
+   "For USB UPSes, usually you want to set this to an empty string (the
+default).  For other UPS types, you must specify an appropriate port or
+address.
+
+@table @code
+@item apcsmart
+Set to the appropriate @file{/dev/tty**} device.
+
+@item usb
+A null string setting enables auto-detection, which is the best choice for
+most installations.
+
+@item net
+Set to @code{@var{hostname}:@var{port}}.
+
+@item snmp
+Set to @code{@var{hostname}:@var{port}:@var{vendor}:@var{community}}.  The
+@var{hostname} is the ip address or hostname of the UPS on the network.  The
+@var{vendor} can be can be \"APC\" or \"APC_NOTRAP\".  \"APC_NOTRAP\" will
+disable SNMP trap catching; you usually want \"APC\".  The @var{port} is
+usually 161.  The @var{community} is usually \"private\".
+
+@item netsnmp
+Same as the @code{'snmp}.
+
+@item dumb
+Set to the appropriate @file{/dev/tty**} device.
+
+@item pcnet
+Set to @code{@var{ipaddr}:@var{username}:@var{passphrase}:@var{port}}.  The
+@var{ipaddr} is the IP address of the UPS management card.  The @var{username}
+and the @var{passphrase} are the credentials for which the card has been
+configured.  The @var{port} is the port number on which to listen for messages
+from the UPS, normally 3052.  If this parameter is empty or missing, the
+default of 3052 will be used.
+
+@item modbus
+Set to the appropriate @file{/dev/tty**} device.  You can also leave it empty
+for MODBUS over USB or set to the serial number of the UPS.
+
+@end table")
+  (poll-time
+   (integer 60)
+   "The interval (in seconds) at which apcupsd polls the UPS for status.  This
+setting applies both to directly-attached UPSes (apcsmart, usb, dumb) and
+networked UPSes (net, snmp).  Lowering this setting will improve the apcupsd's
+responsiveness to certain events at the cost of higher CPU utilization.")
+
+  ;; Configuration parameters used during power failures
+  (on-batery-delay
+   (integer 6)
+   "The time in seconds from when a power failure is detected until we react
+to it with an onbattery event.  The @code{'powerout} event will be triggered
+immediately when a power failure is detected.  However, the @code{'onbattery}
+event will be trigger only after this delay.")
+  (battery-level
+   (integer 5)
+   "If during a power failure, the remaining battery percentage (as reported
+by the UPS) is below or equal to this value, the apcupsd will initiate a
+system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation")
+  (remaining-minutes
+   (integer 3)
+   "If during a power failure, the remaining runtime in minutes (as calculated
+internally by the UPS) is below or equal to this value, apcupsd will initiate
+a system shutdown.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation")
+  (timeout
+   (integer 0)
+   "If during a power failure, the UPS has run on batteries for this many
+seconds or longer, apcupsd will initiate a system shutdown.  The value of 0
+disables this timer.
+
+@quotation Note
+@code{battery-level}, @code{remaining-minutes}, and @code{timeout} work
+in a conjunction, so the first that occurs will cause the initation of a
+shutdown.
+@end quotation")
+  (annoy-interval
+   (integer 300)
+   "The time in seconds between annoying users (via the @code{'annoyme} event)
+to sign off prior to system shutdown.  0 disables.")
+  (annoy-delay
+   (integer 60)
+   "The initial delay in seconds after a power failure before warning users to
+get off the system.")
+  (no-logon
+   (enum-no-logon 'disable)
+   "The condition which determines when users are prevented from logging in
+during a power failure.")
+  (kill-delay
+   (integer 0)
+   "If this is non-zero, the apcupsd will continue running after a shutdown
+has been requested, and after the specified time in seconds attempt to kill
+the power.  This is for use on systems where apcupsd cannot regain control
+after a shutdown.")
+
+  ;; Configuration statements for Network Information Server
+  (net-server
+   (boolean #f)
+   "If enabled, a network information server process will be started.")
+  (net-server-ip
+   (string "127.0.0.1")
+   "An IP address on which the NIS server will listen for incoming
+connections.")
+  (net-server-port
+   (integer 3551)
+   "An IP port on which the NIS server will listen for incoming connections.")
+  (net-server-events-file
+   maybe-string
+   "If you want the last few EVENTS to be available over the network by the
+network information server, you must set this to a file name.")
+  (net-server-events-file-max-size
+   (integer 10)
+   "The maximum size of the events file in kilobytes.")
+  ;; Configuration statements used if sharing a UPS with more than one machine
+  (class (enum-class 'standalone)
+    "Normally standalone unless you share an UPS using an APC ShareUPS card.")
+  (mode (enum-mode 'disable)
+    "Normally disable unless you share an UPS using an APC ShareUPS card.")
+  ;; Configuration statements to control apcupsd system logging
+  (stat-time
+   (integer 0)
+   "The time interval in seconds between writing the status file, 0
+disables.")
+  (log-stats
+   (boolean #f)
+   "Also write the stats as a logs.  This generates a lot of output.")
+  (data-time
+   (integer 0)
+   "The time interval in seconds between writing the data records to the log
+file, 0 disables.")
+  (facility
+   maybe-string
+   "The logging facility for the syslog.")
+
+  ;; Event handlers
+  (event-handlers
+   (apcupsd-event-handlers (apcupsd-event-handlers))
+   "Handlers for events produced by apcupsd."
+   empty-serializer))
+
+(define (%apccontrol config)
+  (program-file
+   "apccontrol"
+   (match-record (apcupsd-configuration-event-handlers config)
+       <apcupsd-event-handlers>
+       ( killpower commfailure commok powerout onbattery offbattery mainsback
+         failing timeout loadlimit runlimit doreboot doshutdown annoyme
+         emergency changeme remotedown startselftest endselftest battdetach
+         battattach )
+     #~(begin
+         (use-modules (ice-9 format)
+                      (ice-9 match)
+                      (ice-9 popen)
+                      (srfi srfi-9)
+                      #$@(apcupsd-event-handlers-modules
+                          (apcupsd-configuration-event-handlers config)))
+         ;; Script dir depends on these, and the configuration depends on the
+         ;; script dir.  To sever the cyclic dependency, pass the file names via
+         ;; environment variables.
+         (define conf (getenv "GUIX_APCUPSD_CONF"))
+         (define powerfail-file (getenv "GUIX_APCUPSD_POWERFAIL_FILE"))
+
+         (define (err . args)
+           (apply format (current-error-port) args))
+         (define (wall . args)
+           (system* #$(file-append util-linux "/bin/wall") (apply format #f args)))
+         (define (apcupsd . args)
+           (apply system* #$(file-append apcupsd "/sbin/apcupsd") "-f" conf args))
+         (define (mail-to-root subject body)
+           (let ((port (open-pipe* OPEN_WRITE
+                                   "/run/privileged/bin/sendmail"
+                                   "-F" "apcupsd"
+                                   "root")))
+             (format port "Subject: ~a~%~%~a~&" subject body)
+             (close-pipe port)))
+         (match (cdr (command-line))
+           (((? string? cmd) name connected powered)
+            (let ((connected? (match connected
+                                ("1" #t)
+                                ("0" #f)))
+                  (powered? (match powered
+                              ("1" #t)
+                              ("0" #f))))
+              (match cmd
+                ("killpower"     #$killpower)
+                ("commfailure"   #$commfailure)
+                ("commok"        #$commok)
+	        ("powerout"      #$powerout)
+	        ("onbattery"     #$onbattery)
+	        ("offbattery"    #$offbattery)
+	        ("mainsback"     #$mainsback)
+	        ("failing"       #$failing)
+	        ("timeout"       #$timeout)
+	        ("loadlimit"     #$loadlimit)
+	        ("runlimit"      #$runlimit)
+	        ("doreboot"      #$doreboot)
+	        ("doshutdown"    #$doshutdown)
+	        ("annoyme"       #$annoyme)
+	        ("emergency"     #$emergency)
+	        ("changeme"      #$changeme)
+	        ("remotedown"    #$remotedown)
+	        ("startselftest" #$startselftest)
+	        ("endselftest"   #$endselftest)
+	        ("battdetach"    #$battdetach)
+	        ("battattach"    #$battattach)
+                (_
+                 (err "Unknown event: ~a~%" cmd)
+                 (err "Iff the event was emitted by apcupsd, this is a bug.~%")
+                 (err "Please report to bug-guix@gnu.org.~%")
+                 (exit #f)))))
+           (args
+            (err "Unknown arguments: ~a~%" args)
+            (err "Iff the arguments were passed by apcupsd, this is a bug.~%")
+            (err "Please report to bug-guix@gnu.org.~%")
+            (exit #f)))))))
+
+(define (apcupsd-script-dir config)
+  (computed-file
+   "apcupsd-script-dir"
+   #~(begin
+       (mkdir #$output)
+       (chdir #$output)
+       (symlink #$(%apccontrol config) "apccontrol"))))
+
+(define (apcupsd-config-file config)
+  (let ((run-dir (apcupsd-configuration-run-dir config)))
+    (mixed-text-file
+     "apcupsd.conf"
+     "\
+## apcupsd.conf v1.1 ##
+#
+#  for apcupsd - GNU Guix
+#
+# \"apcupsd\" POSIX config file (generated by apcupsd-service-type)
+"
+     (serialize-configuration config apcupsd-configuration-fields)
+     ;; This one is confusing.  The manual page states:
+     ;;
+     ;; > It must be changed when running more than one copy of apcupsd on the
+     ;; > same computer to control multiple UPSes.
+     ;;
+     ;; However would you not want the lock to be per-device, not per-process?
+     ;; I decided to follow the documentation, but I do not understand why it
+     ;; should be like this.  I do not have multiple UPSes to try.
+     (serialize-string 'lock-dir (string-append run-dir "/lock"))
+     (serialize-string 'power-fail-dir run-dir)
+     (serialize-string 'no-login-dir run-dir)
+     (serialize-string 'stat-file (string-append run-dir "/apcupsd.status"))
+     "SCRIPTDIR " (apcupsd-script-dir config) "\n")))
+
+(define (apcupsd-activation config)
+  (match-record config <apcupsd-configuration> (run-dir)
+    #~(begin
+        (use-modules (guix build utils))
+        (mkdir-p #$(string-append run-dir "/lock")))))
+
+(define (apcupsd-shepherd-services config)
+  (match-record config <apcupsd-configuration>
+                ( apcupsd pid-file debug-level run-dir
+                  shepherd-service-name auto-start?)
+    (let ((config-file (apcupsd-config-file config)))
+      (list
+       (shepherd-service
+        (documentation "Run the apcupsd daemon.")
+        (requirement '(user-processes))
+        (provision (list shepherd-service-name))
+        (auto-start? auto-start?)
+        (start #~(make-forkexec-constructor
+                  '(#$(file-append apcupsd "/sbin/apcupsd")
+                    "-b"                ;do not daemonize
+                    "-f" #$config-file
+                    "-P" #$pid-file
+                    "-d" #$(number->string debug-level))
+                  #:log-file
+                  #$(format #f "/var/log/~a.log" shepherd-service-name)
+                  #:environment-variables
+                  (cons* (string-append "GUIX_APCUPSD_CONF="
+                                        #$config-file)
+                         #$(string-append "GUIX_APCUPSD_POWERFAIL_FILE="
+                                          run-dir "/powerfail")
+                         (default-environment-variables))))
+        (stop #~(make-kill-destructor))
+        (actions (list (shepherd-configuration-action config-file))))))))
+
+(define (apcupsd-pam-extensions config)
+  ;; The apcupsd can be configured to prevent users from logging in on certain
+  ;; conditions.  This is implemented by creation of a "nologin" file, and
+  ;; using a pam nologin module to prevent the login (if the file exists).
+  (define pam-nologin
+    (pam-entry
+     (control "required")
+     (module "pam_nologin.so")
+     (arguments (list (string-append "file="
+                                     (apcupsd-configuration-run-dir config)
+                                     "/nologin")))))
+
+  (list (pam-extension
+         (transformer
+          (lambda (pam)
+            (pam-service
+             (inherit pam)
+             (auth (cons pam-nologin (pam-service-auth pam)))))))))
+
+(define apcupsd-service-type
+  (service-type
+   (name 'apcupsd)
+   (description "Configure and optionally start the apcupsd.")
+   (extensions (list (service-extension activation-service-type
+                                        apcupsd-activation)
+                     (service-extension shepherd-root-service-type
+                                        apcupsd-shepherd-services)
+                     (service-extension pam-root-service-type
+                                        apcupsd-pam-extensions)))
+   (compose identity)
+   (extend (lambda (cfg lst)
+             (fold (cut <> <>) cfg lst)))
+   (default-value (apcupsd-configuration))))