about summary refs log tree commit diff
path: root/nixos/modules/services/backup
diff options
context:
space:
mode:
authorEelco Dolstra <eelco.dolstra@logicblox.com>2013-10-10 13:28:20 +0200
committerEelco Dolstra <eelco.dolstra@logicblox.com>2013-10-10 13:28:20 +0200
commit5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010 (patch)
treea6c0f605be6de3f372ae69905b331f9f75452da7 /nixos/modules/services/backup
parent6070bc016bd2fd945b04347e25cfd3738622d2ac (diff)
downloadnixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar
nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.gz
nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.bz2
nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.lz
nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.xz
nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.tar.zst
nixlib-5c1f8cbc70cd5e6867ef6a2a06d27a40daa07010.zip
Move all of NixOS to nixos/ in preparation of the repository merge
Diffstat (limited to 'nixos/modules/services/backup')
-rw-r--r--nixos/modules/services/backup/almir.nix171
-rw-r--r--nixos/modules/services/backup/bacula.nix408
-rw-r--r--nixos/modules/services/backup/mysql-backup.nix81
-rw-r--r--nixos/modules/services/backup/postgresql-backup.nix66
-rw-r--r--nixos/modules/services/backup/rsnapshot.nix65
-rw-r--r--nixos/modules/services/backup/sitecopy-backup.nix104
6 files changed, 895 insertions, 0 deletions
diff --git a/nixos/modules/services/backup/almir.nix b/nixos/modules/services/backup/almir.nix
new file mode 100644
index 000000000000..d5bc932c6b96
--- /dev/null
+++ b/nixos/modules/services/backup/almir.nix
@@ -0,0 +1,171 @@
+{ config, pkgs, ... }:
+
+with pkgs.lib;
+
+let
+  cfg = config.services.almir;
+
+  bconsoleconf = pkgs.writeText "bconsole.conf"
+    ''
+      Director {
+        Name = ${cfg.director_name}
+        DIRport = ${toString cfg.director_port}
+        address = ${cfg.director_address}
+        Password = "${cfg.director_password}"
+      }
+    '';
+
+  productionini = pkgs.writeText "production.ini"
+    ''
+[app:main]
+use = egg:almir
+
+pyramid.reload_templates = false
+pyramid.debug_authorization = false
+pyramid.debug_notfound = false
+pyramid.debug_routematch = false
+pyramid.debug_templates = false
+pyramid.default_locale_name = en
+pyramid.includes =
+    pyramid_exclog
+exclog.extra_info = true
+
+sqlalchemy.url = ${cfg.sqlalchemy_engine_url}
+timezone = ${cfg.timezone}
+bconsole_config = ${bconsoleconf}
+
+[server:main]
+use = egg:waitress#main
+host = 127.0.0.1
+port = ${toString cfg.port}
+
+
+# Begin logging configuration
+
+[loggers]
+keys = root, almir, sqlalchemy, exc_logger
+
+[handlers]
+keys = console
+
+[formatters]
+keys = generic
+
+[logger_root]
+level = WARN
+handlers = console
+
+[logger_almir]
+level = WARN
+handlers =
+qualname = almir
+
+[logger_exc_logger]
+level = ERROR
+handlers =
+qualname = exc_logger
+
+[logger_sqlalchemy]
+level = WARN
+handlers =
+qualname = sqlalchemy.engine
+# "level = INFO" logs SQL queries.
+# "level = DEBUG" logs SQL queries and results.
+# "level = WARN" logs neither.  (Recommended for production systems.)
+
+[handler_console]
+class = StreamHandler
+args = (sys.stderr,)
+level = NOTSET
+formatter = generic
+
+[formatter_generic]
+format = %(asctime)s %(levelname)-5.5s [%(name)s][%(threadName)s] %(message)s
+    '';
+in {
+  options = {
+    services.almir = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable Almir web server. Also configures postgresql database and installs bacula.
+        '';
+      };
+
+      port = mkOption {
+        default = 35000;
+        type = types.uniq types.int;
+        description = ''
+          Port for Almir web server to listen on.
+        '';
+      };
+
+      timezone = mkOption {
+	description = ''
+         Timezone as specified in https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
+        '';
+        example = "Europe/Ljubljana";
+      };
+
+      sqlalchemy_engine_url = mkOption {
+        example = ''
+          postgresql://bacula:bacula@localhost:5432/bacula
+          mysql+mysqlconnector://<user>:<password>@<hostname>/<database>'
+          sqlite:////var/lib/bacula/bacula.db'
+        '';
+	description = ''
+         Define SQL database connection to bacula catalog as specified in http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls
+        '';
+      };
+
+      director_name = mkOption {
+        description = ''
+          Name of the Director to connect with bconsole.
+        '';
+      };
+
+      director_password = mkOption {
+        description = ''
+          Password for Director to connect with bconsole.
+        '';
+      };
+
+      director_port = mkOption {
+        default = 9101;
+        type = types.int;
+        description = ''
+          Port for Director to connect with bconsole.
+        '';
+      };
+
+      director_address = mkOption {
+        default = "127.0.0.1";
+        description = ''
+          IP/Hostname for Director to connect with bconsole.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+    systemd.services.almir = {
+      after = [ "network.target" "postgresql.service" ];
+      description = "Almir web app";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.pythonPackages.almir ];
+      serviceConfig.ExecStart = "${pkgs.pythonPackages.almir}/bin/pserve ${productionini}";
+    };
+
+    environment.systemPackages = [ pkgs.pythonPackages.almir ];
+
+    users.extraUsers.almir = {
+      group = "almir";
+      uid = config.ids.uids.almir;
+      createHome = true;
+      shell = "${pkgs.bash}/bin/bash";
+    };
+
+    users.extraGroups.almir.gid = config.ids.gids.almir;
+  };
+}
diff --git a/nixos/modules/services/backup/bacula.nix b/nixos/modules/services/backup/bacula.nix
new file mode 100644
index 000000000000..272903c99e33
--- /dev/null
+++ b/nixos/modules/services/backup/bacula.nix
@@ -0,0 +1,408 @@
+{ config, pkgs, ... }:
+
+# TODO: test configuration when building nixexpr (use -t parameter)
+# TODO: support sqlite3 (it's deprecate?) and mysql
+
+with pkgs.lib;
+
+let
+  libDir = "/var/lib/bacula";
+
+  fd_cfg = config.services.bacula-fd;
+  fd_conf = pkgs.writeText "bacula-fd.conf"
+    ''
+      Client {
+        Name = "${fd_cfg.name}";
+        FDPort = ${toString fd_cfg.port};
+        WorkingDirectory = "${libDir}";
+        Pid Directory = "/var/run";
+        ${fd_cfg.extraClientConfig}
+      }
+     
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Director {
+        Name = "${name}";
+        Password = "${value.password}";
+        Monitor = "${value.monitor}";
+      }
+      '') fd_cfg.director)}
+     
+      Messages {
+        Name = Standard;
+        syslog = all, !skipped, !restored
+        ${fd_cfg.extraMessagesConfig}
+      }
+    '';
+
+  sd_cfg = config.services.bacula-sd;
+  sd_conf = pkgs.writeText "bacula-sd.conf" 
+    ''
+      Storage {
+        Name = "${sd_cfg.name}";
+        SDPort = ${toString sd_cfg.port};
+        WorkingDirectory = "${libDir}";
+        Pid Directory = "/var/run";
+        ${sd_cfg.extraStorageConfig}
+      }
+ 
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Device {
+        Name = "${name}";
+        Archive Device = "${value.archiveDevice}";
+        Media Type = "${value.mediaType}";
+        ${value.extraDeviceConfig}
+      }
+      '') sd_cfg.device)}
+
+      ${concatStringsSep "\n" (mapAttrsToList (name: value: ''
+      Director {
+        Name = "${name}";
+        Password = "${value.password}";
+        Monitor = "${value.monitor}";
+      }
+      '') sd_cfg.director)}
+
+      Messages {
+        Name = Standard;
+        syslog = all, !skipped, !restored
+        ${sd_cfg.extraMessagesConfig}
+      }
+    '';
+
+  dir_cfg = config.services.bacula-dir;
+  dir_conf = pkgs.writeText "bacula-dir.conf" 
+    ''
+    Director {
+      Name = "${dir_cfg.name}";
+      Password = "${dir_cfg.password}";
+      DirPort = ${toString dir_cfg.port};
+      Working Directory = "${libDir}";
+      Pid Directory = "/var/run/";
+      QueryFile = "${pkgs.bacula}/etc/query.sql";
+      ${dir_cfg.extraDirectorConfig}
+    }
+
+    Catalog {
+      Name = "PostgreSQL";
+      dbname = "bacula";
+      user = "bacula";
+    }
+
+    Messages {
+      Name = Standard;
+      syslog = all, !skipped, !restored
+      ${dir_cfg.extraMessagesConfig}
+    }
+
+    ${dir_cfg.extraConfig}
+    '';
+
+  # TODO: by default use this config
+  bconsole_conf = pkgs.writeText "bconsole.conf"
+    ''
+    Director {
+      Name = ${dir_cfg.name};
+      Address = "localhost";
+      DirPort = ${toString dir_cfg.port};
+      Password = "${dir_cfg.password}";
+    }
+    '';
+
+  directorOptions = {name, config, ...}:
+  {
+    options = {
+      password = mkOption {
+        # TODO: required?
+        description = ''
+           Specifies the password that must be supplied for a Director to b
+        '';
+      };
+      
+      monitor = mkOption {
+        default = "no";
+        example = "yes";
+        description = ''
+           If Monitor is set to no (default), this director will have full 
+        '';
+      };
+    };
+  };
+
+  deviceOptions = {name, config, ...}:
+  {
+    options = {
+      archiveDevice = mkOption {
+        # TODO: required?
+        description = ''
+          The specified name-string gives the system file name of the storage device managed by this storage daemon. This will usually be the device file name of a removable storage device (tape drive), for example " /dev/nst0" or "/dev/rmt/0mbn". For a DVD-writer, it will be for example /dev/hdc. It may also be a directory name if you are archiving to disk storage.
+        '';
+      };
+
+      mediaType = mkOption {
+        # TODO: required?
+        description = ''
+          The specified name-string names the type of media supported by this device, for example, "DLT7000". Media type names are arbitrary in that you set them to anything you want, but they must be known to the volume database to keep track of which storage daemons can read which volumes. In general, each different storage type should have a unique Media Type associated with it. The same name-string must appear in the appropriate Storage resource definition in the Director's configuration file.
+        '';
+      };
+
+      extraDeviceConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Device directive.
+        '';
+        example = ''
+          LabelMedia = yes
+          Random Access = no
+          AutomaticMount = no
+          RemovableMedia = no
+          MaximumOpenWait = 60
+          AlwaysOpen = no
+        '';
+      };
+    };
+  };
+
+in {
+  options = {
+    services.bacula-fd = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Bacula File Daemon.
+        '';
+      };
+ 
+      name = mkOption {
+        default = "${config.networking.hostName}-fd";
+        description = ''
+        	The client name that must be used by the Director when connecting. Generally, it is a good idea to use a name related to the machine so that error messages can be easily identified if you have multiple Clients. This directive is required.
+        '';
+      };
+ 
+      port = mkOption {
+        default = 9102;
+        type = types.uniq types.int;
+        description = ''
+        	This specifies the port number on which the Client listens for Director connections. It must agree with the FDPort specified in the Client resource of the Director's configuration file. The default is 9102.
+        '';
+      };
+ 
+      director = mkOption {
+        default = {};
+        description = ''
+          This option defines director resources in Bacula File Daemon.
+        '';
+        type = types.attrsOf types.optionSet;
+        options = [ directorOptions ];
+      };
+
+      extraClientConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Client directive.
+        '';
+        example = ''
+          Maximum Concurrent Jobs = 20;
+          Heartbeat Interval = 30;
+        '';
+      };
+
+      extraMessagesConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Messages directive.
+        '';
+        example = ''
+          console = all
+        '';
+      };
+    };
+
+    services.bacula-sd = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Bacula Storage Daemon.
+        '';
+      };
+ 
+      name = mkOption {
+        default = "${config.networking.hostName}-sd";
+        description = ''
+          Specifies the Name of the Storage daemon.
+        '';
+      };
+ 
+      port = mkOption {
+        default = 9103;
+        type = types.uniq types.int;
+        description = ''
+          Specifies port number on which the Storage daemon listens for Director connections. The default is 9103.
+        '';
+      };
+
+      director = mkOption {
+        default = {};
+        description = ''
+          This option defines Director resources in Bacula Storage Daemon.
+        '';
+        type = types.attrsOf types.optionSet;
+        options = [ directorOptions ];
+      };
+
+      device = mkOption {
+        default = {};
+        description = ''
+          This option defines Device resources in Bacula Storage Daemon.
+        '';
+        type = types.attrsOf types.optionSet;
+        options = [ deviceOptions ];
+      };
+ 
+      extraStorageConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Storage directive.
+        '';
+        example = ''
+          Maximum Concurrent Jobs = 20;
+          Heartbeat Interval = 30;
+        '';
+      };
+
+      extraMessagesConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Messages directive.
+        '';
+        example = ''
+          console = all
+        '';
+      };
+ 
+    };
+
+    services.bacula-dir = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Whether to enable Bacula Director Daemon.
+        '';
+      };
+
+      name = mkOption {
+        default = "${config.networking.hostName}-dir";
+        description = ''
+          The director name used by the system administrator. This directive is required.
+        '';
+      };
+ 
+      port = mkOption {
+        default = 9101;
+        type = types.uniq types.int;
+        description = ''
+          Specify the port (a positive integer) on which the Director daemon will listen for Bacula Console connections. This same port number must be specified in the Director resource of the Console configuration file. The default is 9101, so normally this directive need not be specified. This directive should not be used if you specify DirAddresses (N.B plural) directive.
+        '';
+      };
+ 
+      password = mkOption {
+        # TODO: required?
+        description = ''
+           Specifies the password that must be supplied for a Director.
+        '';
+      };
+
+      extraMessagesConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Messages directive.
+        '';
+        example = ''
+          console = all
+        '';
+      };
+
+      extraDirectorConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration to be passed in Director directive.
+        '';
+        example = ''
+          Maximum Concurrent Jobs = 20;
+          Heartbeat Interval = 30;
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        description = ''
+          Extra configuration for Bacula Director Daemon.
+        '';
+        example = ''
+          TODO
+        '';
+      };
+    };
+  };
+
+  config = mkIf (fd_cfg.enable || sd_cfg.enable || dir_cfg.enable) {
+    systemd.services.bacula-fd = mkIf fd_cfg.enable {
+      after = [ "network.target" ];
+      description = "Bacula File Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.bacula ];
+      serviceConfig.ExecStart = "${pkgs.bacula}/sbin/bacula-fd -f -u root -g bacula -c ${fd_conf}";
+      serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+    };
+
+    systemd.services.bacula-sd = mkIf sd_cfg.enable {
+      after = [ "network.target" ];
+      description = "Bacula Storage Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.bacula ];
+      serviceConfig.ExecStart = "${pkgs.bacula}/sbin/bacula-sd -f -u bacula -g bacula -c ${sd_conf}";
+      serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+    };
+
+    services.postgresql.enable = dir_cfg.enable == true;
+
+    systemd.services.bacula-dir = mkIf dir_cfg.enable {
+      after = [ "network.target" "postgresql.service" ];
+      description = "Bacula Director Daemon";
+      wantedBy = [ "multi-user.target" ];
+      path = [ pkgs.bacula ];
+      serviceConfig.ExecStart = "${pkgs.bacula}/sbin/bacula-dir -f -u bacula -g bacula -c ${dir_conf}";
+      serviceConfig.ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
+      preStart = ''
+        if ! test -e "${libDir}/db-created"; then
+            ${pkgs.postgresql}/bin/createuser --no-superuser --no-createdb --no-createrole bacula
+            #${pkgs.postgresql}/bin/createdb --owner bacula bacula
+
+            # populate DB
+            ${pkgs.bacula}/etc/create_bacula_database postgresql
+            ${pkgs.bacula}/etc/make_bacula_tables postgresql
+            ${pkgs.bacula}/etc/grant_bacula_privileges postgresql
+            touch "${libDir}/db-created"
+        else
+            ${pkgs.bacula}/etc/update_bacula_tables postgresql || true
+        fi
+      '';
+    };
+
+    environment.systemPackages = [ pkgs.bacula ];
+
+    users.extraUsers.bacula = {
+      group = "bacula";
+      uid = config.ids.uids.bacula;
+      home = "${libDir}";
+      createHome = true;
+      description = "Bacula Daemons user";
+      shell = "${pkgs.bash}/bin/bash";
+    };
+
+    users.extraGroups.bacula.gid = config.ids.gids.bacula;
+  };
+}
diff --git a/nixos/modules/services/backup/mysql-backup.nix b/nixos/modules/services/backup/mysql-backup.nix
new file mode 100644
index 000000000000..3ff9978fbb96
--- /dev/null
+++ b/nixos/modules/services/backup/mysql-backup.nix
@@ -0,0 +1,81 @@
+{ config, pkgs, ... }:
+
+with pkgs.lib;
+
+let
+
+  inherit (pkgs) mysql gzip;
+
+  cfg = config.services.mysqlBackup ;
+  location = cfg.location ;
+  mysqlBackupCron = db : ''
+    ${cfg.period} ${cfg.user} ${mysql}/bin/mysqldump ${if cfg.singleTransaction then "--single-transaction" else ""} ${db} | ${gzip}/bin/gzip -c > ${location}/${db}.gz
+  '';
+
+in
+
+{
+  options = {
+
+    services.mysqlBackup = {
+
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to enable MySQL backups.
+        '';
+      };
+
+      period = mkOption {
+        default = "15 01 * * *";
+        description = ''
+          This option defines (in the format used by cron) when the
+          databases should be dumped.
+          The default is to update at 01:15 (at night) every day.
+        '';
+      };
+
+      user = mkOption {
+        default = "mysql";
+        description = ''
+          User to be used to perform backup.
+        '';
+      };
+
+      databases = mkOption {
+        default = [];
+        description = ''
+          List of database names to dump.
+        '';
+      };
+
+      location = mkOption {
+        default = "/var/backup/mysql";
+        description = ''
+          Location to put the gzipped MySQL database dumps.
+        '';
+      };
+
+      singleTransaction = mkOption {
+        default = false;
+        description = ''
+          Whether to create database dump in a single transaction
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf config.services.mysqlBackup.enable {
+
+    services.cron.systemCronJobs = map mysqlBackupCron config.services.mysqlBackup.databases;
+
+    system.activationScripts.mysqlBackup = stringAfter [ "stdio" "users" ]
+      ''
+        mkdir -m 0700 -p ${config.services.mysqlBackup.location}
+        chown ${config.services.mysqlBackup.user} ${config.services.mysqlBackup.location}
+      '';
+
+  };
+
+}
diff --git a/nixos/modules/services/backup/postgresql-backup.nix b/nixos/modules/services/backup/postgresql-backup.nix
new file mode 100644
index 000000000000..e68ad794a96b
--- /dev/null
+++ b/nixos/modules/services/backup/postgresql-backup.nix
@@ -0,0 +1,66 @@
+{ config, pkgs, ... }:
+
+with pkgs.lib;
+
+let
+  inherit (pkgs) postgresql gzip;
+
+  location = config.services.postgresqlBackup.location ;
+
+  postgresqlBackupCron = db:
+    ''
+      ${config.services.postgresqlBackup.period} root ${postgresql}/bin/pg_dump ${db} | ${gzip}/bin/gzip -c > ${location}/${db}.gz
+    '';
+
+in
+
+{
+
+  options = {
+
+    services.postgresqlBackup = {
+
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to enable PostgreSQL dumps.
+        '';
+      };
+
+      period = mkOption {
+        default = "15 01 * * *";
+        description = ''
+          This option defines (in the format used by cron) when the
+          databases should be dumped.
+          The default is to update at 01:15 (at night) every day.
+        '';
+      };
+
+      databases = mkOption {
+        default = [];
+        description = ''
+          List of database names to dump.
+        '';
+      };
+
+      location = mkOption {
+        default = "/var/backup/postgresql";
+        description = ''
+          Location to put the gzipped PostgreSQL database dumps.
+        '';
+      };
+    };
+
+  };
+
+  config = mkIf config.services.postgresqlBackup.enable {
+    services.cron.systemCronJobs = map postgresqlBackupCron config.services.postgresqlBackup.databases;
+
+    system.activationScripts.postgresqlBackup = stringAfter [ "stdio" "users" ]
+      ''
+        mkdir -m 0700 -p ${config.services.postgresqlBackup.location}
+        chown root ${config.services.postgresqlBackup.location}
+      '';
+  };
+
+}
diff --git a/nixos/modules/services/backup/rsnapshot.nix b/nixos/modules/services/backup/rsnapshot.nix
new file mode 100644
index 000000000000..178ba3ec7207
--- /dev/null
+++ b/nixos/modules/services/backup/rsnapshot.nix
@@ -0,0 +1,65 @@
+{ config, pkgs, ... }:
+
+with pkgs.lib;
+
+let cfg = config.services.rsnapshot;
+in
+{
+  options = {
+    services.rsnapshot = {
+      enable = mkEnableOption "rsnapshot backups";
+
+      extraConfig = mkOption {
+        default = "";
+        example = ''
+          retains	hourly	24
+          retain	daily	365
+          backup	/home/	localhost/
+        '';
+        type = types.lines;
+        description = ''
+          rsnapshot configuration option in addition to the defaults from
+          rsnapshot and this module.
+
+          Note that tabs are required to separate option arguments, and
+          directory names require trailing slashes.
+
+          The "extra" in the option name might be a little misleading right
+          now, as it is required to get a functional configuration.
+        '';
+      };
+
+      cronIntervals = mkOption {
+        default = {};
+        example = { "hourly" = "0 * * * *"; "daily" = "50 21 * * *"; };
+        type = types.attrsOf types.string;
+        description = ''
+          Periodicity at which intervals should be run by cron.
+          Note that the intervals also have to exist in configuration
+          as retain options.
+        '';
+      };
+    };
+  };
+
+  config = mkIf cfg.enable (let
+    myRsnapshot = pkgs.rsnapshot.override { configFile = rsnapshotCfg; };
+    rsnapshotCfg = with pkgs; writeText "gen-rsnapshot.conf" (''
+        config_version	1.2
+        cmd_cp	${coreutils}/bin/cp
+        cmd_rsync	${rsync}/bin/rsync
+        cmd_ssh	${openssh}/bin/ssh
+        cmd_logger	${inetutils}/bin/logger
+        cmd_du	${coreutils}/bin/du
+        lockfile	/run/rsnapshot.pid
+
+        ${cfg.extraConfig}
+      '');
+    in {
+      environment.systemPackages = [ myRsnapshot ];
+
+      services.cron.systemCronJobs =
+        mapAttrsToList (interval: time: "${time} root ${myRsnapshot}/bin/rsnapshot ${interval}") cfg.cronIntervals;
+    }
+  );
+}
diff --git a/nixos/modules/services/backup/sitecopy-backup.nix b/nixos/modules/services/backup/sitecopy-backup.nix
new file mode 100644
index 000000000000..5c7f7ffae5b3
--- /dev/null
+++ b/nixos/modules/services/backup/sitecopy-backup.nix
@@ -0,0 +1,104 @@
+{ config, pkgs, ... }:
+
+with pkgs.lib;
+
+let
+  inherit (pkgs) sitecopy;
+
+  stateDir = "/var/spool/sitecopy";
+
+  sitecopyCron = backup : ''
+    ${if backup ? period then backup.period else config.services.sitecopy.period} root ${sitecopy}/bin/sitecopy --storepath=${stateDir} --rcfile=${stateDir}/${backup.name}.conf --update ${backup.name} >> /var/log/sitecopy.log 2>&1
+  '';
+in
+
+{
+
+  options = {
+
+    services.sitecopy = {
+
+      enable = mkOption {
+        default = false;
+        description = ''
+          Whether to enable sitecopy backups of specified directories.
+        '';
+      };
+
+      period = mkOption {
+        default = "15 04 * * *";
+        description = ''
+          This option defines (in the format used by cron) when the
+          sitecopy backup are being run.
+          The default is to update at 04:15 (at night) every day.
+        '';
+      };
+
+      backups = mkOption {
+        example = [
+          { name = "test";
+            local = "/tmp/backup";
+            remote = "/staff-groups/ewi/st/strategoxt/backup/test";
+            server = "webdata.tudelft.nl";
+            protocol = "webdav";
+            https = true ;
+            symlinks = "maintain" ;
+          }
+        ];
+        default = [];
+        description = ''
+           List of attributesets describing the backups.
+
+           Username/password are extracted from <filename>${stateDir}/sitecopy.secrets</filename> at activation
+           time. The secrets file lines should have the following structure:
+           <screen>
+             server username password
+           </screen>
+        '';
+      };
+
+    };
+
+  };
+
+  config = mkIf config.services.sitecopy.enable {
+    environment.systemPackages = [ sitecopy ];
+
+    services.cron.systemCronJobs = map sitecopyCron config.services.sitecopy.backups;
+
+    system.activationScripts.sitecopyBackup = stringAfter [ "stdio" "users" ]
+      ''
+        mkdir -m 0700 -p ${stateDir}
+        chown root ${stateDir}
+        touch ${stateDir}/sitecopy.secrets
+        chown root ${stateDir}/sitecopy.secrets
+
+        ${pkgs.lib.concatStrings (map ( b: ''
+            unset secrets
+            unset secret
+            secrets=`grep '^${b.server}' ${stateDir}/sitecopy.secrets | head -1`
+            secret=($secrets)
+            cat > ${stateDir}/${b.name}.conf << EOF
+              site ${b.name}
+              server ${b.server}
+              protocol ${b.protocol}
+              username ''${secret[1]}
+              password ''${secret[2]}
+              local ${b.local}
+              remote ${b.remote}
+              symlinks ${b.symlinks}
+              ${if b.https then "http secure" else ""}
+            EOF
+            chmod 0600 ${stateDir}/${b.name}.conf
+            if ! test -e ${stateDir}/${b.name} ; then
+              echo " * Initializing sitecopy '${b.name}'"
+              ${sitecopy}/bin/sitecopy --storepath=${stateDir} --rcfile=${stateDir}/${b.name}.conf --initialize ${b.name}
+            else
+              echo " * Sitecopy '${b.name}' already initialized"
+            fi
+          '' ) config.services.sitecopy.backups
+        )}
+      '';
+  };
+
+}