Extend NixOS: Difference between revisions
imported>Fadenb |
→Quick Implementation: Use current channel for search.nixos.org link Tags: Mobile edit Mobile web edit Advanced mobile edit |
||
(24 intermediate revisions by 13 users not shown) | |||
Line 1: | Line 1: | ||
This tutorial | This tutorial shows how to extend a NixOS configuration to include custom [[systemd]] units, by creating a [[systemd]] unit that initializes IRC client every time a system session starts. Beginning by adding functionality directly to a {{ic|configuration.nix}} file, it then shows how to abstract the functionality into a separate NixOS [[module]]. | ||
= | = The Problem = | ||
We want to start up an IRC client whenever a user logs into/starts their session. | |||
It is possible to find a variety of different ways to do this, but a simple modern approach that fits well within NixOS's [[Declarative model]] is to declare a {{ic|systemd}} unit which initializes the IRC client upon session login by a user. | |||
Assume that our IRC client is {{ic|irssi}} as the IRC client. We'll run it inside a [https://wiki.archlinux.org/title/GNU_Screen screen] daemon, which apart from allowing us to [https://en.wikipedia.org/wiki/Terminal_multiplexer multiplex] our terminal sessions, also enables the IRC session to continue even after we log out of our shell session. | |||
Note that due to the details of systemd, the service we create will run *per user*, not *per session*. | |||
= Some helpful links = | |||
This article assumes some familiarity with [[systemd]], and [[NixOS options]]. The following links will be helpful for providing this background: | |||
* General overviews of {{ic|systemd}}: [https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/chap-managing_services_with_systemd from RedHat], [https://wiki.archlinux.org/title/Systemd the Arch Linux Wiki], [https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units tutorial from Digital Ocean] | |||
* [https://www.freedesktop.org/software/systemd/man/latest/systemd.html# systemd man pages] | |||
* NixOS [[ modules ]] | |||
* use [[NixOS search|NixOS Search]] and [https://nixos.org/nixos/manual/options.html NixOS Manual: List of Options] to look up more information about specific module options that we use | |||
= Implementations = | = Implementations = | ||
Line 13: | Line 24: | ||
== Quick Implementation == | == Quick Implementation == | ||
NixOS provides [https://search.nixos.org/options?show=systemd a systemd module] with a wide variety of configuration options. A small number of those (which you can check out on [[ NixOS search ]]) allows us to implement this little snippet within our {{ic|configuration.nix}}: | |||
<syntaxhighlight lang="nix"> | |||
# pkgs is used to fetch screen & irssi. | # pkgs is used to fetch screen & irssi. | ||
{pkgs, ...}: | |||
{ | { | ||
# ircSession is the name of the new service we'll be creating | |||
systemd.services.ircSession = { | |||
# this service is "wanted by" (see systemd man pages, or other tutorials) the system | |||
# level that allows multiple users to login and interact with the machine non-graphically | |||
# (see the Red Hat tutorial or Arch Linux Wiki for more information on what each target means) | |||
# this is the "node" in the systemd dependency graph that will run the service | |||
wantedBy = [ "multi-user.target" ]; | |||
# systemd service unit declarations involve specifying dependencies and order of execution | |||
# of systemd nodes; here we are saying that we want our service to start after the network has | |||
# set up (as our IRC client needs to relay over the network) | |||
after = [ "network.target" ]; | |||
description = "Start the irc client of username."; | |||
serviceConfig = { | |||
# see systemd man pages for more information on the various options for "Type": "notify" | |||
# specifies that this is a service that waits for notification from its predecessor (declared in | |||
# `after=`) before starting | |||
Type = "notify"; | |||
# username that systemd will look for; if it exists, it will start a service associated with that user | |||
User = "username"; | |||
# the command to execute when the service starts up | |||
ExecStart = ''${pkgs.screen}/bin/screen -dmS irc ${pkgs.irssi}/bin/irssi''; | |||
# and the command to execute | |||
ExecStop = ''${pkgs.screen}/bin/screen -S irc -X quit''; | |||
}; | |||
}; | }; | ||
environment.systemPackages = [ pkgs.screen ]; | environment.systemPackages = [ pkgs.screen ]; | ||
# ... usual configuration ... | # ... usual configuration ... | ||
} | } | ||
</syntaxhighlight> | |||
What does this do? | What does this do? | ||
* <code>systemd.services.ircSession</code> option adds our new service to the {{ic|systemd}} module's {{ic|services}} [https://search.nixos.org/options?channel=unstable&show=systemd.services&from=0&size=50&sort=relevance&type=packages&query=systemd.service attribute set]. | |||
* The comments explain the various configuration steps declaring the definition of the new service. As you can see, we configure it to start when the network connects, and to execute a shell command. | |||
After rebuilding the NixOS configuration with this file, our IRC session should start when our network connects. The IRC session is started as a child to the screen daemon, which is independent of any user's session and will continue running when we log out. To connect to the IRC session, we SSH into the system, reconnect to the screen session, and choose the IRC window. Here's the command: | After rebuilding the NixOS configuration with this file, our IRC session should start when our network connects. The IRC session is started as a child to the screen daemon, which is independent of any user's session and will continue running when we log out. To connect to the IRC session, we SSH into the system, reconnect to the screen session, and choose the IRC window. Here's the command: | ||
ssh username@my-server -t screen -d -R irc | {{ ic | # ssh username@my-server -t screen -d -R irc }} | ||
== Conditional Implementation == | == Conditional Implementation == | ||
Line 60: | Line 76: | ||
We can use the <code>mkIf</code> function in the <code>configuration.nix</code> file to add conditional behavior. Here's the new implementation: | We can use the <code>mkIf</code> function in the <code>configuration.nix</code> file to add conditional behavior. Here's the new implementation: | ||
<syntaxhighlight lang="nix"> | |||
{config, pkgs, ...}: | {config, pkgs, lib, ...}: | ||
{ | { | ||
systemd.services = lib.mkIf (config.networking.hostName == "my-server") { | |||
ircSession = { | |||
wantedBy = [ "multi-user.target" ]; | |||
after = [ "network.target" ]; | |||
description = "Start the irc client of username."; | |||
serviceConfig = { | |||
Type = "forking"; | |||
User = "username"; | |||
ExecStart = ''${pkgs.screen}/bin/screen -dmS irc ${pkgs.irssi}/bin/irssi''; | |||
ExecStop = ''${pkgs.screen}/bin/screen -S irc -X quit''; | |||
}; | |||
}; | |||
}; | }; | ||
environment.systemPackages = | environment.systemPackages = lib.mkIf (config.networking.hostName == "my-server") [ pkgs.screen ]; | ||
# ... usual configuration ... | # ... usual configuration ... | ||
} | } | ||
</syntaxhighlight> | |||
This works, but if we use too many conditionals, our code will become difficult to read and modify. For example, what do we do when we want to change the hostname? | This works, but if we use too many conditionals, our code will become difficult to read and modify. For example, what do we do when we want to change the hostname? | ||
== Modular Configuration == | == Modular Configuration == | ||
To avoid using conditional expressions in our <code>configuration.nix</code> file, we can separate these properties into units and blend them together differently for each host. Nix allows us to do this with the <code>imports</code> | To avoid using conditional expressions in our <code>configuration.nix</code> file, we can separate these properties into units and blend them together differently for each host. Nix allows us to do this with the <code>imports</code> attribute (see [https://nixos.org/nixos/manual/index.html#sec-modularity NixOS Manual: Modularity]) to separate each concern into its own file. One way to organize this is to place common properties in the <code>configuration.nix</code> file and move the the IRC-related properties into an <code>irc-client.nix</code> file. | ||
If we move the IRC stuff into the <code>irc-client.nix</code> file, we change the <code>configuration.nix</code> file like this: | If we move the IRC stuff into the <code>irc-client.nix</code> file, we change the <code>configuration.nix</code> file like this: | ||
<syntaxhighlight lang="nix"> | |||
{ | { | ||
imports = [ | imports = [ | ||
Line 93: | Line 114: | ||
# ... usual configuration ... | # ... usual configuration ... | ||
} | } | ||
</syntaxhighlight> | |||
The <code>irc-client.nix</code> file will, of course, look like this: | The <code>irc-client.nix</code> file will, of course, look like this: | ||
<syntaxhighlight lang="nix"> | |||
{config, pkgs, ...}: | {config, pkgs, lib, ...}: | ||
lib.mkIf (config.networking.hostName == "my-server") { | |||
systemd.services.ircSession = { | |||
wantedBy = [ "multi-user.target" ]; | |||
after = [ "network.target" ]; | |||
description = "Start the irc client of username."; | |||
serviceConfig = { | |||
Type = "forking"; | |||
User = "username"; | |||
ExecStart = ''${pkgs.screen}/bin/screen -dmS irc ${pkgs.irssi}/bin/irssi''; | |||
ExecStop = ''${pkgs.screen}/bin/screen -S irc -X quit''; | |||
}; | |||
}; | }; | ||
environment.systemPackages = [ pkgs.screen ]; | environment.systemPackages = [ pkgs.screen ]; | ||
} | |||
</syntaxhighlight> | |||
If we organize our configuration like this, sharing it across machines is easier. In addition, our IRC client can be consistent across machines that choose to use it. | If we organize our configuration like this, sharing it across machines is easier. In addition, our IRC client can be consistent across machines that choose to use it. | ||
Line 117: | Line 143: | ||
NixOS supports this idea, but it is called "options". We can add options to our module for both the condition and the username. Here is what a <code>irc-client.nix</code> module with parameters/options looks like: | NixOS supports this idea, but it is called "options". We can add options to our module for both the condition and the username. Here is what a <code>irc-client.nix</code> module with parameters/options looks like: | ||
< | <syntaxhighlight lang="nix"> | ||
{config, pkgs, ...}: | {config, pkgs, lib, ...}: | ||
let | let | ||
Line 124: | Line 150: | ||
in | in | ||
with | with lib; | ||
{ | { | ||
Line 148: | Line 174: | ||
config = mkIf cfg.enable { | config = mkIf cfg.enable { | ||
systemd.services.ircSession = { | |||
description = "Start the irc client of | wantedBy = [ "multi-user.target" ]; | ||
after = [ "network.target" ]; | |||
description = "Start the irc client of username."; | |||
serviceConfig = { | |||
Type = "forking"; | |||
User = "${cfg.user}"; | |||
ExecStart = ''${pkgs.screen}/bin/screen -dmS irc ${pkgs.irssi}/bin/irssi''; | |||
ExecStop = ''${pkgs.screen}/bin/screen -S irc -X quit''; | |||
}; | |||
}; | }; | ||
environment.systemPackages = [ pkgs.screen ]; | environment.systemPackages = [ pkgs.screen ]; | ||
}; | }; | ||
} | } | ||
</ | </syntaxhighlight> | ||
This module is now independent of the system. Now, we must update our <code>configuration.nix</code> file to pass our condition and hostname into our new module. | This module is now independent of the system. Now, we must update our <code>configuration.nix</code> file to pass our condition and hostname into our new module. | ||
< | <syntaxhighlight lang="nix"> | ||
{config, ...}: | {config, ...}: | ||
Line 170: | Line 201: | ||
]; | ]; | ||
services.ircClient.enable = config.networking. | services.ircClient.enable = config.networking.hostName == "my-server"; | ||
services.ircClient.user = "username"; | services.ircClient.user = "username"; | ||
# ... usual configuration ... | # ... usual configuration ... | ||
} | } | ||
</ | </syntaxhighlight> | ||
= Testing Configuration Changes in a VM = | = Testing Configuration Changes in a VM = | ||
Line 247: | Line 214: | ||
To see how this works, create a file like this: | To see how this works, create a file like this: | ||
< | <syntaxhighlight lang="nix"> | ||
{config, pkgs, ...}: | {config, pkgs, ...}: | ||
{ | { | ||
Line 274: | Line 241: | ||
}; | }; | ||
} | } | ||
</ | </syntaxhighlight> | ||
Then, we build the new configuration inside a VM. If we named the above file <code>vmtest.nix</code>, we can use these commands: | |||
<syntaxhighlight lang="console"> | |||
# Create a VM from the new configuration. | |||
$ NIXOS_CONFIG=`pwd`/vmtest.nix nixos-rebuild -I nixos=/path/to/nixos/ build-vm | $ NIXOS_CONFIG=`pwd`/vmtest.nix nixos-rebuild -I nixos=/path/to/nixos/ build-vm | ||
# Then start it. | |||
$ ./result/bin/run-vmhost-vm | $ ./result/bin/run-vmhost-vm | ||
</syntaxhighlight> | |||
= What Next? = | = What Next? = | ||
Line 289: | Line 257: | ||
If you have another tutorial about extending NixOS, add a link below. | If you have another tutorial about extending NixOS, add a link below. | ||
* [http://larrythecow.org/archives/2011-10-11.html System Services on NixOS: larrythecow.org] | * [https://web.archive.org/web/20150331124128/http://larrythecow.org/archives/2011-10-11.html System Services on NixOS: larrythecow.org (archived)] | ||
[[Category:systemd]] | |||
[[Category:Tutorial]] | |||
[[Category:NixOS]] |