commit 8ba635232e568f4c464385ad6df6c4b056126c54 Author: Jeffrey C. Ollie Date: Tue Apr 2 14:02:52 2024 -0500 first diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..eccd3d5 --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1694529238, + "narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "ff7b65b44d01cf9ba6a71320833626af21126384", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1711668574, + "narHash": "sha256-u1dfs0ASQIEr1icTVrsKwg2xToIpn7ZXxW3RHfHxshg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "219951b495fc2eac67b1456824cc1ec1fd2ee659", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-23.11", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..dc89ce0 --- /dev/null +++ b/flake.nix @@ -0,0 +1,357 @@ +{ + description = "anycast"; + + inputs = { + nixpkgs = { + url = "nixpkgs/nixos-23.11"; + }; + flake-utils = { + url = "github:numtide/flake-utils"; + }; + }; + outputs = { + self, + nixpkgs, + flake-utils, + ... + } @ inputs: + flake-utils.lib.eachDefaultSystem + ( + system: let + pkgs = import nixpkgs { + inherit system; + }; + in { + packages = { + anycast-healthchecker = let + pname = "anycast-healthchecker"; + version = "0.9.8"; + hash = "sha256-eu1uRkraHsAcKh4x9w7KZMmpfggxCCorE21MpwWLOU0="; + in + pkgs.python3.pkgs.buildPythonApplication { + inherit pname version; + + pyproject = true; + + src = pkgs.fetchFromGitHub { + owner = "unixsurfer"; + repo = "anycast_healthchecker"; + rev = version; + hash = hash; + }; + + env = { + PBR_VERSION = version; + }; + + postPatch = '' + substituteInPlace anycast_healthchecker/servicecheck.py \ + --replace /sbin/ip ${pkgs.iproute2}/bin/ip + ''; + + nativeBuildInputs = [ + pkgs.python3Packages.pbr + pkgs.python3Packages.setuptools + ]; + + propagatedBuildInputs = [ + pkgs.python3Packages.docopt + pkgs.python3Packages.prometheus-client + pkgs.python3Packages.python-json-logger + ]; + meta = with pkgs.lib; {}; + }; + }; + } + ) + // { + nixosModules = { + anycast = { + config, + lib, + pkgs, + ... + }: let + cfg = config.anycast; + package = self.packages.${pkgs.system}.anycast-healthchecker; + statedir = "/var/lib/anycast-healthchecker"; + in { + options = { + anycast = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "anycast"; + + interface = lib.options.mkOption { + type = lib.types.str; + default = "lo"; + }; + + ipv4 = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "ipv4"; + conf = lib.options.mkOption { + type = lib.types.str; + default = "${statedir}/anycast-v4-prefixes.conf"; + }; + variable = lib.options.mkOption { + type = lib.types.str; + default = "ANYCAST_V4_PREFIXES"; + }; + reconfigureCommand = lib.options.mkOption { + type = lib.types.str; + default = "${pkgs.bird}/bin/birdc configure"; + }; + dummyPrefix = lib.options.mkOption { + type = lib.types.str; + default = "192.0.2.1/32"; + description = '' + An IP prefix in the form / which will be always available in the list defined by bird_variable to avoid having an empty list. The dummy_ip_prefix must not be used by any service or assigned to the interface set with interface or configured anywhere on the network as anycast-healthchecker does not perform any checks for it. + ''; + }; + keepChanges = lib.options.mkOption { + type = lib.types.bool; + default = false; + description = '' + Keep a history of changes for bird_conf file by copying it to a directory. During the startup of anycast-healthchecker a directory with the name history is created under the directory where bird_conf file resides. The daemon has to have sufficient privileges to create that directory. + ''; + }; + changesCounter = lib.options.mkOption { + type = lib.types.ints.positive; + default = 128; + description = '' + How many bird_conf files to keep in the history directory. + ''; + }; + }; + }; + default = {}; + }; + + ipv6 = lib.options.mkOption { + type = lib.types.submodule { + options = { + enable = lib.options.mkEnableOption "ipv6"; + conf = lib.options.mkOption { + type = lib.types.str; + default = "${statedir}/anycast-v6-prefixes.conf"; + }; + variable = lib.options.mkOption { + type = lib.types.str; + default = "ANYCAST_V6_PREFIXES"; + }; + reconfigureCommand = lib.options.mkOption { + type = lib.types.str; + default = "${pkgs.bird}/bin/birdc configure"; + }; + dummyPrefix = lib.options.mkOption { + type = lib.types.str; + default = "2001:db8::1/128"; + description = '' + An IPv6 prefix in the form / which will be always available in the list defined by bird_variable to avoid having an empty list. The dummy_ip_prefix must not be used by any service or assigned to the interface set with interface or configured anywhere on the network as anycast-healthchecker does not perform any checks for it. + ''; + }; + keepChanges = lib.options.mkOption { + type = lib.types.bool; + default = false; + description = '' + Keep a history of changes for by copying it to a directory. During the startup of anycast-healthchecker a directory with the name history is created under the directory where bird_conf file resides. The daemon has to have sufficient privileges to create that directory. + ''; + }; + changesCounter = lib.options.mkOption { + type = lib.types.ints.positive; + default = 128; + description = '' + How many files to keep in the history directory. + ''; + }; + }; + }; + default = {}; + }; + + checks = lib.options.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options = { + checkDisabled = lib.options.mkOption { + type = lib.types.bool; + default = false; + description = '' + true disables the check, false enables it + ''; + }; + onDisabled = lib.options.mkOption { + type = lib.types.enum [ + "withdraw" + "advertise" + ]; + default = "withdraw"; + description = '' + What to do when check is disabled, either withdraw or advertise + ''; + }; + checkCommand = lib.options.mkOption { + type = lib.types.str; + description = '' + The command to run to determine the status of the service based on the return code. Complex health checking should be wrapped in a script. When check command fails, the stdout and stderr appears in the log file. + ''; + }; + checkInterval = lib.options.mkOption { + type = lib.types.ints.positive; + default = 2; + description = '' + How often to run the check. + ''; + }; + checkTimeout = lib.options.mkOption { + type = lib.types.ints.positive; + default = 2; + description = '' + Maximum time in seconds for the check command to complete. anycast-healthchecker will try kill the check if it doesn't return after check_timeout seconds. If check_cmd runs under another user account (root) via sudo then it won't be killed. anycast-healthchecker could run as root to overcome this problem, but it is highly recommended to run it as normal user. + ''; + }; + checkFail = lib.options.mkOption { + type = lib.types.ints.positive; + default = 2; + description = '' + A service is considered DOWN after these many consecutive unsuccessful health checks + ''; + }; + checkRise = lib.options.mkOption { + type = lib.types.ints.positive; + default = 2; + description = '' + A service is considered HEALTHY after these many consecutive successful health checks + ''; + }; + ipPrefix = lib.options.mkOption { + type = lib.types.str; + description = '' + IP prefix associated with the service. It must be assigned to the interface set in interface parameter unless ip_check_disabled is set to true. Prefix length is optional and defaults to 32 for IPv4 addresses and to 128 for IPv6 addresses. + ''; + }; + ipCheckDisabled = lib.options.mkOption { + type = lib.types.bool; + default = false; + description = '' + true disables the assignment check of ip_prefix to the interface set in interface, false enables it. + If the check_cmd checks the availability of the service by sending a request to the Anycasted IP address then this request may be served by another node that advertises the same IP address on the network. This usually happens when the Anycasted IP address is not assigned to loopback or any other interface on the local node. + Therefore, it should be only enabled in environments where the network or the network configuration of the local node prevents the request from check_cmd to be forwarded to another node. + ''; + }; + interface = lib.options.mkOption { + type = lib.types.str; + default = "lo"; + description = '' + The name of the interface that ip_prefix is assigned to + ''; + }; + customBirdReconfigureCommand = lib.options.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + A custom command to trigger a reconfiguration of Bird daemon. This overwrites the value of bird_reconfigure_cmd and bird6_reconfigure_cmd settings. This setting allows the use of a custom command to trigger a reconfiguration of Bird daemon after an IP prefix is either added to or removed from Bird configuration. If return code is not a zero value then an error is logged together with STDERR of the command, if there is any. anycast-healthchecker passes one argument to the command, which is up when IP prefix is added or down when is removed, so the command can perform different things depending the status of the service. + ''; + }; + customBirdReconfigureCommandTimeout = lib.options.mkOption { + type = lib.types.int; + default = 2; + description = '' + Maximum time in seconds for the custom_bird_reconfigure_cmd to complete. anycast-healthchecker will try kill the command if it doesn't return after custom_bird_reconfigure_cmd_timeout seconds. If custom_bird_reconfigure_cmd runs under another user account (root) via sudo then it won't be killed. anycast-healthchecker could run as root to overcome this problem, but it is highly recommended to run it as normal user. + ''; + }; + }; + } + ); + default = {}; + }; + }; + }; + }; + }; + + config = let + format = pkgs.formats.ini {}; + conf = + format.generate "anycast-healthchecker.conf" + { + DEFAULT = { + interface = cfg.interface; + }; + daemon = { + pidfile = "/run/anycast-healthchecker/anycast-healthechecker.pid"; + ipv4 = cfg.ipv4.enable; + ipv6 = cfg.ipv6.enable; + bird_conf = cfg.ipv4.conf; + bird6_conf = cfg.ipv6.conf; + bird_variable = cfg.ipv4.variable; + bird6_variable = cfg.ipv6.variable; + bird_reconfigure_cmd = cfg.ipv4.reconfigureCommand; + bird6_reconfigure_cmd = cfg.ipv6.reconfigureCommand; + dummy_ip_prefix = cfg.ipv4.dummyPrefix; + dummy_ip6_prefix = cfg.ipv6.dummyPrefix; + bird_keep_changes = cfg.ipv4.keepChanges; + bird6_keep_changes = cfg.ipv6.keepChanges; + bird_changes_counter = cfg.ipv4.changesCounter; + bird6_changes_counter = cfg.ipv6.changesCounter; + purge_ip_prefixes = false; + loglevel = "debug"; + log_maxbytes = 104857600; + log_backups = 8; + log_server_port = 514; + json_stdout = true; + json_log_file = false; + json_log_server = false; + }; + }; + conf_d = + pkgs.linkFarm "anycast-healthchecker.d" + ( + lib.attrsets.mapAttrsToList + ( + name: value: ( + let + conf = format.generate "${name}.conf" { + ${name} = { + check_cmd = value.checkCommand; + check_interval = value.checkInterval; + check_timeout = value.checkTimeout; + check_fail = value.checkFail; + check_rise = value.checkRise; + check_disabled = value.checkDisabled; + on_disabled = value.onDisabled; + ip_prefix = value.ipPrefix; + ip_check_disabled = value.ipCheckDisabled; + }; + }; + in { + name = "${name}.conf"; + path = "${conf}"; + } + ) + ) + cfg.checks + ); + in + lib.mkIf cfg.enable { + systemd.services.anycast-healthchecker = { + enable = cfg.enable; + serviceConfig = { + Type = "simple"; + User = "anycast-healthchecker"; + Group = "bird2"; + DynamicUser = true; + RuntimeDirectory = "anycast-healthchecker"; + StateDirectory = "anycast-healthchecker"; + ExecStart = "${package}/bin/anycast-healthchecker --file ${conf} --dir ${conf_d}"; + Restart = "on-failure"; + }; + wantedBy = ["multi-user.target"]; + }; + }; + }; + }; + }; +}