Skip to content

Instantly share code, notes, and snippets.

@FlakM
Last active November 6, 2024 14:20
Show Gist options
  • Save FlakM/0535b8aa7efec56906c5ab5e32580adf to your computer and use it in GitHub Desktop.
Save FlakM/0535b8aa7efec56906c5ab5e32580adf to your computer and use it in GitHub Desktop.

Setting up qemu VM using nix flakes

Did you know that it is rather easy to setup a VM to test your NixOs configuration?

Create simple flake:

# flake.nix
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";

  outputs = { self, nixpkgs, ... }:
    let
      system = "x86_64-linux";
    in
    {
      # test is a hostname for our machine
      nixosConfigurations.test = nixpkgs.lib.nixosSystem {
        inherit system;
        modules = [
          ./configuration.nix
        ];
      };
    };
}

Add your configuration

# configuration.nix
{ config, lib, pkgs, ... }: {
  # customize kernel version
  boot.kernelPackages = pkgs.linuxPackages_5_15;
  
  users.groups.admin = {};
  users.users = {
    admin = {
      isNormalUser = true;
      extraGroups = [ "wheel" ];
      password = "admin";
      group = "admin";
    };
  };

  virtualisation.vmVariant = {
    # following configuration is added only when building VM with build-vm
    virtualisation = {
      memorySize = 2048; # Use 2048MiB memory.
      cores = 3;
      graphics = false;
    };
  };

  services.openssh = {
    enable = true;
    settings.PasswordAuthentication = true;
  };

  networking.firewall.allowedTCPPorts = [ 22 ];
  environment.systemPackages = with pkgs; [
    htop
  ];

  system.stateVersion = "23.05";
}

Run a VM

git init # skip this step if you are inside already tracked repository
git add . # flakes requires at least tracking the files
nixos-rebuild build-vm --flake .#test
# expose port 22 from guest
export QEMU_NET_OPTS="hostfwd=tcp::2221-:22"
result/bin/run-nixos-vm

Profit!

# ssh onto the machine
ssh -oUserKnownHostsFile=/dev/null -oStrictHostKeyChecking=no admin@localhost -p 2221
@eljamm
Copy link

eljamm commented Oct 28, 2024

Just to be clear, do I need to create an app for each VM I want, right?

@sanzoghenzo Yup, but this can easily be simplified with a function if you'd like to avoid repetition. For example:

let
  # Create a VM app from a nixos config
  mkAppVM = name: {
    type = "app";
    program = "${self.nixosConfigurations.${name}.config.system.build.vm}/bin/run-nixos-vm";
  };
in
{
  apps = rec {
    default = vm-test;

    # VM tests
    vm-test = mkAppVM "test"; # start with `nix run .#apps.vm-test`
    vm-other = mkAppVM "other"; # start with `nix run .#apps.vm-other`
  };
};

Which can also be done for the system config:

let
  mkSystem =
    modules:
    nixpkgs.lib.nixosSystem {
      specialArgs.inputs = inputs;
      system = "x86_64-linux";
      modules = [
        ./hosts/common.nix
      ] ++ modules;
    };
in
{
  nixosConfigurations = {
    test = mkSystem [
      ./hosts/vm/test.nix
      ./modules/desktops/gnome
    ];

    other = mkSystem [
      ./hosts/vm/other.nix
      ./modules/desktops/kde
    ];
  };
};

Note: The rec keyword allows us to refer to vm-test from within the same attrset (see Nix language basics — nix.dev).

Also, you can include the QEMU_NET_OPTS env var into your dev shell:

let
  pkgs = nixpkgs.legacyPackages.${system};
  system = "x86_64-linux";
in
{
  devShells = {
    default = pkgs.mkShellNoCC {
      QEMU_NET_OPTS = "hostfwd=tcp::2221-:22";
    };
  };
}

Just like with apps, you can write more devShells according to your needs and activate them with nix develop .#devShells.<name>.

Alternatively, you can forward the port in the system config with:

virtualisation.forwardPorts = [
  {
    from = "host";
    host.port = 2221;
    guest.port = 22;
  }
];

Notes:

  • You can set up flake-utils so you can directly use nix run .#<name> and nix develop .#<name> instead of nix run .#apps.<name> and nix develop .#devShells.<name>
  • You can use direnv to auto-activate the devShell when you navigate to the directory.

Now, we can refactor things a bit and import mkAppVM and mkSystem from a separate file, which might make things cleaner:

utils.nix
{ inputs, system }:
{
  # Create a VM app from a nixos config
  mkAppVM = name: {
    type = "app";
    program = "${inputs.self.nixosConfigurations.${name}.config.system.build.vm}/bin/run-nixos-vm";
  };

  mkSystem =
    modules:
    inputs.nixpkgs.lib.nixosSystem {
      specialArgs.inputs = inputs;
      inherit system;
      modules = [
        ./hosts/common.nix
      ] ++ modules;
    };
}

Which leaves us with this final configuration:

flake.nix
{
  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  };

  outputs =
    { self, nixpkgs, ... }@inputs:
    (
      let
        system = "x86_64-linux";
        pkgs = nixpkgs.legacyPackages.${system};

        # utility functions
        inherit (import ./utils.nix { inherit inputs system; }) mkAppVM mkSystem;
      in
      {
        nixosConfigurations = {
          test = mkSystem [
            ./hosts/vm/test.nix
            ./modules/desktop/gnome
          ];

          other = mkSystem [
            ./hosts/vm/other.nix
            ./modules/desktop/kde
          ];
        };

        apps = rec {
          default = vm-test;

          # VM tests: `nix run .#apps.<name>`
          vm-test = mkAppVM "test";
          vm-other = mkAppVM "other";
        };

        devShells = {
          default = pkgs.mkShellNoCC {
            QEMU_NET_OPTS = "hostfwd=tcp::2221-:22";
          };
        };
      }
    );
}

I've resorted to task (which I was already using at work as a make alternative) to simplify my commands and avoid repetitions

Thanks for suggesting this as it looks like an interesting and flexible tool, but for this usecase it makes more sense to me to use apps because it integrates directly with Nix, thus leveraging its full capabilites. Also, after removing the boilerplate, the Nix code looks cleaner and more readable to me.

That said, I'm curious if task and similar tools would be more suitable for more complex cases.

@sanzoghenzo
Copy link

sanzoghenzo commented Oct 29, 2024

Thanks @eljamm for the exaustive explanation!

program = "${inputs.self.nixosConfigurations.${name}.config.system.build.vm}/bin/run-nixos-vm";
inherit (import ./utils.nix { inherit inputs system; }) mkAppVM mkSystem;

Not what I'd call "cleaner" and "more readable" if you ask me 😅
Of course this is me trying to leverage the wonders of NixOS (declarativeness, reproducibility) but being too lazy to learn yet another programming language 🤣

But seriously, thanks again, I'll make treasure of your lesson!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment