GithubHelp home page GithubHelp logo

morph's Introduction

morph

Build

Morph is a tool for managing existing NixOS hosts - basically a fancy wrapper around nix-build, nix copy, nix-env, /nix/store/.../bin/switch-to-configuration, scp and more. Morph supports updating multiple hosts in a row, and with support for health checks makes it fairly safe to do so.

Notable features

  • multi host support
  • health checks
  • no state

Installation and prerequisites

Morph requires nix (at least v2), ssh and scp to be available on $PATH. It should work on any modern Linux distribution, but NixOS is the only one we test on.

Pre-built binaries are not provided, since we install morph through an overlay.

The easiest way to get morph up and running is to fork this repository and run nix-build, which should result in a store path containing the morph binary. Consider checking out a specific tag, or at least pin the version of morph you're using somehow.

Using morph

All commands support a --help flag; morph --help as of v1.0.0:

$ morph --help
usage: morph [<flags>] <command> [<args> ...]

NixOS host manager

Flags:
  --help     Show context-sensitive help (also try --help-long and --help-man).
  --version  Show application version.
  --dry-run  Don't do anything, just eval and print changes

Commands:
  help [<command>...]
    Show help.

  build [<flags>] <deployment>
    Evaluate and build deployment configuration to the local Nix store

  push [<flags>] <deployment>
    Build and transfer items from the local Nix store to target machines

  deploy [<flags>] <deployment> <switch-action>
    Build, push and activate new configuration on machines according to switch-action

  check-health [<flags>] <deployment>
    Run health checks

  upload-secrets [<flags>] <deployment>
    Upload secrets

  exec [<flags>] <deployment> <command>...
    Execute arbitrary commands on machines

Notably, morph deploy requires a <switch-action>. The switch-action must be one of dry-activate, test, switch or boot corresponding to nixos-rebuild arguments of the same name. Refer to the NixOS manual for a detailed description of switch-actions.

For help on this and other commands, run morph <cmd> --help.

Example deployments can be found in the examples directory, and built as follows:

$ morph build examples/simple.nix
Selected 2/2 hosts (name filter:-0, limits:-0):
	  0: db01 (secrets: 0, health checks: 0)
	  1: web01 (secrets: 0, health checks: 0)

<probably lots of nix-build output>

/nix/store/grvny5ga2i6jdxjjbh2ipdz7h50swi1n-morph
nix result path:
/nix/store/grvny5ga2i6jdxjjbh2ipdz7h50swi1n-morph

The result path is written twice, which is a bit silly, but the reason is that only the result path is written to stdout, and everything else (including nix-build output) is redirected to stderr. This makes it easy to use morph for scripting, e.g. if one want to build using morph and then nix copy the result path somewhere else.

Note that examples/simple.nix contain two different hosts definitions, and a lot of copy paste. All the usual nix tricks can of course be used to avoid duplication.

Hosts can be deployed with the deploy command as follows: morph deploy examples/simple.nix (this will fail without modifying examples/simple.nix).

Selecting/filtering hosts to build and deploy

All hosts defined in a deployment file is returned to morph as a list of hosts, which can be manipulated with the following flags:

  • --on glob can be used to select hosts by name, with support for glob patterns
  • --limit n puts an upper limit on the number of hosts
  • --skip n ignore the first n hosts
  • --every n selects every n'th host, useful for e.g. selecting all even (or odd) numbered hosts

(all relevant commands should already support these flags.)

The ordering currently can't be changed, but should be deterministic because of nix.

Most commands output a header like this:

Selected 4/17 hosts (name filter:-6, limits:-7):
	  0: foo-p02 (secrets: 0, health checks: 1)
	  1: foo-p05 (secrets: 0, health checks: 1)
	  2: foo-p08 (secrets: 0, health checks: 1)
	  3: foo-p11 (secrets: 0, health checks: 1)

The output is pretty self explanatory, except probably for the last bit of the first line. name filter shows the change in number of hosts after glob matching on the hosts name, and limits shows the change after applying --limit, --skip and --every.

Tagging hosts

Each host can be tagged with an arbitrary amount of tags, which can be used to select and sort hosts.

To tag a host, use the deployment.tags option, e.g. deployment.tags = [ "prod" "master" "rack-17" ]. Hosts can now be selected with the --tagged option, e.g.--tagged="prod,master" will only select hosts tagged both prod and master.

To sort hosts based on tags, use the network.ordering.tags option, e.g. network.ordering.tags = [ "master" "slave"]. This ordering can be changed at runtime using the --order-by-tags option, eg. --order-by-tags="slave,master" (this also works when network.ordering.tags isn't defined). Hosts without matching tags will end up at the end of the list.

Environment Variables

Morph supports the following (optional) environment variables:

  • SSH_IDENTITY_FILE the (local) path to the SSH private key file that should be used
  • SSH_USER specifies the user that should be used to connect to the remote system
  • SSH_SKIP_HOST_KEY_CHECK if set disables host key verification
  • SSH_CONFIG_FILE allows to change the location of the ~/.ssh/config file
  • MORPH_NIX_EVAL_CMD morph will invoke this command instead of default: "nix-instantiate" on PATH
  • MORPH_NIX_BUILD_CMD morph will invoke this command instead of default: "nix-build" on PATH
  • MORPH_NIX_SHELL_CMD morph will invoke this command instead of default: "nix-shell" on PATH
  • MORPH_NIX_EVAL_MACHINES path to a custom eval-machines.nix. Defaults to the eval-machines.nix bundled with morph

Secrets

Files can be uploaded without ever ending up in the nix store, by specifying each file as a secret. This will use scp for copying a local file to the remote host.

See examples/secrets.nix or the type definitions in data/options.nix.

To upload secrets, use the morph upload-secrets subcommand, or pass --upload-secrets to morph deploy.

Note: Morph will automatically create directories parent to secret.Destination if they don't exist. New dirs will be owned by root:root and have mode 755 (drwxr-xr-x). Automatic directory creation can be disabled by setting secret.mkDirs = false.

Health checks

Morph has support for two types of health checks:

  • command based health checks, which are run on the target host (success defined as exit code == 0)
  • HTTP based health checks, which are run from the host Morph is running on (success defined as HTTP response codes in the 2xx range)

See examples/healthchecks.nix for an example.

There are no guarantees about the order health checks are run in, so if you need something complex you should write a script for it (e.g. using pkgs.writeScript). Health checks will be repeated until success, and the interval can be configured with the period option (see data/options.nix for details).

It is currently possible to have expressions like "test \"$(systemctl list-units --failed --no-legend --no-pager |wc -l)\" -eq 0" (count number of failed systemd units, fail if non-zero) as the first argument in a cmd-healthcheck. This works, but is discouraged, and might break at any time.

Advanced configuration

nix.conf-options: The "network"-attrset supports a sub-attrset named "nixConfig". Options configured here will pass --option <name> <value> to all nix commands. Note: these options apply to an entire deployment and are not configurable on per-host basis. The default is an empty set, meaning that the nix configuration is inherited from the build environment. See man nix.conf.

network.buildShell By passing --allow-build-shell and setting network.buildShell to a nix-shell compatible derivation (eg. pkgs.mkShell ...), it's possible to make morph execute builds from within the defined shell. This makes it possible to have arbitrary dependencies available during the build, say for use with nix build hooks. Be aware that the shell can potentially execute any command on the local system.

special deployment options:

(per-host granularity)

buildOnly makes morph skip the "push" and "switch" steps for the given host, even if "morph deploy" or "morph push" is executed. (default: false)

substituteOnDestination Sets the --substitute-on-destination flag on nix copy, allowing for the deployment target to use substitutes. See nix copy --help. (default: false)

Example usage of nixConfig and deployment module options:

network = {
    nixConfig = {
        "extra-sandbox-paths" = "/foo/bar";
    };
};

machine1 = { ... }: {
    deployment.buildOnly = true;
};

machine2 = { ... }: {
    deployment.substituteOnDestination = true;
};

mutually recursive configurations Each host's configuration has access to a nodes argument, which contains the compiled configurations of all hosts.

machine1 = { nodes, ... }: {
    hostnames.machine2 = 
        (builtins.head nodes.machine2.networking.interfaces.foo.ipv4.addresses).address;
    networking.interfaces.foo.ipv4.addresses = [
        {
            address = "10.0.0.10";
            prefixLength = 32;
        }
    ];
}

machine2 = { nodes, ... }: {
    hostnames.machine1 = 
        (builtins.head nodes.machine1.networking.interfaces.foo.ipv4.addresses).address;
    networking.interfaces.foo.ipv4.addresses = [
        {
            address = "10.0.0.20";
            prefixLength = 32;
        }
    ];
}

Hacking morph

All commands mentioned below is available in the nix-shell, if you run nix-shell with working dir = project root. The included shell.nix uses the latest nixos-unstable from GitHub by default, but you can override this by passing in another, eg. nix-shell --arg nixpkgs '<nixpkgs>' for your $NIX_PATH nixpkgs.

Go dependency management

From within nix-shell, go get -u updates all go modules. Remember to update the vendorSha256 in ./default.nix

Building the project with pinned dependencies

$ nix-build --arg nixpkgs "builtins.fetchTarball https://github.com/NixOS/nixpkgs/archive/<rev>.tar.gz"

About the project

We needed a tool for managing our NixOS servers, and ended up writing one ourself. This is it. We use it on a daily basis to build and deploy our NixOS fleet, and when we need a feature we add it.

Morph is by no means done. The CLI UI might (and probably will) change once in a while. The code is written by humans with an itch to scratch, and we're discussing a complete rewrite (so feel free to complain about the source code since we don't like it either). It probably wont accidentally switch your local machine, so you should totally try it out, but do consider pinning to a specific git revision.

morph's People

Contributors

0z13 avatar adamtulinius avatar andir avatar bb010g avatar bradfitz avatar c4710n avatar cafkafk avatar ckiee avatar cole-h avatar delroth avatar dependabot[bot] avatar diamondburned avatar eskytthe avatar fooker avatar johanot avatar kalbasit avatar mweinelt avatar nhey avatar phile314 avatar radvendii avatar shados avatar srhb avatar tomprince avatar zimbatm avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

morph's Issues

feat: dont build configuration locally before uploading it to remote host

i've made morph push <config> and I see that it created lots of files in my local machine

https://github.com/DBCDK/morph/blob/master/morph.go#L328
https://github.com/DBCDK/morph/blob/master/nix/nix.go#L168

It seems like you are building them with nix-build

Consider how nix-rebuild is implemented

https://github.com/NixOS/nixpkgs/blob/master/nixos/modules/installer/tools/nixos-rebuild.sh#L100-L102

it uses nix-copy-closure to upload machine configuration to remote and build packages on remote

`nixConfig.post-build-hook` file does not exist

I tried to use the network.nixConfig option to push all the paths that had been build into a binary cache using the post-build-hook feature of Nix. During runtime I get the error that Nix can't find the file that I delcared there. It seems like Morph doesn't try to build those but rather copies their out path and passes that on to Nix.

example config:

{
  network.nixConfig.post-build-hook = toString (pkgs.writeScript "post-build-hook" ''
    echo "$2"
    exit 0
  '');
}

Morph output:

$ morph deploy config/servers.nix --on="*foo*" --reboot boot
Selected 1/3 hosts (name filter:-2, limits:-0):
          0: foo (secrets: 1, health checks: 0)

these derivations will be built:
  /nix/store/svmkhm58r2hmd12nlbbwsb4jqr6q1xgl-morph.drv
building '/nix/store/svmkhm58r2hmd12nlbbwsb4jqr6q1xgl-morph.drv'...
running post-build-hook '/nix/store/wq221k7n2zv6inpwgrnf914x5wbp32rh-post-build-hook'...
post-build-hook: error: executing '/nix/store/wq221k7n2zv6inpwgrnf914x5wbp32rh-post-build-hook': No such file or directory
error: program '/nix/store/wq221k7n2zv6inpwgrnf914x5wbp32rh-post-build-hook' failed with exit code 1

Part of the problem here is probably that I used toStringbut since the string still carries a reference to the actual script it should still work:

nix-repl> s = toString (pkgs.writeScript "test" "exit 1")
nix-repl> builtins.getContext s
{ "/nix/store/7whw7iq2qldb8bh41k6wpra40zlbn232-test.drv" = { ... }; }

Note:This can probably not be solved with #100 entirely as some paths might just be intermediate build results with no references in the output path(s).

Specify target user as depoyment attribute

It would be nice to specify the remote user (as used in the ssh commands) as an attribute of the deployment settings similar to how it's done with targetHost.

This would allow fine grained control and makes the user selection more persistent as it is specified in the deployment expression. The SSH_USER environment variable can still serve as a global fallback.

As a workaround, one can specify the remote user inside the remoteHost option using the user@target format. This will work as long as the SSH_USER environment variable is not set.

Nix build failing (exit 1) without erroring?

I was trying to deploy this over GitLab's CI using morph, but morph keeps failing. Here's the CI output: https://0x0.st/zylt.txt

From the output, I can't seem to point out any errors, it just seems like nix-build just exited 1 for whatever reason. How do I debug?

CI:

image: nixos/nix

variables:
  SSH_SKIP_HOST_KEY_CHECK: "1"

morph:
  cache:
    key: nix
    paths:
      - .nix/
  before_script:
    # Different /nix/store path
    - mkdir -p "$PWD/.nix"
    - mv -f /nix/* "$PWD/.nix/"
    - mount -o bind "$PWD/.nix" /nix
    # Add the stable channel and update
    - nix-channel --add https://nixos.org/channels/nixos-unstable nixos
    - nix-channel --update
    # Install dependencies
    - nix-env -iA nixos.git nixos.gnutar
    - nix-env -iA nixos.morph nixos.openssh
    # Change the permission of the private key so SSH can be happy
    - chown root "$SSH_IDENTITY_FILE"
    - chmod 600 "$SSH_IDENTITY_FILE"
  script:
    - export GIT_SSH_COMMAND="ssh -i $SSH_IDENTITY_FILE -o StrictHostKeyChecking=no"
    #- morph build default.nix
    #- morph deploy default.nix test
    - morph deploy default.nix switch

morph version: 1.3.0 from NixOS Unstable

pkgs is from

    pkgs = import (builtins.fetchGit {
        url = "https://github.com/NixOS/nixpkgs-channels.git";
        ref = "nixos-19.09";
        rev = "64e38f246aebc1fcd800952322c08f92d1420660";
    }) {};

Does morph roll-back in case healthcheck fails?

Does morph roll-back the host in case a health check fails?

I have the situation where nodes are in places with occasionally very bad connectivity. It is crucial the VPN connection is functioning (when the internet connection is up). Can Morph let the node automatically roll-back when a new configuration is applied but fails to come up?

I suppose what I need is NixOS/nixpkgs#65477.

How to install this as a part of systemPackages?

The README states...

Pre-built binaries are not provided, since we install morph through an overlay.

... but no instructions are provided on how to do this, and the shell.nix does not appear suitable for anything other than spawning an ad-hoc shell.

How does one make this part of their system configuration, such that morph is available as a system-wide binary?

Headers in HTTP health checks

A look at the code suggests that there is support for specifying headers to pass along when doing HTTP healthchecks. The Nix option specification however, says that this is not implemented.

It's not clear whether headers are currently supported or not. But if they are not, it would IMO be quite important to support them; especially when setting up a server for which no public DNS records exist yet, you'll often want to test vhosts and such by making requests to the machine's hostname with a spoofed Host header.

What's the current status of header support, and if unsupported, are there any plans to add support for them?

(Aside, I'm quite enjoying morph so far, I'm finding it much easier to reason about than NixOps!)

index out of bounds

Combining --limit with any other similar selecter seems to cause this index out of bounds error:

goroutine 1 [running]:
github.com/dbcdk/morph/filter.FilterHosts(0xc00008fc00, 0x7, 0x8, 0x0, 0x2, 0x8, 0x7, 0x8, 0x0)
	/build/go/src/github.com/dbcdk/morph/filter/filter.go:34 +0x2d5
main.getHosts(0x7ffddba644ea, 0x39, 0x0, 0x0, 0x7da543, 0x5, 0x0)
	/build/go/src/github.com/dbcdk/morph/morph.go:440 +0x205
main.main()
	/build/go/src/github.com/dbcdk/morph/morph.go:179 +0xd5

The file being built contains 7 hosts.

Good:

morph build ~/platform/deployments/hostgroups/dhcp-server.nix --limit 8

Bad:

morph build ~/platform/deployments/hostgroups/dhcp-server.nix --every 2 --limit 8
morph build ~/platform/deployments/hostgroups/dhcp-server.nix --skip 5 --limit 8

Missing documentation: build vs. push vs. deploy

It's not really clear from the README what the difference between these is.

My guess is that:

  • build means evaluating the system configuration and building it into the local Nix store.
  • push means uploading the Nix store items to the target system. Implicitly builds if needed.
  • deploy means actually applying the system configuration that is made up of those uploaded store items, to the target system. Implicitly builds and pushes if needed.

... but I'm not sure that that guess is accurate.

automatically create directories when uploading secrets

Currently morph expects the destination directory to exist.

A simple solution could be to mkdir -p the path, and make sure the owner is root:root with permissions 0755. Later on, if needed, we can then make it possible to specify exact permissions for the directories.

Feature Request: Support for decrypting encrypted secrets at deploy-time

I'd like to be able to keep secrets (say, SSH keys) in the same git repo with my host deployment configuration files, but right now that means I have to either manually manage decrypting them prior to any upload-secrets run (and cleaning up the temporarily decrypted files), or write a wrapper of some kind that does the same.

This isn't all that hard to do*, but it would be much better to have support for deploy-time secrets decryption baked in (ala Ansible Vault, say).

Some options that would be nice to have:

  • Decrypt with passphrase(s)
  • Decrypt with keyfile(s)
  • Request secrets from a secrets server (e.g. a HashiCorp Vault instance)

*: For anyone interested, I'm currently playing with a shell.nix that uses cryfs to mount a directory of encrypted secrets.

Support push in the background

When updating multiple hosts, it would be neat if morph could keep pushing data to the remaining hosts in the background, while waiting for the current host to switch configuration/reboot/&c.

I'm not really sure how to handle this in the UI, since we're currently just attaching stdin/stdout of the nix-process to the current stdin/stdout, which isn't possible in the background. I don't think hiding the progress is a problem, but if the main thread catches up with the push one, we should somehow attach stdin/stdout at that point (or show the progress in some other manner).

Add flag to disable SSH HostKeyChecking

It would be nice to have a flag like --skip-hostkey-check for morph, which will set the following in the morph SSH context:

   StrictHostkeyChecking No
   UserKnownHostsFile /dev/null

The motivation for this is to be able to do first-time host key uploads using the secrets upload feature, or to peform other TOFU (https://en.wikipedia.org/wiki/Trust_on_first_use) provisioning tasks, without being bothered by security-related panics in the process.

Strange secret upload issue, when SSH writes something to stderr

As visualized below, problems occur when a warning or error message is yielded from SSH on stderr while trying to create a temp-file for secret upload on a remote host. This is caused by the temp-file location being read from CombinedOuput of the remote command (both stdout and stderr).

MkTempFile should not use stderr as input for the tempfile location.

Couldn't upload file: secrets/ceph/staging/ceph.client.admin.keyring -> cephmon-s03:@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The fingerprint for the ED25519 key sent by the remote host is
SHA256:XeASo2+9fnm+9FC6RBFJcw/Y42MWs1BFXp4h6TDBkwc.
Please contact your system administrator.
Add correct host key in /home/jth/.ssh/known_hosts to get rid of this message.
Offending ED25519 key in /home/jth/.ssh/known_hosts:138
Password authentication is disabled to avoid man-in-the-middle attacks.
Keyboard-interactive authentication is disabled to avoid man-in-the-middle attacks.
/tmp/tmp.IOhlS2DMca

Document the secrets functionality

I can't seem to find any documentation on the upload-secrets functionality. Could this be added to the README and the examples?

Thanks!

Custom host for deployment.healthChecks.http

Something like

http = [
    {
        host = "127.0.0.1";
        ...
    }
];

Reason being for some hosts such as AWS, using the long EC2 address is typical, though they're never used in actual webservers. It would make more sense to feed the actual domain in or localhost.

(Actual domain may not work, as some are reverse proxied over CloudFlare as well)

"morph build" unnecessarily downloads many source archives

Test case, which is basically examples/simple.nix pinning to current nixpkgs master:

let
  # Pin the deployment package-set to a specific version of nixpkgs
  pkgs = import (builtins.fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/98d9589819218971a95fd64c172fe5996a9734f5.tar.gz";
    sha256 = "0blscxj13qbcnlxkzwjsyqa80ssnx9wm0wz0bg6gkc1fa412w4f9";
  }) {};
in
{
  network =  {
    inherit pkgs;
    description = "simple hosts";
  };

  "web01.example.com" = { config, pkgs, ... }: {
    boot.loader.systemd-boot.enable = true;
    boot.loader.efi.canTouchEfiVariables = true;

    fileSystems = {
        "/" = { label = "nixos"; fsType = "ext4"; };
        "/boot" = { label = "boot"; fsType = "vfat"; };
    };
  };
}

Unexpected behavior: morph build with this network ends up downloading tons of source archives and dev packages:

...
  /nix/store/8lim9yzfpmxf0h62l2wylmgrwg7vx0x3-diffutils-3.7.tar.xz
  /nix/store/8lmlh79y7y10v4d5p6p7amgsfw7z2v49-gcc-wrapper-7.4.0-man
  /nix/store/8lz4k7gnsj4mj44y7vwcrhxw92m00xwp-gmp-6.1.2.tar.bz2
  /nix/store/8m37y0190w6h8njki0q4n2h10rw2g9sl-python3.7-zipp-0.5.1
  /nix/store/8mai2l0w93gsxibrxxs14fj1vg6k3ydh-curl-7.65.3-dev
  /nix/store/8mdb32pazg38aavh03x36z4s5f85wws3-zip-3.0
  /nix/store/8n7rrkyw2xq53z235id2s3xl8a0wp13b-nix.conf
  /nix/store/8nwwykns35qj19fik1blr748ivj6d563-libtiff-4.0.10-doc
  /nix/store/8pakfmb2lc9i871vh0jcrjggjmj9q5w6-openldap-2.4.48-devdoc
  /nix/store/8pm5qd1n4cf2pi30rxdq2y59bw1548km-automake-1.16.1
  /nix/store/8rrynknxlrycsfa3hcf0n1znqhih37q5-source
  /nix/store/8s8kq6pq9wzqavmmpsa2fz7lk7yvxh36-libcap-2.27-doc
  /nix/store/8spbwgczsk6w12swv4ald35mdcc6rzqx-autogen-5.18.12-bin
  /nix/store/8xbg46w9a4fkdkn2lsvdlkybsp9lpr7p-prawn-icon-2.3.0.gem
  /nix/store/8xk19qj1s5nbw8i0rl4qy53mla4zrpb6-libndctl-66
  /nix/store/8yl8rlah4wkvy4hymk844l7sj2pff5yb-mini_portile2-2.4.0.gem
  /nix/store/8ym99rjmd0cxxkvc5k846n51a15bv1y1-perl5.30.0-CGI-Fast-2.15-devdoc
  /nix/store/8z393qxyp4vvr52jvqvvzmh60bkzj865-perl5.30.0-Net-DBus-1.1.0-devdoc
...

This seems to be because of the following in eval-machines.nix:

ln -s ${nodeDef.config.system.build.toplevel.drvPath} $out/${nodeName}.drv

I'm guessing that this ends up dragging in the whole transitive closure of .drv files and their own dependencies. Maybe? I don't know much about Nix :-)

Transient network errors are poorly handled

If an SSH connection to a deployment target dies, Morph appears to hang indefinitely, neither retrying nor failing with an error.

I recently left a deploy running and came back hours later only to find:

Pushing paths to storage002:                                                            
        * ...-nixos-system-storage002-19.09pre-git
        * ...-nixos-system-storage002-19.09pre-git.drv                     
[1/104/594 copied (192.0/685.2 MiB)] copying path '...-linux-4.19.79' to 'ssh://storage002'Timeout, server w.x.y.z not responding.

The server was fine but my network hiccuped at some point.

Instead of hanging forever, morph should either actually fail (allowing a morph-invoking driver to implement retry logic) or perform retries itself.

--upload-secrets does not honor SSH_USER, SSH_IDENTITY_FILE

The call to scp isn't passed the correct arguments to take SSH_USER and SSH_IDENTITY_FILE environment variables into account.

Since scp accepts the same arguments as ssh, the initialSSHArgs function should be able to be used just the same, with just a little reordering of the arguments.

New per-machine nixpkgs merge causes pkgs.pkgsi686Linux to be cross-compiled

Test case: have a morph deployment with hardware.pulseaudio.support32Bit = true;

Current behavior: morph build cross-builds half of the world to build an i686 alsa-plugins package.

Expected behavior: morph build does not do that, which is what happens with e.g. nixops or nixos-rebuild on the same config.

Can be easily repro'd by trying to build anything from pkgsi686Linux:

nix-build eval-machines.nix -A 'nodes.mynode.pkgs.pkgsi686Linux.alsaPlugins' --arg networkExpr mynet.nix

cc @Shados

Directory permissions for secret deployments

I'm not sure how I can directory permissions for deployed secrets. In this case I have a secret for nginx, but the nginx user is not allowed to access /run/keys/nginx/something.htpasswd, actually not even /run/keys.

{
  deployment.secrets."something.htpasswd" = {
    source = "./machines/${config.networking.hostName}/secrets/nginx/something.htpasswd";
    destination = "/run/keys/nginx/something.htpasswd";
    owner.user = "nginx";
    owner.group = "root";
    permissions = "0400";
    action = [ "sudo" "systemctl" "restart" "nginx.service" ];
  };
}
Oct 31 21:33:38 host.example.com nginx[22670]: 2019/10/31 21:33:38 [crit] 22680#22680: *10 open() "/run/keys/nginx/something.htpasswd" failed (13: Permission denied), client: 0.0.0.0, server: host.example.com, request: "GET /protected/config HTTP/1.1", host: "host.example.com"
# sudo -u nginx ls /run/keys/
ls: cannot open directory '/run/keys/': Permission denied
# ls -lah /run/keys
drwxr-x---  4 root root   0 Oct 31 20:02 .
[...]

I'm currently not entirely sure how to grant nginx access to that path.

Support running hook (scripts) during different parts of the deployment process

It is probably a good place to have a discussion about this kind of feature here.

I'd like to be able to hook into the deployment of my nodes with some custom scripts. Depending on the server I'd like to unlock the rootfs via SSH when I reboot it. e.g. morph deploy ... reboot should execute some custom script that is executed as soon as the machine is reachable again.

Ideas for hooks:

  • before-build: before the actual machines build is executed this command shall be run. Potentially with some meaningful arguments (path to network, target name, flags?).

  • after-build: once the build of a machine has finished this would be executed. Useful if you want to upload all your build outputs to some (remote) binary cache before actually deploying something.

  • before-deploy: Before running a deployment on a single machine (fires once for each?) this script would be executed.

  • after-deploy: Pretty much at the time where you would push secrets I guess.

  • before-reboot: Before the reboot is triggered. Could help users to remove a server from a LB or to gracefully kill some process.

  • after-reboot: After the reboot when the servers become available again.

I would expect the configuration to look like this:

{
network.hooks = {
   pre-build = pkgs.writeScript "pre-build" "exit 0";
};

some-host.deployment = {
   hooks = {
    pre-build = pkgs.writeScript "yet-another-script-pre-build" "exit 1";
    after-build = pkgs.writeScript "push-out-path" "nix copy $1 --to file:///some/local/binary/cache";
  };
};
}

By no means I am set on any of the interfaces or specific hooks outlines above. I just want to throw my (brief) idea in here and start a discussion about this.

If --reboot is given --upload-secrets should be done after the reboot

When deploying to a host and passing --reboot and --upload-secrets the secrets are deployed before rebooting, which doesn't make sense when deploying to volatile storage. Instead please upload the secrets after the reboot has finished.

Example log:

[hexa@gaia:~/git/hexa/nixos-servers]$ morph deploy config/default.nix boot --reboot --upload-secrets --on=helios.lossy.network
Selected 1/2 hosts (name filter:-1, limits:-0):
	  0: helios.lossy.network (secrets: 2, health checks: 0)

these derivations will be built:
  /nix/store/xb8lwh8pdxphc143x1b5xxpkjxhyd26r-morph.drv
building '/nix/store/xb8lwh8pdxphc143x1b5xxpkjxhyd26r-morph.drv'...
/nix/store/h7hyav57jw9p9m9kb5m89n2b5nxq8l6a-morph
nix result path: 
/nix/store/h7hyav57jw9p9m9kb5m89n2b5nxq8l6a-morph

Pushing paths to helios.lossy.network (@helios.lossy.network):
	* /nix/store/kz4k4yw6hnvwnrsin2wabm5d3wl50d4x-nixos-system-helios.lossy.network-19.09pre-git
copying path '/nix/store/lcsj6nwhf2pnw9j82f0r0spmick2ryp5-bind-9.14.9-man' from 'https://cache.nixos.org'...
copying path '/nix/store/50z37qm9ij90brs5fjvny3fskf0cczqz-bind-9.14.9-lib' from 'https://cache.nixos.org'...
copying path '/nix/store/4ih5rin77afk09gf8n4crimxb0p4w12w-firmware' from 'https://cache.nixos.org'...
copying path '/nix/store/yh51mg28azshz680vrharja7w8iwjrc8-bind-9.14.9-host' from 'https://cache.nixos.org'...
copying path '/nix/store/1vm6cdpbp0wj187rzapsij7wzhd4c61r-jrpqx2yppvvr5hy6xsvzaqalrxf7dixm-source' from 'https://cache.nixos.org'...
copying path '/nix/store/3gw0vaq30spqg5ndicic6amhqmz57z6b-linux-4.19.95' from 'https://cache.nixos.org'...
copying path '/nix/store/jgh11psr8gvqdl3wnzccd2v266a0si1j-nixos-manpages' from 'https://cache.nixos.org'...
copying path '/nix/store/kdb5rmri32kcvc1cynsrpa2p8g7hf5h5-kernel-modules' from 'https://cache.nixos.org'...
copying path '/nix/store/0753ri56m1ksrcj0dggk53jsqh43q0c7-nixos-manual-html' from 'https://cache.nixos.org'...
copying path '/nix/store/iz0vgnyd23xy3px203hdjj4yzgd3k4am-nixos-help' from 'https://cache.nixos.org'...
[14 copied (17.9 MiB)]

Uploading secrets to helios.lossy.network (helios.lossy.network):
	* borgbackup-password (65 bytes).. OK
	* borgbackup-sshkey (399 bytes).. OK
Running healthchecks on helios.lossy.network (helios.lossy.network):
Health checks OK

Executing 'boot' on matched hosts:

** helios.lossy.network
updating GRUB 2 menu...

Asking host to reboot ... OK
Waiting for host to come online .ssh_exchange_identification: read: Connection reset by peer
...... OK
Running healthchecks on helios.lossy.network (helios.lossy.network):
Health checks OK
Done: helios.lossy.network

I'm using 0630d6b

Support diffing a machine

Fetch the drv corresponding to current-system from the remote host and diff it with the generated drv.

Add Travis CI build

Add Travis CI build configuration to build morph.
Preferably using the Travis Nix module.

Minimum requirements are:

  • Build morph when pushing to a branch
  • Build using nix derivation in /nix-packaging

Nice to haves:

  • Block merge if build fails
  • Test sourcing of autocomplete scripts
  • Test binary execution

nixpkgs config inside machine definitions is ignored

Problem

Here's an example deployment file to demonstrate what I mean:

let
  pkgs = import <nixpkgs> { };
in
{
  network = {
    inherit pkgs;
    description = "Example overlay/override failure";
  };

  "example.com" = { config, lib, pkgs, ...}: {
    nixpkgs.overlays = [
      (self: super: {
        pam_mount = super.pam_mount.overrideAttrs(oldAttrs: {
          name = "pam_mount overidden by overlay";
        });
      })
    ];
    nixpkgs.config.packageOverrides = pkgs: {
      pam_mount = pkgs.pam_mount.overrideAttrs(oldAttrs: {
        name = "pam_mount overidden by package overrides";
      });
    };
    system.activationScripts.testingOverlays = ''
      echo "${builtins.trace pkgs.pam_mount.name "nak"}"
    '';

    # Boilerplate
    fileSystems."/" = { label = "root"; fsType = "ext4"; };
    boot.loader.grub.device = "/dev/sda";
  };
}

If you morph build that, you'll get trace output of trace: pam_mount-2.16 or similar, rather than trace: pam_mount overridden by overlay as you would if you were building the same NixOS configuration any other way.

Cause

Morph passes pkgs from the network to the machine definitions, which is nice in terms of pinning nixpkgs versions and sharing nixpkgs config between machines, but problematic because nixpkgs' eval-config.nix will set the pkgs module argument via mkForce if pkgs is passed into it like this.

Fix

I think this would require an upstream nixpkgs change or using a vendored, modified eval-config.nix to fix*, but it's pretty near the end of a work day here and I have not fully thought it through.

*: Where 'fix' means "merge machine-specific nixpkgs elements on top of the network-specified ones"

Encrypt secrets locally

Hello,

First, thanks for this great project that seems to solve most issues I had with NixOps. There is however a feature I'd love to see before I move my own deploy code to morph : a way to encrypt secrets locally. Indeed, I like to put my secrets in the git repo of my configuration, that way I can work from any places, and if someone access my computer he usually won't be able to run get the passwords.

The way I proceed in my deploy code is as follow : I usually encrypt my file with gpg, and when I deploy it, I check if the file mysecret.txt.gpg is already on the server. If not, or if the checksum are different, I decrypt the file (morph could just take the decrypt command as a parameter for more flexibility, and the command is expected to write the decrypted file in stdout), copy the decrypted file into the file mysecret.txt, and finally I replace the file mysecret.txt.gpg on the server (order is important, it makes sure that if the connection stops in the middle, re-running the command will put the good password file online). Of course, I'm not pretending it's the only (or even best) solution, and it could also be possible to update secrets based on the last-modified time, but I just chose it because it's pretty robust to clock issues (it's not rare to see a computer with poorly configured clock), while still making sure that decryption happens only when the secret change.

One advantage of that method is that as soon as the user is free to choose any decryption method, it could even be possible to encrypt a file with passwords that are given to only some specific teams, or even with more involved scheme based on email's pgp public keys…

What do you think ?

Thanks.

morph evaluates nixpkgs.overlays twice

Example repro:

let
  # Pin the deployment package-set to a specific version of nixpkgs
  pkgs = import (builtins.fetchTarball {
    url = "https://github.com/NixOS/nixpkgs/archive/98d9589819218971a95fd64c172fe5996a9734f5.tar.gz";
    sha256 = "0blscxj13qbcnlxkzwjsyqa80ssnx9wm0wz0bg6gkc1fa412w4f9";
  }) {};
in
{
  network =  {
    inherit pkgs;
    description = "simple hosts";
  };

  "web01.example.com" = { config, pkgs, ... }: {
    boot.loader.systemd-boot.enable = true;
    boot.loader.efi.canTouchEfiVariables = true;

    nixpkgs.overlays = [(self: super: {
      my_hello = (if super?my_hello then throw "fail" else super.hello);
    })];

    environment.systemPackages = with pkgs; [ my_hello ];

    fileSystems = {
        "/" = { label = "nixos"; fsType = "ext4"; };
        "/boot" = { label = "boot"; fsType = "vfat"; };
    };
  };
}

morph build fails to build this derivation because the throw is triggered. This means that the overlay is applied twice.

This causes real world issues when for example you use an overlay to apply a patch to a package: patches = old.patches ++ [ ./mypatch ]; will end up appending the patch twice, causing build failures.

Both nixops and the normal nixos-config handle this case correctly and apply the overlay only once.

cc @Shados (again, sorry :P I'm working through the issues trying to migrate my small network from nixops to morph)

morph exec should have a --script option

It should be possible to supply a full shell script to morph to be executed on a/multiple remote host(s).

Something along the lines of morph exec --script foobar.sh ./hostgroups/k8s.nix

The supplied script would be uploaded to the remote hosts as tempfiles and executed like so: bash /tmp/xXya81.foobar.sh

If the --script flag is provided, the command argument should either be:

  • omitted

  • its value be prepended to the executing shell command at the remote

  • its value be replacing the executing shell command at the remote

missing examples

Create a simple example that can be built (and theoretically deployed) with morph.

Document latest features

  • network.nixConfig
  • network.runCommand
  • network.lib
  • network.pkgs (now optional)
  • deployment.substituteOnDestination
  • deployment.targetHost
  • deployment.targetUser

Secret not deployed

deployment.secrets:

deployment = {
	secrets = {
 		"shihoya-key" = {
			source = "./secrets/shihoya-key";
			destination = "/var/shihoya-key";
			owner = {
				user = "shihoya";
				group = "shihoya";
			};
			permissions = "0400";
			action = [ "sudo" "systemctl" "restart" "shihoya-api" ];
		};
	};
};

Deployed node:

[root@ceris-cbt:~]# ls -a /var
.  ..  cache  db  empty  lib  lock  log  run  spool  tmp  .updated

Host:

► ./secrets 
shihoya-key      shihoya-key.pub  

morph saw the secrets as well:

Selected 1/1 hosts (name filter:-0, limits:-0):
	  0: [email protected] (secrets: 1, health checks: 0)

Support nixos tests

How cool would it be to be able to run <nixpkgs/nixos/tests/make-test.nix> with morph? Right now the problems is the deployment options and secret sharing.

Cannot add path ... because it lacks a valid signature

I'm trying to deploy to a Hetzner physical machine that was provisioned by nixops. I could build (morph build deployment.nix) and deploy secrets (morph upload-secrets deployment.nix) but the actual code deploy fails in this strange way. (At least it's strange to me, I'm fairly new to nix and morph/nixops).

xxx@nixos /h/e/e/morph> morph deploy deployment.nix test
Selected 1/1 hosts (name filter:-0, limits:-0):
          0: xxxx-amy (secrets: 1, health checks: 0)

/nix/store/gmv0xbbjk8liq3h29nwn2sqq7r0jxpgr-morph
nix result path:
/nix/store/gmv0xbbjk8liq3h29nwn2sqq7r0jxpgr-morph

Pushing paths to xxxx-amy:
        * /nix/store/zhsf6yycsyi9p809p9g1z7n31905mhwh-nixos-system-xxx-18.09pre-git
        * /nix/store/wvphz871j2sfz01k2pm1wxbp105kw55d-nixos-system-xxxx-18.09pre-git.drv
[2/0/686 copied (0.0/6.3 MiB)] copying path '/nix/store/0mhh8r6cv9hc3ml7fsclbcmz6riax225-healthcheck-commands.txt' to 'error: cannot add path '/nix/store/0mhh8r6cv9hc3ml7fsclbcmz6riax225-healthcheck-commands.txt' because it lacks a valid signature
error (ignored): unexpected end-of-file
error (ignored): writing to file: Broken pipe
error (ignored): writing to file: Broken pipe
[1 copied (1 failed) (0.0 MiB)]
error: unexpected end-of-file
xxx@nixos /h/e/e/morph>

switch-to-configuration: line 3: use: command not found

Hello,

I'm trying to use morph to deploy a Raspberry-pi 3b+ from my x86_64 system. I was able to replicate this issue also deploying an x86_64 system from my x86_64 system and I'm not sure why this is happening. Please find the error log, as well as all of the files used for the morph build.

Error log:

 pure  λ  SSH_USER=root morph deploy nixos/testlab.nix boot
Selected 1/1 hosts (name filter:-0, limits:-0):
          0: 172.25.10.151 (secrets: 0, health checks: 0)

/nix/store/122d3ylj2b4c50a0aidv5l7ikasilrc6-morph
nix result path:
/nix/store/122d3ylj2b4c50a0aidv5l7ikasilrc6-morph

Pushing paths to 172.25.10.151:
        * /nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git
        * /nix/store/l8b8np9l8ry2r9qga0l8m4zp9gyn2k92-nixos-system-kore-19.09pre-git.drv

Executing 'boot' on matched hosts:

** 172.25.10.151
/nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git/bin/switch-to-configuration: line 3: use: command not found
/nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git/bin/switch-to-configuration: line 4: use: command not found
/nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git/bin/switch-to-configuration: line 5: use: command not found
/nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git/bin/switch-to-configuration: line 6: use: command not found
/nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git/bin/switch-to-configuration: line 7: use: command not found
/nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git/bin/switch-to-configuration: line 8: syntax error near unexpected token `('
/nix/store/wckw45dkz19ncfi5nzfb736qnhzv77zg-nixos-system-kore-19.09pre-git/bin/switch-to-configuration: line 8: `use Sys::Syslog qw(:standard :macros);'
Error while activating new configuration.% 

Nixpkgs is pinned to NixOS/nixpkgs@0419d98.

testlab.nix
let
  pkgs = import ../../../vendor/nix/nixpkgs.nix {};
in {
  network = {
    inherit pkgs;
    description = "KeepTruckin Nix builders";
  };

  # TODO: This does not work. I have to use the IP directly as the name of the
  # host instead of naming it kore and using deployment.targetHost. When I do
  # that, I get kore: No such file or directory.
  # "kore" = { config, pkgs, ... }: {
  #   imports = [
  #     ./hosts/kore
  #   ];
  #
  #   deployment.targetHost = "172.25.10.151";
  # };

  "172.25.10.151" = import ./hosts/kore;
}
hosts/kore/default.nix
{ config, pkgs, ... }:

{
  imports = [
    ./hardware-configuration.nix
  ];

  networking.hostName = "kore";
  # networking.wireless.enable = true;

  security.sudo.enable = true;

  time.timeZone = "America/Los_Angeles";

  environment.systemPackages = with pkgs; [
    vim
  ];

  nixpkgs.config.allowUnfree = true;

  services.openssh.enable = true;

  system.stateVersion = "19.09"; # Did you read the comment?
}
hosts/kore/hardware-configuration.nix
{ config, lib, ... }:

let
  pkgs = import ../../../../../vendor/nix/nixpkgs.nix {};
in {
  imports =
    [ "${pkgs.path}/nixos/modules/installer/scan/not-detected.nix"
    ];

  # Use the extlinux boot loader. (NixOS wants to enable GRUB by default)
  boot.loader.grub.enable = false;
  # Enables the generation of /boot/extlinux/extlinux.conf
  boot.loader.generic-extlinux-compatible.enable = true;

  boot.initrd.availableKernelModules = [ ];
  boot.initrd.kernelModules = [ ];
  boot.kernelModules = [ ];
  boot.extraModulePackages = [ ];
  boot.kernelParams = [ "console=ttyS1,115200n8" ];

  nixpkgs.system = "aarch64-linux";

  hardware.enableRedistributableFirmware = true;

  fileSystems."/" =
    { device = "/dev/disk/by-uuid/44444444-4444-4444-8888-888888888888";
      fsType = "ext4";
    };

  swapDevices = [ ];

  nix.maxJobs = lib.mkDefault 4;
}

I'm using morph version 1.2.0.

HTTP health checks can pass before the host has rebooted

Output from when using the --reboot flag of the deploy command.

Executing 'boot' on matched hosts:

** dhcp2036-p01

Asking host to reboot ... 
Running healthchecks on dhcp2036-p01:
	* Test Infestor is running: OK
	* Ask systemd whether there are failed or queued units.: Failed (Health check error: Failed to connect to bus: Broken pipe
Failed to connect to bus: Broken pipe
)
	* Test DHCP request using /nix/store/7dq9wap169g1dx8ssy2z0gxmmyrjrprw-check-dhcp: Failed (Health check error: Timeout after 5s)

The Infester check is HTTP based, while the others require SSH. Immediately after requesting a reboot, morph will start performing health checks, and it's pretty clear that a service can still be up at that point in time. We should implement some logic to check whether the host has rebooted, before running health checks.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.