Difference between revisions of "Extend NixOS"

From NixOS Wiki
Jump to: navigation, search
m (rollback unauthorized mass edits)
Tag: Rollback
 
(11 intermediate revisions by 8 users not shown)
Line 1: Line 1:
This tutorial covers the major points of NixOS configuration. In this tutorial, we configure NixOS to start an IRC client every time the OS starts. This tutorial will  start by adding this functionality to the <code>configuration.nix</code> file. Then, this functionality is extracted into a separate file, which NixOS calls a "module".
+
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]].  
  
= Example: Add a System Service =
+
= The Problem =
  
Suppose you want to start an IRC client and connect to your favorite channel every time your NixOS starts. To do this, we will start the IRC client using a shell command. In other distributions, to perform an action on system start, you place the shell command in an init script, which is usually the <code>/etc/init.d</code> file.
+
We want to start up an IRC client whenever a user logs into/starts their session.
  
In NixOS, on the other hand, all system files are overwritten when the <code>configuration.nix</code> file is updated and the system is rebuilt. How does a NixOS user execute a shell command on system start? In NixOS, all dependencies are clean and organized, which means they are changed in a single place - the <code>configuration.nix</code> and the modules it loads. To change the system's behavior, then, we must add this IRC shell command to this configuration file.
+
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.
  
Lets start the IRC session using <code>irssi</code> as the IRC client. We'll run it inside a <code>screen</code> daemon, which enables the IRC session to continue even after we log out of our shell session.
+
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 ]] 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 ==
  
The simplest way to implement this is to add a simple snippet of code to the <code>/etc/nixos/configuration.nix</code> file:
+
NixOS provides [https://search.nixos.org/options?channel=23.05&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">
 
<syntaxhighlight lang="nix">
{pkgs, ...}:
 
 
 
  # 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 = {
 
   systemd.services.ircSession = {
       wantedBy = [ "multi-user.target" ];  
+
      # 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" ];
 
       after = [ "network.target" ];
 
       description = "Start the irc client of username.";
 
       description = "Start the irc client of username.";
 
       serviceConfig = {
 
       serviceConfig = {
         Type = "forking";
+
        # 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";
 
         User = "username";
         ExecStart = ''${pkgs.screen}/bin/screen -dmS irc ${pkgs.irssi}/bin/irssi'';         
+
        # 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'';
 
         ExecStop = ''${pkgs.screen}/bin/screen -S irc -X quit'';
 
       };
 
       };
Line 38: Line 61:
 
  }
 
  }
 
</syntaxhighlight>
 
</syntaxhighlight>
What does this do? The <code>systemd.services.ircSession</code> bit is an option which adds a new system service. This option is defined elsewhere in the NixOS configuration files. This is one of many options; you can see a list of all NixOS configuration options in the [https://nixos.org/nixos/manual/options.html NixOS Manual: List of Options] (or use https://nixos.org/nixos/options.html to search for options). We add attributes to this option to configure our new service. As you can see, we configure it to start when the network connects, and to execute a shell command.
+
 
 +
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 53: Line 80:
 
   
 
   
 
  {
 
  {
   systemd.services = lib.mkIf (config.networking.hostname == "my-server") {
+
   systemd.services = lib.mkIf (config.networking.hostName == "my-server") {
 
       ircSession = {
 
       ircSession = {
 
         wantedBy = [ "multi-user.target" ];  
 
         wantedBy = [ "multi-user.target" ];  
Line 67: Line 94:
 
   };
 
   };
 
   
 
   
   environment.systemPackages = lib.mkIf (config.networking.hostname == "my-server") [ pkgs.screen ];
+
   environment.systemPackages = lib.mkIf (config.networking.hostName == "my-server") [ pkgs.screen ];
 
   
 
   
 
   # ... usual configuration ...
 
   # ... usual configuration ...
Line 76: Line 103:
 
== 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> keyword (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.
+
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:
Line 92: Line 119:
 
  {config, pkgs, lib, ...}:
 
  {config, pkgs, lib, ...}:
 
   
 
   
  lib.mkIf (config.networking.hostname == "my-server") {
+
  lib.mkIf (config.networking.hostName == "my-server") {
 
   systemd.services.ircSession = {
 
   systemd.services.ircSession = {
 
       wantedBy = [ "multi-user.target" ];  
 
       wantedBy = [ "multi-user.target" ];  
Line 174: Line 201:
 
   ];
 
   ];
 
   
 
   
   services.ircClient.enable = config.networking.hostname == "my-server";
+
   services.ircClient.enable = config.networking.hostName == "my-server";
 
   services.ircClient.user = "username";
 
   services.ircClient.user = "username";
 
   
 
   
Line 216: Line 243:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Then, we build the new configuration inside a VM. If we named the above file <code>my-new-service.nix</code>, we can use these commands:
+
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">
 
<syntaxhighlight lang="console">
 
  # Create a VM from the new configuration.
 
  # Create a VM from the new configuration.
Line 231: Line 258:
  
 
* [https://web.archive.org/web/20150331124128/http://larrythecow.org/archives/2011-10-11.html System Services on NixOS: larrythecow.org (archived)]
 
* [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]]

Latest revision as of 10:53, 6 April 2024

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 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 Template:Declarative model is to declare a systemd unit which initializes the IRC client upon session login by a user.

Assume that our IRC client is irssi as the IRC client. We'll run it inside a screen daemon, which apart from allowing us to 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:

Implementations

Quick Implementation

NixOS provides 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 configuration.nix:

 # 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 ];
 
   # ... usual configuration ...
 }

What does this do?

  • systemd.services.ircSession option adds our new service to the systemd module's services 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:

# ssh username@my-server -t screen -d -R irc

Conditional Implementation

Suppose we want to share this functionality with your second computer, which is a similar NixOS system. The computers are very similar, so we can reuse most of the configuration file. How do we use the same configuration file, but change behavior depending on the host system? One way is to assume the "hostname" of each system is unique. If the hostname is X, we enable the service, and if it is Y, we disable it.

We can use the mkIf function in the configuration.nix file to add conditional behavior. Here's the new implementation:

 {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 = lib.mkIf (config.networking.hostName == "my-server") [ pkgs.screen ];
 
   # ... usual configuration ...
 }

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

To avoid using conditional expressions in our configuration.nix file, we can separate these properties into units and blend them together differently for each host. Nix allows us to do this with the imports attribute (see NixOS Manual: Modularity) to separate each concern into its own file. One way to organize this is to place common properties in the configuration.nix file and move the the IRC-related properties into an irc-client.nix file.

If we move the IRC stuff into the irc-client.nix file, we change the configuration.nix file like this:

 {
   imports = [
     ./irc-client.nix
   ];
 
   # ... usual configuration ...
 }

The irc-client.nix file will, of course, look like this:

 {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 ];
}

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.

Generic Module Configuration

Our IRC module is pretty useful, so we tell our friends on IRC about it. Now, they want to use our module. We still have our hostname hard-coded in our module, which isn't useful to our friends. We should remove stuff like this from our module before we distribute it to our friends. We should add a parameter so a user can pass their hostname to our module. How do we add a parameter to a module?

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 irc-client.nix module with parameters/options looks like:

 {config, pkgs, lib, ...}:
 
 let
   cfg = config.services.ircClient;
 in
 
 with lib;
 
 {
   options = {
     services.ircClient = {
       enable = mkOption {
         default = false;
         type = with types; bool;
         description = ''
           Start an irc client for a user.
         '';
       };
 
       user = mkOption {
         default = "username";
         type = with types; uniq string;
         description = ''
           Name of the user.
         '';
       };
     };
   };
 
   config = mkIf cfg.enable {
     systemd.services.ircSession = {
       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 ];
   };
 }

This module is now independent of the system. Now, we must update our configuration.nix file to pass our condition and hostname into our new module.

{config, ...}:

 {
   imports = [
     ./irc-client.nix
   ];
 
   services.ircClient.enable = config.networking.hostName == "my-server";
   services.ircClient.user = "username";
 
   # ... usual configuration ...
 }

Testing Configuration Changes in a VM

Creating or modifying a NixOS configuration can be trial-and-error. Rather than change our working system on each configuration change, we can build it completely inside a VM, which is much safer.

To see how this works, create a file like this:

 {config, pkgs, ...}:
 {
   # You need to configure a root filesytem
   fileSystems."/".label = "vmdisk";
 
   # The test vm name is based on the hostname, so it's nice to set one
   networking.hostName = "vmhost"; 
 
   # Add a test user who can sudo to the root account for debugging
   users.extraUsers.vm = {
     password = "vm";
     shell = "${pkgs.bash}/bin/bash";
     group = "wheel";
   };
   security.sudo = {
     enable = true;
     wheelNeedsPassword = false;
   };
 
   # Enable your new service!
   services =  {
     myNewService = {
       enable = true;
     };
   };
 }

Then, we build the new configuration inside a VM. If we named the above file vmtest.nix, we can use these commands:

 # Create a VM from the new configuration.
 $ NIXOS_CONFIG=`pwd`/vmtest.nix nixos-rebuild  -I nixos=/path/to/nixos/ build-vm
 # Then start it.
 $ ./result/bin/run-vmhost-vm

What Next?

This tutorial follows the evolution of NixOS configuration modification, which ends in creating distributable modules.

If you have another tutorial about extending NixOS, add a link below.