summary refs log tree commit diff
path: root/nixos
diff options
context:
space:
mode:
authorNicolas B. Pierron <nicolas.b.pierron@gmail.com>2014-12-14 14:17:56 +0100
committerNicolas B. Pierron <nicolas.b.pierron@gmail.com>2014-12-14 14:17:56 +0100
commit42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502 (patch)
tree120fc5aa29603144f280cf1a6956da4089a113b9 /nixos
parent28f4ef2a5fda5d65e3567a0ab48ff17656cf9eac (diff)
parent3d74b3810104878527f0fde8aad65908579a504e (diff)
downloadnixpkgs-42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502.tar
nixpkgs-42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502.tar.gz
nixpkgs-42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502.tar.bz2
nixpkgs-42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502.tar.lz
nixpkgs-42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502.tar.xz
nixpkgs-42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502.tar.zst
nixpkgs-42c3c205c4b0047f7dd3e39b43f5e5bd0f0b9502.zip
Merge remote-tracking branch 'origin/master' into syncserver
Diffstat (limited to 'nixos')
-rw-r--r--nixos/doc/manual/development/sources.xml6
-rw-r--r--nixos/doc/manual/installation/upgrading.xml12
-rwxr-xr-xnixos/maintainers/scripts/ec2/create-ebs-amis.py2
-rw-r--r--nixos/modules/config/networking.nix2
-rw-r--r--nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt2
-rw-r--r--nixos/modules/installer/tools/nixos-install.sh6
-rw-r--r--nixos/modules/misc/ids.nix2
-rwxr-xr-xnixos/modules/module-list.nix2
-rw-r--r--nixos/modules/profiles/container.nix19
-rw-r--r--nixos/modules/programs/virtualbox-host.nix14
-rw-r--r--nixos/modules/services/desktops/profile-sync-daemon.nix139
-rw-r--r--nixos/modules/services/misc/defaultUnicornConfig.rb206
-rw-r--r--nixos/modules/services/misc/gitlab.nix295
-rw-r--r--nixos/modules/services/networking/networkmanager.nix2
-rw-r--r--nixos/modules/system/boot/stage-1.nix16
-rw-r--r--nixos/release-combined.nix1
-rw-r--r--nixos/release.nix4
-rw-r--r--nixos/tests/gitlab.nix20
-rw-r--r--nixos/tests/kubernetes.nix2
-rw-r--r--nixos/tests/quake3.nix4
-rw-r--r--nixos/tests/virtualbox.nix250
21 files changed, 969 insertions, 37 deletions
diff --git a/nixos/doc/manual/development/sources.xml b/nixos/doc/manual/development/sources.xml
index 992a07af981..f9fadd6bf5e 100644
--- a/nixos/doc/manual/development/sources.xml
+++ b/nixos/doc/manual/development/sources.xml
@@ -50,8 +50,8 @@ Or, to base your local branch on the latest version available in the
 NixOS channel:
 
 <screen>
-$ curl -sI http://nixos.org/channels/nixos-unstable/ | grep Location
-Location: http://releases.nixos.org/nixos/unstable/nixos-14.10pre43986.acaf4a6/
+$ curl -sI https://nixos.org/channels/nixos-unstable/ | grep Location
+Location: https://releases.nixos.org/nixos/unstable/nixos-14.10pre43986.acaf4a6/
 
 $ git checkout -b local acaf4a6
 </screen>
@@ -92,4 +92,4 @@ to <command>nix-env</command>, as it will break after interpreting expressions
 in <filename>nixos/</filename> as packages.</para>
 -->
 
-</chapter>
\ No newline at end of file
+</chapter>
diff --git a/nixos/doc/manual/installation/upgrading.xml b/nixos/doc/manual/installation/upgrading.xml
index e4e21603329..46d3af56b57 100644
--- a/nixos/doc/manual/installation/upgrading.xml
+++ b/nixos/doc/manual/installation/upgrading.xml
@@ -15,7 +15,7 @@ been built.  These channels are:
 <itemizedlist>
   <listitem>
     <para>Stable channels, such as <literal
-    xlink:href="http://nixos.org/channels/nixos-14.04">nixos-14.04</literal>.
+    xlink:href="https://nixos.org/channels/nixos-14.04">nixos-14.04</literal>.
     These only get conservative bug fixes and package upgrades.  For
     instance, a channel update may cause the Linux kernel on your
     system to be upgraded from 3.4.66 to 3.4.67 (a minor bug fix), but
@@ -26,7 +26,7 @@ been built.  These channels are:
   </listitem>
   <listitem>
     <para>The unstable channel, <literal
-    xlink:href="http://nixos.org/channels/nixos-unstable">nixos-unstable</literal>.
+    xlink:href="https://nixos.org/channels/nixos-unstable">nixos-unstable</literal>.
     This corresponds to NixOS’s main development branch, and may thus
     see radical changes between channel updates.  It’s not recommended
     for production systems.</para>
@@ -34,7 +34,7 @@ been built.  These channels are:
 </itemizedlist>
 
 To see what channels are available, go to <link
-xlink:href="http://nixos.org/channels"/>.  (Note that the URIs of the
+xlink:href="https://nixos.org/channels"/>.  (Note that the URIs of the
 various channels redirect to a directory that contains the channel’s
 latest version and includes ISO images and VirtualBox
 appliances.)</para>
@@ -53,20 +53,20 @@ nixos https://nixos.org/channels/nixos-unstable
 To switch to a different NixOS channel, do
 
 <screen>
-$ nix-channel --add http://nixos.org/channels/<replaceable>channel-name</replaceable> nixos
+$ nix-channel --add https://nixos.org/channels/<replaceable>channel-name</replaceable> nixos
 </screen>
 
 (Be sure to include the <literal>nixos</literal> parameter at the
 end.)  For instance, to use the NixOS 14.04 stable channel:
 
 <screen>
-$ nix-channel --add http://nixos.org/channels/nixos-14.04 nixos
+$ nix-channel --add https://nixos.org/channels/nixos-14.04 nixos
 </screen>
 
 But if you want to live on the bleeding edge:
 
 <screen>
-$ nix-channel --add http://nixos.org/channels/nixos-unstable nixos
+$ nix-channel --add https://nixos.org/channels/nixos-unstable nixos
 </screen>
 
 </para>
diff --git a/nixos/maintainers/scripts/ec2/create-ebs-amis.py b/nixos/maintainers/scripts/ec2/create-ebs-amis.py
index 62525651ae0..6c91aa68694 100755
--- a/nixos/maintainers/scripts/ec2/create-ebs-amis.py
+++ b/nixos/maintainers/scripts/ec2/create-ebs-amis.py
@@ -75,7 +75,7 @@ m.run_command("mount {0} /mnt".format(device))
 m.run_command("touch /mnt/.ebs")
 m.run_command("mkdir -p /mnt/etc/nixos")
 
-m.run_command("nix-channel --add http://nixos.org/channels/nixos-{} nixos".format(args.channel))
+m.run_command("nix-channel --add https://nixos.org/channels/nixos-{} nixos".format(args.channel))
 m.run_command("nix-channel --update")
 
 version = m.run_command("nix-instantiate --eval-only -A lib.nixpkgsVersion '<nixpkgs>'", capture_stdout=True).split(' ')[0].replace('"','').strip()
diff --git a/nixos/modules/config/networking.nix b/nixos/modules/config/networking.nix
index 77e55fddf69..f99cea7d17b 100644
--- a/nixos/modules/config/networking.nix
+++ b/nixos/modules/config/networking.nix
@@ -140,7 +140,7 @@ in
             '' + optionalString config.services.nscd.enable ''
               # Invalidate the nscd cache whenever resolv.conf is
               # regenerated.
-              libc_restart='${pkgs.systemd}/bin/systemctl try-restart --no-block nscd.service'
+              libc_restart='${pkgs.systemd}/bin/systemctl try-restart --no-block nscd.service 2> /dev/null'
             '' + optionalString cfg.dnsSingleRequest ''
               # only send one DNS request at a time
               resolv_conf_options='single-request'
diff --git a/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt b/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
index 8f0a8d355c6..84252f292c5 100644
--- a/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
+++ b/nixos/modules/installer/cd-dvd/system-tarball-pc-readme.txt
@@ -80,7 +80,7 @@ had booted this nixos. Run:
 *  `grep local-cmds run/current-system/init`
 
 Then you can proceed normally subscribing to a nixos channel:
-  nix-channel --add http://nixos.org/channels/nixos-unstable
+  nix-channel --add https://nixos.org/channels/nixos-unstable
   nix-channel --update
 
 Testing:
diff --git a/nixos/modules/installer/tools/nixos-install.sh b/nixos/modules/installer/tools/nixos-install.sh
index bd334c2a3cb..a7333fbd541 100644
--- a/nixos/modules/installer/tools/nixos-install.sh
+++ b/nixos/modules/installer/tools/nixos-install.sh
@@ -89,6 +89,12 @@ ln -s /run $mountPoint/var/run
 rm -f $mountPoint/etc/{resolv.conf,hosts}
 cp -Lf /etc/resolv.conf /etc/hosts $mountPoint/etc/
 
+if [ -e "$SSL_CERT_FILE" ]; then
+    cp -Lf "$SSL_CERT_FILE" "$mountPoint/tmp/ca-cert.crt"
+    export SSL_CERT_FILE=/tmp/ca-cert.crt
+    # For Nix 1.7
+    export CURL_CA_BUNDLE=/tmp/ca-cert.crt
+fi
 
 if [ -n "$runChroot" ]; then
     if ! [ -L $mountPoint/nix/var/nix/profiles/system ]; then
diff --git a/nixos/modules/misc/ids.nix b/nixos/modules/misc/ids.nix
index bdaf85a03ce..3bb7fdb9b2d 100644
--- a/nixos/modules/misc/ids.nix
+++ b/nixos/modules/misc/ids.nix
@@ -172,6 +172,7 @@
       kubernetes = 162;
       peerflix = 163;
       chronos = 164;
+      gitlab = 165;
 
       # When adding a uid, make sure it doesn't match an existing gid. And don't use uids above 399!
 
@@ -308,6 +309,7 @@
       bosun = 157;
       kubernetes = 158;
       fleet = 159;
+      gitlab = 160;
 
       # When adding a gid, make sure it doesn't match an existing uid. And don't use gids above 399!
 
diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index cbd5ac6a7ef..caf81de2af2 100755
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -137,6 +137,7 @@
   ./services/desktops/gnome3/seahorse.nix
   ./services/desktops/gnome3/sushi.nix
   ./services/desktops/gnome3/tracker.nix
+  ./services/desktops/profile-sync-daemon.nix
   ./services/desktops/telepathy.nix
   ./services/games/ghost-one.nix
   ./services/games/minecraft-server.nix
@@ -176,6 +177,7 @@
   ./services/misc/etcd.nix
   ./services/misc/felix.nix
   ./services/misc/folding-at-home.nix
+  ./services/misc/gitlab.nix
   ./services/misc/gitolite.nix
   ./services/misc/gpsd.nix
   ./services/misc/mesos-master.nix
diff --git a/nixos/modules/profiles/container.nix b/nixos/modules/profiles/container.nix
index 5b531e5c3df..dd2e6579a93 100644
--- a/nixos/modules/profiles/container.nix
+++ b/nixos/modules/profiles/container.nix
@@ -20,17 +20,18 @@ in {
     contents = [];
     extraArgs = "--owner=0";
 
-    # Some container managers like lxc need these
-    extraCommands = "mkdir -p proc sys dev";
-
     # Add init script to image
     storeContents = [
       { object = config.system.build.toplevel + "/init";
         symlink = "/init";
       }
     ] ++ (pkgs2storeContents [ pkgs.stdenv ]);
+
+    # Some container managers like lxc need these
+    extraCommands = "mkdir -p proc sys dev";
   };
 
+  boot.isContainer = true;
   boot.postBootCommands =
     ''
       # After booting, register the contents of the Nix store in the Nix
@@ -40,18 +41,16 @@ in {
         rm /nix-path-registration
       fi
 
-      # nixos-rebuild also requires a "system" profile and an
-      # /etc/NIXOS tag.
-      touch /etc/NIXOS
+      # nixos-rebuild also requires a "system" profile
       ${config.nix.package}/bin/nix-env -p /nix/var/nix/profiles/system --set /run/current-system
     '';
 
-  boot.isContainer = true;
-
   # Disable some features that are not useful in a container.
   sound.enable = mkDefault false;
   services.udisks2.enable = mkDefault false;
 
-  # Shut up warnings about not having a boot loader.
-  system.build.installBootLoader = "${pkgs.coreutils}/bin/true";
+  # Install new init script
+  system.activationScripts.installInitScript = ''
+    ln -fs $systemConfig/init /init
+  '';
 }
diff --git a/nixos/modules/programs/virtualbox-host.nix b/nixos/modules/programs/virtualbox-host.nix
index ea962d5d6ce..603b25e3cfc 100644
--- a/nixos/modules/programs/virtualbox-host.nix
+++ b/nixos/modules/programs/virtualbox-host.nix
@@ -9,9 +9,14 @@ in
 {
   options = {
     services.virtualboxHost.enable = mkEnableOption "VirtualBox Host support";
+    services.virtualboxHost.addNetworkInterface = mkOption {
+      type = types.bool;
+      default = true;
+      description = "Automatically set up a vboxnet0 host-only network interface.";
+    };
   };
 
-  config = mkIf config.services.virtualboxHost.enable {
+  config = mkIf config.services.virtualboxHost.enable (mkMerge [{
     boot.kernelModules = [ "vboxdrv" "vboxnetadp" "vboxnetflt" ];
     boot.extraModulePackages = [ virtualbox ];
     environment.systemPackages = [ virtualbox ];
@@ -24,10 +29,7 @@ in
         setuid = true;
       };
     in map mkVboxStub [
-      "VBoxBFE"
-      "VBoxBalloonCtrl"
       "VBoxHeadless"
-      "VBoxManage"
       "VBoxSDL"
       "VirtualBox"
     ];
@@ -46,7 +48,7 @@ in
       '';
 
     # Since we lack the right setuid binaries, set up a host-only network by default.
-
+  } (mkIf config.services.virtualboxHost.addNetworkInterface {
     systemd.services."vboxnet0" =
       { description = "VirtualBox vboxnet0 Interface";
         requires = [ "dev-vboxnetctl.device" ];
@@ -68,5 +70,5 @@ in
       };
 
     networking.interfaces.vboxnet0.ip4 = [ { address = "192.168.56.1"; prefixLength = 24; } ];
-  };
+  })]);
 }
diff --git a/nixos/modules/services/desktops/profile-sync-daemon.nix b/nixos/modules/services/desktops/profile-sync-daemon.nix
new file mode 100644
index 00000000000..c662c5d0fa6
--- /dev/null
+++ b/nixos/modules/services/desktops/profile-sync-daemon.nix
@@ -0,0 +1,139 @@
+{ config, pkgs, lib, ... }:
+
+with lib;
+
+let
+  cfg = config.services.psd;
+
+  configFile = ''
+    ${if cfg.users != [ ] then ''
+      USERS="${concatStringsSep " " cfg.users}"
+    '' else ""}
+
+    ${if cfg.browsers != [ ] then ''
+      BROWSERS="${concatStringsSep " " cfg.browsers}"
+    '' else ""}
+
+    ${optionalString (cfg.volatile != "") "VOLATILE=${cfg.volatile}"}
+    ${optionalString (cfg.daemonFile != "") "DAEMON_FILE=${cfg.daemonFile}"}
+  '';
+in {
+  options.services.psd = with types; {
+    enable = mkOption {
+      type = bool;
+      default = false;
+      description = ''
+        Whether to enable the Profile Sync daemon.
+      '';
+    };
+
+    users = mkOption {
+      type = listOf str;
+      default = [ ];
+      example = [ "demo" ];
+      description = ''
+        A list of users whose browser profiles should be sync'd to tmpfs.
+      '';
+    };
+
+    browsers = mkOption {
+      type = listOf str;
+      default = [ ];
+      example = [ "chromium" "firefox" ];
+      description = ''
+        A list of browsers to sync. Available choices are:
+
+        chromium chromium-dev conkeror.mozdev.org epiphany firefox
+        firefox-trunk google-chrome google-chrome-beta google-chrome-unstable
+        heftig-aurora icecat luakit midori opera opera-developer opera-beta
+        qupzilla palemoon rekonq seamonkey
+
+        An empty list will enable all browsers.
+      '';
+    };
+
+    resyncTimer = mkOption {
+      type = str;
+      default = "1h";
+      example = "1h 30min";
+      description = ''
+        The amount of time to wait before syncing browser profiles back to the
+        disk.
+
+        Takes a systemd.unit time span. The time unit defaults to seconds if
+        omitted.
+      '';
+    };
+
+    volatile = mkOption {
+      type = str;
+      default = "/run/psd-profiles";
+      description = ''
+        The directory where browser profiles should reside(this should be
+        mounted as a tmpfs). Do not include a trailing backslash.
+      '';
+    };
+
+    daemonFile = mkOption {
+      type = str;
+      default = "/run/psd";
+      description = ''
+        Where the pid and backup configuration files will be stored.
+      '';
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    systemd = {
+      services = {
+        psd = {
+          description = "Profile Sync daemon";
+          wants = [ "psd-resync.service" "local-fs.target" ];
+          wantedBy = [ "multi-user.target" ];
+          preStart = "mkdir -p ${cfg.volatile}";
+
+          path = with pkgs; [ glibc rsync gawk ];
+
+          unitConfig = {
+            RequiresMountsFor = [ "/home/" ];
+          };
+
+          serviceConfig = {
+            Type = "oneshot";
+            RemainAfterExit = "yes";
+            ExecStart = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon sync";
+
+            ExecStop = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon unsync";
+          };
+        };
+
+        psd-resync = {
+          description = "Timed profile resync";
+          after = [ "psd.service" ];
+          wants = [ "psd-resync.timer" ];
+          partOf = [ "psd.service" ];
+
+          path = with pkgs; [ glibc rsync gawk ];
+
+          serviceConfig = {
+            Type = "oneshot";
+            ExecStart = "${pkgs.profile-sync-daemon}/bin/profile-sync-daemon resync";
+          };
+        };
+      };
+
+      timers.psd-resync = {
+        description = "Timer for profile sync daemon - 1 Hour";
+        partOf = [ "psd-resync.service" "psd.service" ];
+
+        timerConfig = {
+          OnUnitActiveSec = "${cfg.resyncTimer}";
+        };
+      };
+    };
+
+    environment.etc."psd.conf".text = configFile;
+
+  };
+}
diff --git a/nixos/modules/services/misc/defaultUnicornConfig.rb b/nixos/modules/services/misc/defaultUnicornConfig.rb
new file mode 100644
index 00000000000..81abaf336dc
--- /dev/null
+++ b/nixos/modules/services/misc/defaultUnicornConfig.rb
@@ -0,0 +1,206 @@
+# The following was taken from github.com/crohr/syslogger and is BSD
+# licensed.
+require 'syslog'
+require 'logger'
+require 'thread'
+
+class Syslogger
+
+  VERSION = "1.6.0"
+
+  attr_reader :level, :ident, :options, :facility, :max_octets
+  attr_accessor :formatter
+
+  MAPPING = {
+    Logger::DEBUG => Syslog::LOG_DEBUG,
+    Logger::INFO => Syslog::LOG_INFO,
+    Logger::WARN => Syslog::LOG_WARNING,
+    Logger::ERROR => Syslog::LOG_ERR,
+    Logger::FATAL => Syslog::LOG_CRIT,
+    Logger::UNKNOWN => Syslog::LOG_ALERT
+  }
+
+  #
+  # Initializes default options for the logger
+  # <tt>ident</tt>:: the name of your program [default=$0].
+  # <tt>options</tt>::  syslog options [default=<tt>Syslog::LOG_PID | Syslog::LOG_CONS</tt>].
+  #                     Correct values are:
+  #                       LOG_CONS    : writes the message on the console if an error occurs when sending the message;
+  #                       LOG_NDELAY  : no delay before sending the message;
+  #                       LOG_PERROR  : messages will also be written on STDERR;
+  #                       LOG_PID     : adds the process number to the message (just after the program name)
+  # <tt>facility</tt>:: the syslog facility [default=nil] Correct values include:
+  #                       Syslog::LOG_DAEMON
+  #                       Syslog::LOG_USER
+  #                       Syslog::LOG_SYSLOG
+  #                       Syslog::LOG_LOCAL2
+  #                       Syslog::LOG_NEWS
+  #                       etc.
+  #
+  # Usage:
+  #   logger = Syslogger.new("my_app", Syslog::LOG_PID | Syslog::LOG_CONS, Syslog::LOG_LOCAL0)
+  #   logger.level = Logger::INFO # use Logger levels
+  #   logger.warn "warning message"
+  #   logger.debug "debug message"
+  #
+  def initialize(ident = $0, options = Syslog::LOG_PID | Syslog::LOG_CONS, facility = nil)
+    @ident = ident
+    @options = options || (Syslog::LOG_PID | Syslog::LOG_CONS)
+    @facility = facility
+    @level = Logger::INFO
+    @mutex = Mutex.new
+    @formatter = Logger::Formatter.new
+  end
+
+  %w{debug info warn error fatal unknown}.each do |logger_method|
+    # Accepting *args as message could be nil.
+    #  Default params not supported in ruby 1.8.7
+    define_method logger_method.to_sym do |*args, &block|
+      return true if @level > Logger.const_get(logger_method.upcase)
+      message = args.first || block && block.call
+      add(Logger.const_get(logger_method.upcase), message)
+    end
+
+    unless logger_method == 'unknown'
+      define_method "#{logger_method}?".to_sym do
+        @level <= Logger.const_get(logger_method.upcase)
+      end
+    end
+  end
+
+  # Log a message at the Logger::INFO level. Useful for use with Rack::CommonLogger
+  def write(msg)
+    add(Logger::INFO, msg)
+  end
+
+  # Logs a message at the Logger::INFO level.
+  def <<(msg)
+    add(Logger::INFO, msg)
+  end
+
+  # Low level method to add a message.
+  # +severity+::  the level of the message. One of Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL, Logger::UNKNOWN
+  # +message+:: the message string.
+  #             If nil, the method will call the block and use the result as the message string.
+  #             If both are nil or no block is given, it will use the progname as per the behaviour of both the standard Ruby logger, and the Rails BufferedLogger.
+  # +progname+:: optionally, overwrite the program name that appears in the log message.
+  def add(severity, message = nil, progname = nil, &block)
+    if message.nil? && block.nil? && !progname.nil?
+      message, progname = progname, nil
+    end
+    progname ||= @ident
+
+    @mutex.synchronize do
+      Syslog.open(progname, @options, @facility) do |s|
+        s.mask = Syslog::LOG_UPTO(MAPPING[@level])
+        communication = clean(message || block && block.call)
+        if self.max_octets
+          buffer = "#{tags_text}"
+          communication.bytes do |byte|
+            buffer.concat(byte)
+            # if the last byte we added is potentially part of an escape, we'll go ahead and add another byte
+            if buffer.bytesize >= self.max_octets && !['%'.ord,'\\'.ord].include?(byte)
+              s.log(MAPPING[severity],buffer)
+              buffer = ""
+            end
+          end
+          s.log(MAPPING[severity],buffer) unless buffer.empty?
+        else
+          s.log(MAPPING[severity],"#{tags_text}#{communication}")
+        end
+      end
+    end
+  end
+
+  # Set the max octets of the messages written to the log
+  def max_octets=(max_octets)
+    @max_octets = max_octets
+  end
+
+  # Sets the minimum level for messages to be written in the log.
+  # +level+:: one of <tt>Logger::DEBUG</tt>, <tt>Logger::INFO</tt>, <tt>Logger::WARN</tt>, <tt>Logger::ERROR</tt>, <tt>Logger::FATAL</tt>, <tt>Logger::UNKNOWN</tt>
+  def level=(level)
+    level = Logger.const_get(level.to_s.upcase) if level.is_a?(Symbol)
+
+    unless level.is_a?(Fixnum)
+      raise ArgumentError.new("Invalid logger level `#{level.inspect}`")
+    end
+
+    @level = level
+  end
+
+  # Sets the ident string passed along to Syslog
+  def ident=(ident)
+    @ident = ident
+  end
+
+  # Tagging code borrowed from ActiveSupport gem
+  def tagged(*tags)
+    new_tags = push_tags(*tags)
+    yield self
+  ensure
+    pop_tags(new_tags.size)
+  end
+
+  def push_tags(*tags)
+    tags.flatten.reject{ |i| i.respond_to?(:empty?) ? i.empty? : !i }.tap do |new_tags|
+      current_tags.concat new_tags
+    end
+  end
+
+  def pop_tags(size = 1)
+    current_tags.pop size
+  end
+
+  def clear_tags!
+    current_tags.clear
+  end
+
+  protected
+
+  # Borrowed from SyslogLogger.
+  def clean(message)
+    message = message.to_s.dup
+    message.strip! # remove whitespace
+    message.gsub!(/\n/, '\\n') # escape newlines
+    message.gsub!(/%/, '%%') # syslog(3) freaks on % (printf)
+    message.gsub!(/\e\[[^m]*m/, '') # remove useless ansi color codes
+    message
+  end
+
+  private
+
+  def tags_text
+    tags = current_tags
+    if tags.any?
+      tags.collect { |tag| "[#{tag}] " }.join
+    end
+  end
+
+  def current_tags
+    Thread.current[:syslogger_tagged_logging_tags] ||= []
+  end
+end
+
+worker_processes 2
+working_directory ENV["GITLAB_PATH"]
+pid ENV["UNICORN_PATH"] + "/tmp/pids/unicorn.pid"
+
+listen ENV["UNICORN_PATH"] + "/tmp/sockets/gitlab.socket", :backlog => 1024
+listen "127.0.0.1:8080", :tcp_nopush => true
+
+timeout 60
+
+logger Syslogger.new
+
+preload_app true
+
+GC.respond_to?(:copy_on_write_friendly=) and
+  GC.copy_on_write_friendly = true
+
+check_client_connection false
+
+after_fork do |server, worker|
+  defined?(ActiveRecord::Base) and
+    ActiveRecord::Base.establish_connection
+end
diff --git a/nixos/modules/services/misc/gitlab.nix b/nixos/modules/services/misc/gitlab.nix
new file mode 100644
index 00000000000..efa139c8dfd
--- /dev/null
+++ b/nixos/modules/services/misc/gitlab.nix
@@ -0,0 +1,295 @@
+{ config, lib, pkgs, ... }:
+
+# TODO: support non-postgresql
+
+with lib;
+
+let
+  cfg = config.services.gitlab;
+
+  ruby = pkgs.ruby;
+  rubyLibs = pkgs.rubyLibs;
+
+  databaseYml = ''
+    production:
+      adapter: postgresql
+      database: ${cfg.databaseName}
+      host: ${cfg.databaseHost}
+      password: ${cfg.databasePassword}
+      username: ${cfg.databaseUsername}
+      encoding: utf8
+  '';
+  gitlabShellYml = ''
+    user: gitlab
+    gitlab_url: "http://${cfg.host}:${toString cfg.port}/"
+    http_settings:
+      self_signed_cert: false
+    repos_path: "${cfg.stateDir}/repositories"
+    log_file: "${cfg.stateDir}/log/gitlab-shell.log"
+    redis:
+      bin: ${pkgs.redis}/bin/redis-cli
+      host: 127.0.0.1
+      port: 6379
+      database: 0
+      namespace: resque:gitlab
+  '';
+
+  unicornConfig = builtins.readFile ./defaultUnicornConfig.rb;
+
+  gitlab-runner = pkgs.stdenv.mkDerivation rec {
+    name = "gitlab-runner";
+    buildInputs = [ pkgs.gitlab pkgs.rubyLibs.bundler pkgs.makeWrapper ];
+    phases = "installPhase fixupPhase";
+    buildPhase = "";
+    installPhase = ''
+      mkdir -p $out/bin
+      makeWrapper ${rubyLibs.bundler}/bin/bundle $out/bin/gitlab-runner\
+          --set RAKEOPT '"-f ${pkgs.gitlab}/share/gitlab/Rakefile"'\
+          --set UNICORN_PATH "${cfg.stateDir}/"\
+          --set GITLAB_PATH "${pkgs.gitlab}/share/gitlab/"\
+          --set GITLAB_APPLICATION_LOG_PATH "${cfg.stateDir}/log/application.log"\
+          --set GITLAB_SATELLITES_PATH "${cfg.stateDir}/satellites"\
+          --set GITLAB_SHELL_PATH "${pkgs.gitlab-shell}"\
+          --set GITLAB_REPOSITORIES_PATH "${cfg.stateDir}/repositories"\
+          --set GITLAB_SHELL_HOOKS_PATH "${cfg.stateDir}/shell/hooks"\
+          --set BUNDLE_GEMFILE "${pkgs.gitlab}/share/gitlab/Gemfile"\
+          --set GITLAB_EMAIL_FROM "${cfg.emailFrom}"\
+          --set GITLAB_SHELL_CONFIG_PATH "${cfg.stateDir}/shell/config.yml"\
+          --set GITLAB_SHELL_SECRET_PATH "${cfg.stateDir}/config/gitlab_shell_secret"\
+          --set GITLAB_HOST "${cfg.host}"\
+          --set GITLAB_PORT "${toString cfg.port}"\
+          --set GITLAB_BACKUP_PATH"${cfg.backupPath}"\
+          --set RAILS_ENV "production"
+    '';
+  };
+
+in {
+
+  options = {
+    services.gitlab = {
+      enable = mkOption {
+        type = types.bool;
+        default = false;
+        description = ''
+          Enable the gitlab service.
+        '';
+      };
+
+      satelliteDir = mkOption {
+        type = types.str;
+        default = "/var/gitlab/git-satellites";
+        description = "Gitlab directory to store checked out git trees requires for operation.";
+      };
+
+      stateDir = mkOption {
+        type = types.str;
+        default = "/var/gitlab/state";
+        description = "Gitlab state directory, logs are stored here.";
+      };
+
+      backupPath = mkOption {
+        type = types.str;
+        default = cfg.stateDir + "/backup";
+        description = "Gitlab path for backups.";
+      };
+
+      databaseHost = mkOption {
+        type = types.str;
+        default = "127.0.0.1";
+        description = "Gitlab database hostname.";
+      };
+
+      databasePassword = mkOption {
+        type = types.str;
+        default = "";
+        description = "Gitlab database user password.";
+      };
+
+      databaseName = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "Gitlab database name.";
+      };
+
+      databaseUsername = mkOption {
+        type = types.str;
+        default = "gitlab";
+        description = "Gitlab database user.";
+      };
+
+      emailFrom = mkOption {
+        type = types.str;
+        default = "example@example.org";
+        description = "The source address for emails sent by gitlab.";
+      };
+
+      host = mkOption {
+        type = types.str;
+        default = config.networking.hostName;
+        description = "Gitlab host name. Used e.g. for copy-paste URLs.";
+      };
+
+      port = mkOption {
+        type = types.int;
+        default = 8080;
+        description = "Gitlab server listening port.";
+      };
+    };
+  };
+
+  config = mkIf cfg.enable {
+
+    environment.systemPackages = [ gitlab-runner pkgs.gitlab-shell ];
+
+    assertions = [
+      { assertion = cfg.databasePassword != "";
+        message = "databasePassword must be set";
+      }
+    ];
+
+    # Redis is required for the sidekiq queue runner.
+    services.redis.enable = mkDefault true;
+    # We use postgres as the main data store.
+    services.postgresql.enable = mkDefault true;
+    services.postgresql.package = mkDefault pkgs.postgresql;
+    # Use postfix to send out mails.
+    services.postfix.enable = mkDefault true;
+
+    users.extraUsers = [
+      { name = "gitlab";
+        group = "gitlab";
+        home = "${cfg.stateDir}/home";
+        shell = "${pkgs.bash}/bin/bash";
+        uid = config.ids.uids.gitlab;
+      } ];
+
+    users.extraGroups = [
+      { name = "gitlab";
+        gid = config.ids.gids.gitlab;
+      } ];
+
+    systemd.services.gitlab-sidekiq = {
+      after = [ "network.target" "redis.service" ];
+      wantedBy = [ "multi-user.target" ];
+      environment.HOME = "${cfg.stateDir}/home";
+      environment.UNICORN_PATH = "${cfg.stateDir}/";
+      environment.GITLAB_PATH = "${pkgs.gitlab}/share/gitlab/";
+      environment.GITLAB_APPLICATION_LOG_PATH = "${cfg.stateDir}/log/application.log";
+      environment.GITLAB_SATELLITES_PATH = "${cfg.stateDir}/satellites";
+      environment.GITLAB_SHELL_PATH = "${pkgs.gitlab-shell}";
+      environment.GITLAB_REPOSITORIES_PATH = "${cfg.stateDir}/repositories";
+      environment.GITLAB_SHELL_HOOKS_PATH = "${cfg.stateDir}/shell/hooks";
+      environment.BUNDLE_GEMFILE = "${pkgs.gitlab}/share/gitlab/Gemfile";
+      environment.GITLAB_EMAIL_FROM = "${cfg.emailFrom}";
+      environment.GITLAB_SHELL_CONFIG_PATH = "${cfg.stateDir}/shell/config.yml";
+      environment.GITLAB_SHELL_SECRET_PATH = "${cfg.stateDir}/config/gitlab_shell_secret";
+      environment.GITLAB_HOST = "${cfg.host}";
+      environment.GITLAB_PORT = "${toString cfg.port}";
+      environment.GITLAB_DATABASE_HOST = "${cfg.databaseHost}";
+      environment.GITLAB_DATABASE_PASSWORD = "${cfg.databasePassword}";
+      environment.RAILS_ENV = "production";
+      path = with pkgs; [
+        config.services.postgresql.package
+        gitAndTools.git
+        ruby
+        openssh
+        nodejs
+      ];
+      serviceConfig = {
+        Type = "simple";
+        User = "gitlab";
+        Group = "gitlab";
+        TimeoutSec = "300";
+        WorkingDirectory = "${pkgs.gitlab}/share/gitlab";
+        ExecStart="${rubyLibs.bundler}/bin/bundle exec \"sidekiq -q post_receive -q mailer -q system_hook -q project_web_hook -q gitlab_shell -q common -q default -e production -P ${cfg.stateDir}/tmp/sidekiq.pid\"";
+      };
+    };
+
+    systemd.services.gitlab = {
+      after = [ "network.target" "postgresql.service" "redis.service" ];
+      wantedBy = [ "multi-user.target" ];
+      environment.HOME = "${cfg.stateDir}/home";
+      environment.UNICORN_PATH = "${cfg.stateDir}/";
+      environment.GITLAB_PATH = "${pkgs.gitlab}/share/gitlab/";
+      environment.GITLAB_APPLICATION_LOG_PATH = "${cfg.stateDir}/log/application.log";
+      environment.GITLAB_SATELLITES_PATH = "${cfg.stateDir}/satellites";
+      environment.GITLAB_SHELL_PATH = "${pkgs.gitlab-shell}";
+      environment.GITLAB_REPOSITORIES_PATH = "${cfg.stateDir}/repositories";
+      environment.GITLAB_SHELL_HOOKS_PATH = "${cfg.stateDir}/shell/hooks";
+      environment.BUNDLE_GEMFILE = "${pkgs.gitlab}/share/gitlab/Gemfile";
+      environment.GITLAB_EMAIL_FROM = "${cfg.emailFrom}";
+      environment.GITLAB_HOST = "${cfg.host}";
+      environment.GITLAB_PORT = "${toString cfg.port}";
+      environment.GITLAB_DATABASE_HOST = "${cfg.databaseHost}";
+      environment.GITLAB_DATABASE_PASSWORD = "${cfg.databasePassword}";
+      environment.RAILS_ENV = "production";
+      path = with pkgs; [
+        config.services.postgresql.package
+        gitAndTools.git
+        ruby
+        openssh
+        nodejs
+      ];
+      preStart = ''
+        # TODO: use env vars
+        mkdir -p ${cfg.stateDir}
+        mkdir -p ${cfg.stateDir}/log
+        mkdir -p ${cfg.stateDir}/satellites
+        mkdir -p ${cfg.stateDir}/repositories
+        mkdir -p ${cfg.stateDir}/shell/hooks
+        mkdir -p ${cfg.stateDir}/tmp/pids
+        mkdir -p ${cfg.stateDir}/tmp/sockets
+        rm -rf ${cfg.stateDir}/config
+        mkdir -p ${cfg.stateDir}/config
+        # TODO: What exactly is gitlab-shell doing with the secret?
+        head -c 20 /dev/urandom > ${cfg.stateDir}/config/gitlab_shell_secret
+        mkdir -p ${cfg.stateDir}/home/.ssh
+        touch ${cfg.stateDir}/home/.ssh/authorized_keys
+
+        cp -rf ${pkgs.gitlab}/share/gitlab/config ${cfg.stateDir}/
+        cp ${pkgs.gitlab}/share/gitlab/VERSION ${cfg.stateDir}/VERSION
+
+        ln -fs ${pkgs.writeText "database.yml" databaseYml} ${cfg.stateDir}/config/database.yml
+        ln -fs ${pkgs.writeText "unicorn.rb" unicornConfig} ${cfg.stateDir}/config/unicorn.rb
+
+        chown -R gitlab:gitlab ${cfg.stateDir}/
+        chmod -R 755 ${cfg.stateDir}/
+
+        if [ "${cfg.databaseHost}" = "127.0.0.1" ]; then
+          if ! test -e "${cfg.stateDir}/db-created"; then
+            psql postgres -c "CREATE ROLE gitlab WITH LOGIN NOCREATEDB NOCREATEROLE NOCREATEUSER ENCRYPTED PASSWORD '${cfg.databasePassword}'"
+            ${config.services.postgresql.package}/bin/createdb --owner gitlab gitlab || true
+            touch "${cfg.stateDir}/db-created"
+
+            # force=yes disables the manual-interaction yes/no prompt
+            # which breaks without an stdin.
+            force=yes ${rubyLibs.bundler}/bin/bundle exec rake -f ${pkgs.gitlab}/share/gitlab/Rakefile gitlab:setup RAILS_ENV=production
+          fi
+        fi
+
+      # Install the shell required to push repositories
+      ln -fs ${pkgs.writeText "config.yml" gitlabShellYml} ${cfg.stateDir}/shell/config.yml
+      export GITLAB_SHELL_CONFIG_PATH=""${cfg.stateDir}/shell/config.yml
+      ${pkgs.gitlab-shell}/bin/install
+
+      # Change permissions in the last step because some of the
+      # intermediary scripts like to create directories as root.
+      chown -R gitlab:gitlab ${cfg.stateDir}/
+      chmod -R 755 ${cfg.stateDir}/
+      '';
+
+      serviceConfig = {
+        PermissionsStartOnly = true; # preStart must be run as root
+        Type = "simple";
+        User = "gitlab";
+        Group = "gitlab";
+        TimeoutSec = "300";
+        WorkingDirectory = "${pkgs.gitlab}/share/gitlab";
+        ExecStart="${rubyLibs.bundler}/bin/bundle exec \"unicorn -c ${cfg.stateDir}/config/unicorn.rb -E production\"";
+      };
+
+    };
+
+  };
+
+}
diff --git a/nixos/modules/services/networking/networkmanager.nix b/nixos/modules/services/networking/networkmanager.nix
index 37bc1df2361..55a93dfe64c 100644
--- a/nixos/modules/services/networking/networkmanager.nix
+++ b/nixos/modules/services/networking/networkmanager.nix
@@ -194,7 +194,7 @@ in {
     };
 
     powerManagement.resumeCommands = ''
-      Systemctl restart network-manager
+      systemctl restart network-manager
     '';
 
     security.polkit.extraConfig = polkitConf;
diff --git a/nixos/modules/system/boot/stage-1.nix b/nixos/modules/system/boot/stage-1.nix
index cf211404649..cd30ce1b7ce 100644
--- a/nixos/modules/system/boot/stage-1.nix
+++ b/nixos/modules/system/boot/stage-1.nix
@@ -240,8 +240,9 @@ in
       example = "/dev/sda3";
       description = ''
         Device for manual resume attempt during boot. This should be used primarily
-        if you want to resume from file. Specify here the device where the file
-        resides. You should also use <varname>boot.kernelParams</varname> to specify
+        if you want to resume from file. If left empty, the swap partitions are used.
+        Specify here the device where the file resides.
+        You should also use <varname>boot.kernelParams</varname> to specify
         <literal><replaceable>resume_offset</replaceable></literal>.
       '';
     };
@@ -355,10 +356,17 @@ in
 
   config = mkIf (!config.boot.isContainer) {
 
-    assertions = singleton
+    assertions = [
       { assertion = any (fs: fs.mountPoint == "/") (attrValues config.fileSystems);
         message = "The ‘fileSystems’ option does not specify your root file system.";
-      };
+      }
+      { assertion = let inherit (config.boot) resumeDevice; in
+          resumeDevice == "" || builtins.substring 0 1 resumeDevice == "/";
+        message = "boot.resumeDevice has to be an absolute path."
+          + " Old \"x:y\" style is no longer supported.";
+      }
+    ];
+
 
     system.build.bootStage1 = bootStage1;
     system.build.initialRamdisk = initialRamdisk;
diff --git a/nixos/release-combined.nix b/nixos/release-combined.nix
index e850c1f6cdd..f2f68a11a28 100644
--- a/nixos/release-combined.nix
+++ b/nixos/release-combined.nix
@@ -48,6 +48,7 @@ in rec {
         (all nixos.ova)
 
         #(all nixos.tests.containers)
+        (all nixos.tests.chromium)
         (all nixos.tests.firefox)
         (all nixos.tests.firewall)
         (all nixos.tests.gnome3)
diff --git a/nixos/release.nix b/nixos/release.nix
index efc49adce46..04b8fd9bf67 100644
--- a/nixos/release.nix
+++ b/nixos/release.nix
@@ -214,7 +214,7 @@ in rec {
   });
 
   # Provide container tarball for lxc, libvirt-lxc, docker-lxc, ...
-  container_tarball = forAllSystems (system: makeSystemTarball {
+  containerTarball = forAllSystems (system: makeSystemTarball {
     module = ./modules/virtualisation/lxc-container.nix;
     inherit system;
   });
@@ -251,6 +251,7 @@ in rec {
   tests.firefox = callTest tests/firefox.nix {};
   tests.firewall = callTest tests/firewall.nix {};
   tests.fleet = scrubDrv (import tests/fleet.nix { system = "x86_64-linux"; });
+  tests.gitlab = callTest tests/gitlab.nix {};
   tests.gnome3 = callTest tests/gnome3.nix {};
   tests.installer.grub1 = forAllSystems (system: scrubDrv (import tests/installer.nix { inherit system; }).grub1.test);
   tests.installer.lvm = forAllSystems (system: scrubDrv (import tests/installer.nix { inherit system; }).lvm.test);
@@ -306,6 +307,7 @@ in rec {
   tests.simple = callTest tests/simple.nix {};
   tests.tomcat = callTest tests/tomcat.nix {};
   tests.udisks2 = callTest tests/udisks2.nix {};
+  tests.virtualbox = callTest tests/virtualbox.nix {};
   tests.xfce = callTest tests/xfce.nix {};
 
 
diff --git a/nixos/tests/gitlab.nix b/nixos/tests/gitlab.nix
new file mode 100644
index 00000000000..2b0715e97bc
--- /dev/null
+++ b/nixos/tests/gitlab.nix
@@ -0,0 +1,20 @@
+# This test runs gitlab and checks if it works
+
+import ./make-test.nix {
+  name = "gitlab";
+
+  nodes = {
+    gitlab = { config, pkgs, ... }: {
+      virtualisation.memorySize = 768;
+      services.gitlab.enable = true;
+      services.gitlab.databasePassword = "gitlab";
+    };
+  };
+
+  testScript = ''
+    $gitlab->start();
+    $gitlab->waitForUnit("gitlab.service");
+    $gitlab->waitForUnit("gitlab-sidekiq.service");
+    $gitlab->waitUntilSucceeds("curl http://localhost:8080/users/sign_in");
+  '';
+}
diff --git a/nixos/tests/kubernetes.nix b/nixos/tests/kubernetes.nix
index 1ca6153bcb9..8bc7c8d618a 100644
--- a/nixos/tests/kubernetes.nix
+++ b/nixos/tests/kubernetes.nix
@@ -47,7 +47,7 @@ import ./make-test.nix rec {
     master =
       { config, pkgs, lib, nodes, ... }:
         {
-          virtualisation.memorySize = 512;
+          virtualisation.memorySize = 768;
           services.kubernetes = {
             roles = ["master" "node"];
             controllerManager.machines = ["master" "node"];
diff --git a/nixos/tests/quake3.nix b/nixos/tests/quake3.nix
index b16cb179982..e0e6d6eaadb 100644
--- a/nixos/tests/quake3.nix
+++ b/nixos/tests/quake3.nix
@@ -15,14 +15,14 @@ in
 rec {
   name = "quake3";
 
-  makeCoverageReport = true;
+  # TODO: lcov doesn't work atm
+  #makeCoverageReport = true;
 
   client =
     { config, pkgs, ... }:
 
     { imports = [ ./common/x11.nix ];
       hardware.opengl.driSupport = true;
-      services.xserver.defaultDepth = pkgs.lib.mkOverride 0 16;
       environment.systemPackages = [ pkgs.quake3demo ];
       nixpkgs.config.packageOverrides = overrides;
     };
diff --git a/nixos/tests/virtualbox.nix b/nixos/tests/virtualbox.nix
new file mode 100644
index 00000000000..c8912760f3d
--- /dev/null
+++ b/nixos/tests/virtualbox.nix
@@ -0,0 +1,250 @@
+import ./make-test.nix ({ pkgs, ... }: let
+
+  testVMConfig = { config, pkgs, ... }: {
+    boot.kernelParams = [
+      "console=tty0" "console=ttyS0" "ignore_loglevel"
+      "boot.trace" "panic=1" "boot.panic_on_fail"
+    ];
+
+    fileSystems."/" = {
+      device = "vboxshare";
+      fsType = "vboxsf";
+    };
+
+    services.virtualboxGuest.enable = true;
+
+    boot.initrd.kernelModules = [ "vboxsf" ];
+
+    boot.initrd.extraUtilsCommands = ''
+      cp -av -t "$out/bin/" \
+        "${pkgs.linuxPackages.virtualboxGuestAdditions}/sbin/mount.vboxsf"
+    '';
+
+    boot.initrd.postMountCommands = ''
+      touch /mnt-root/boot-done
+
+      i=0
+      while [ ! -e /mnt-root/shutdown ]; do
+        sleep 10
+        i=$(($i + 10))
+        [ $i -le 120 ] || fail
+      done
+
+      rm -f /mnt-root/boot-done /mnt-root/shutdown
+      poweroff -f
+    '';
+
+    system.requiredKernelConfig = with config.lib.kernelConfig; [
+      (isYes "SERIAL_8250_CONSOLE")
+      (isYes "SERIAL_8250")
+    ];
+  };
+
+  testVM = let
+    cfg = (import ../lib/eval-config.nix {
+      system = "i686-linux";
+      modules = [
+        ../modules/profiles/minimal.nix
+        testVMConfig
+      ];
+    }).config;
+  in pkgs.vmTools.runInLinuxVM (pkgs.runCommand "virtualbox-image" {
+    preVM = ''
+      mkdir -p "$out"
+      diskImage="$(pwd)/qimage"
+      ${pkgs.vmTools.qemu}/bin/qemu-img create -f raw "$diskImage" 100M
+    '';
+
+    postVM = ''
+      echo "creating VirtualBox disk image..."
+      ${pkgs.vmTools.qemu}/bin/qemu-img convert -f raw -O vdi \
+        "$diskImage" "$out/disk.vdi"
+    '';
+
+    buildInputs = [ pkgs.utillinux pkgs.perl ];
+  } ''
+    ${pkgs.parted}/sbin/parted /dev/vda mklabel msdos
+    ${pkgs.parted}/sbin/parted /dev/vda -- mkpart primary ext2 1M -1s
+    . /sys/class/block/vda1/uevent
+    mknod /dev/vda1 b $MAJOR $MINOR
+
+    ${pkgs.e2fsprogs}/sbin/mkfs.ext4 /dev/vda1
+    ${pkgs.e2fsprogs}/sbin/tune2fs -c 0 -i 0 /dev/vda1
+    mkdir /mnt
+    mount /dev/vda1 /mnt
+    cp "${cfg.system.build.kernel}/bzImage" /mnt/linux
+    cp "${cfg.system.build.initialRamdisk}/initrd" /mnt/initrd
+
+    ${pkgs.grub2}/bin/grub-install --boot-directory=/mnt /dev/vda
+
+    cat > /mnt/grub/grub.cfg <<GRUB
+    set root=hd0,1
+    linux /linux ${pkgs.lib.concatStringsSep " " cfg.boot.kernelParams}
+    initrd /initrd
+    boot
+    GRUB
+    umount /mnt
+  '');
+
+in {
+  name = "virtualbox";
+
+  machine = { pkgs, ... }: {
+    imports = [ ./common/user-account.nix ./common/x11.nix ];
+    services.virtualboxHost.enable = true;
+
+    systemd.sockets.vboxtestlog = {
+      description = "VirtualBox Test Machine Log Socket";
+      wantedBy = [ "sockets.target" ];
+      before = [ "multi-user.target" ];
+      socketConfig.ListenStream = "/run/virtualbox-log.sock";
+      socketConfig.Accept = true;
+    };
+
+    systemd.services."vboxtestlog@" = {
+      description = "VirtualBox Test Machine Log";
+      serviceConfig.StandardInput = "socket";
+      serviceConfig.StandardOutput = "syslog";
+      serviceConfig.SyslogIdentifier = "testvm";
+      serviceConfig.ExecStart = "${pkgs.coreutils}/bin/cat";
+    };
+  };
+
+  testScript = let
+    mkFlags = pkgs.lib.concatStringsSep " ";
+
+    createFlags = mkFlags [
+      "--ostype Linux26"
+      "--register"
+    ];
+
+    vmFlags = mkFlags [
+      "--uart1 0x3F8 4"
+      "--uartmode1 client /run/virtualbox-log.sock"
+    ];
+
+    controllerFlags = mkFlags [
+      "--name SATA"
+      "--add sata"
+      "--bootable on"
+      "--hostiocache on"
+    ];
+
+    diskFlags = mkFlags [
+      "--storagectl SATA"
+      "--port 0"
+      "--device 0"
+      "--type hdd"
+      "--mtype immutable"
+      "--medium ${testVM}/disk.vdi"
+    ];
+
+    sharedFlags = mkFlags [
+      "--name vboxshare"
+      "--hostpath /home/alice/vboxshare"
+    ];
+  in ''
+    sub ru {
+      return "su - alice -c '$_[0]'";
+    }
+
+    sub waitForVMBoot {
+      $machine->execute(ru(
+        'set -e; i=0; '.
+        'while ! test -e /home/alice/vboxshare/boot-done; do '.
+        'sleep 10; i=$(($i + 10)); [ $i -le 3600 ]; '.
+        'VBoxManage list runningvms | grep -qF test; '.
+        'done'
+      ));
+    }
+
+    sub checkRunning {
+      my $checkrunning = ru "VBoxManage list runningvms | grep -qF test";
+      my ($status, $out) = $machine->execute($checkrunning);
+      return $status == 0;
+    }
+
+    sub waitForStartup {
+      for (my $i = 0; $i <= 120; $i += 10) {
+        $machine->sleep(10);
+        return if checkRunning;
+      }
+      die "VirtualBox VM didn't start up within 2 minutes";
+    }
+
+    sub waitForShutdown {
+      for (my $i = 0; $i <= 120; $i += 10) {
+        $machine->sleep(10);
+        return unless checkRunning;
+      }
+      die "VirtualBox VM didn't shut down within 2 minutes";
+    }
+
+    sub shutdownVM {
+      $machine->succeed(ru "touch /home/alice/vboxshare/shutdown");
+      $machine->waitUntilSucceeds(
+        "test ! -e /home/alice/vboxshare/shutdown ".
+        "  -a ! -e /home/alice/vboxshare/boot-done"
+      );
+      waitForShutdown;
+    }
+
+    sub cleanup {
+      $machine->execute(ru "VBoxManage controlvm test poweroff")
+        if checkRunning;
+      $machine->succeed("rm -rf /home/alice/vboxshare");
+      $machine->succeed("mkdir -p /home/alice/vboxshare");
+      $machine->succeed("chown alice.users /home/alice/vboxshare");
+    }
+
+    $machine->waitForX;
+
+    $machine->succeed(ru "VBoxManage createvm --name test ${createFlags}");
+    $machine->succeed(ru "VBoxManage modifyvm test ${vmFlags}");
+
+    $machine->fail("test -e '/root/VirtualBox VMs'");
+    $machine->succeed("test -e '/home/alice/VirtualBox VMs'");
+
+    $machine->succeed(ru "VBoxManage storagectl test ${controllerFlags}");
+    $machine->succeed(ru "VBoxManage storageattach test ${diskFlags}");
+
+    $machine->succeed(ru "VBoxManage sharedfolder add test ${sharedFlags}");
+
+    $machine->succeed(ru "VBoxManage showvminfo test >&2");
+
+    cleanup;
+
+    subtest "virtualbox-gui", sub {
+      $machine->succeed(ru "VirtualBox &");
+      $machine->waitForWindow(qr/Oracle VM VirtualBox Manager/);
+      $machine->sleep(5);
+      $machine->screenshot("gui_manager_started");
+      $machine->sendKeys("ret");
+      $machine->screenshot("gui_manager_sent_startup");
+      waitForStartup;
+      $machine->screenshot("gui_started");
+      waitForVMBoot;
+      $machine->screenshot("gui_booted");
+      shutdownVM;
+      $machine->sleep(5);
+      $machine->screenshot("gui_stopped");
+      $machine->sendKeys("ctrl-q");
+      $machine->sleep(5);
+      $machine->screenshot("gui_manager_stopped");
+    };
+
+    cleanup;
+
+    subtest "virtualbox-cli", sub {
+      $machine->succeed(ru "VBoxManage startvm test");
+      waitForStartup;
+      $machine->screenshot("cli_started");
+      waitForVMBoot;
+      $machine->screenshot("cli_booted");
+      shutdownVM;
+    };
+
+    $machine->fail("test -e '/root/VirtualBox VMs'");
+    $machine->succeed("test -e '/home/alice/VirtualBox VMs'");
+  '';
+})