commit aa22874883ded3129230794184ed8fff71a46461 Author: Jan-Henrik Bruhn Date: Thu Apr 2 20:38:09 2026 +0200 initial commit§ diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..c58a5fe --- /dev/null +++ b/flake.lock @@ -0,0 +1,48 @@ +{ + "nodes": { + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1773889306, + "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "owner": "nix-community", + "repo": "disko", + "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1774709303, + "narHash": "sha256-D3Q07BbIA2KnTcSXIqqu9P586uWxN74zNoCH3h2ESHg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "8110df5ad7abf5d4c0f6fb0f8f978390e77f9685", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "disko": "disko", + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ed22d64 --- /dev/null +++ b/flake.nix @@ -0,0 +1,51 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { self, nixpkgs, disko, ... }: + let + # Helper to build a NixOS host config from hosts// + mkHost = name: system: nixpkgs.lib.nixosSystem { + modules = [ + { nixpkgs.hostPlatform = system; } + disko.nixosModules.disko + ./modules/common.nix + ./hosts/${name} + ]; + }; + + hosts = { + telefonmann = { system = "x86_64-linux"; }; + }; + in { + # nixosConfigurations is used by nixos-anywhere for initial install + nixosConfigurations = nixpkgs.lib.mapAttrs + (name: cfg: mkHost name cfg.system) + hosts; + + # colmena hive for ongoing deployments + colmena = { + meta = { + nixpkgs = import nixpkgs { system = "x86_64-linux"; }; # fallback for colmena internals + specialArgs = { inherit disko; }; + }; + } // nixpkgs.lib.mapAttrs (name: cfg: { + deployment = { + # Set targetHost per host in hosts//default.nix or override here + # targetHost = "telefonmann.example.com"; + targetUser = "root"; + }; + imports = [ + { nixpkgs.hostPlatform = cfg.system; } + disko.nixosModules.disko + ./modules/common.nix + ./hosts/${name} + ]; + }) hosts; + }; +} diff --git a/hosts/telefonmann/backgrounds/wombel.png b/hosts/telefonmann/backgrounds/wombel.png new file mode 100644 index 0000000..25ce8ff Binary files /dev/null and b/hosts/telefonmann/backgrounds/wombel.png differ diff --git a/hosts/telefonmann/default.nix b/hosts/telefonmann/default.nix new file mode 100644 index 0000000..6e15a81 --- /dev/null +++ b/hosts/telefonmann/default.nix @@ -0,0 +1,61 @@ +{ ... }: { + imports = [ + ./hardware.nix + ./disko.nix + ../../modules/vm-guest.nix + ../../modules/voip + ]; + + networking.hostName = "telefonmann"; + + networking.interfaces.ens19 = { + ipv4.addresses = [{ + address = "10.0.10.2"; + prefixLength = 24; + }]; + }; + + services.voip = { + enable = true; + serverAddress = "10.0.10.2"; + ntpServer = "10.0.10.1"; + + phones = { + "e0899d946ccc" = { + model = "cisco-8961"; + extension = "100"; + label = "Küchentelefon"; + password = "changeme100"; + voicemailTimeout = 10; + }; + "e0899d947650" = { + model = "cisco-8961"; + extension = "102"; + label = "Flur"; + password = "changeme100"; + voicemailTimeout = 10; + }; + "101" = { + model = "sip-client"; + extension = "101"; + password = "changeme101"; + }; + }; + + backgroundImages = { + "Wombel" = ./backgrounds/wombel.png; + }; + + intercomPrefix = "*80"; + + extensions = { + "100" = { displayName = "Küche"; }; + "101" = { displayName = "101"; }; + "102" = { displayName = "Flur"; }; + "*99" = { mode = "page"; displayName = "Durchsage an alle"; }; + "999" = { mode = "app"; app = "Playback(hello-world)"; }; + }; + }; + + deployment.targetHost = "telefonmann"; # or IP address +} diff --git a/hosts/telefonmann/disko.nix b/hosts/telefonmann/disko.nix new file mode 100644 index 0000000..cf70250 --- /dev/null +++ b/hosts/telefonmann/disko.nix @@ -0,0 +1,35 @@ +{ ... }: { + disko.devices = { + disk = { + main = { + type = "disk"; + # Proxmox VirtIO SCSI (scsi0) → /dev/sda + # Proxmox VirtIO Block (virtio0) → /dev/vda + device = "/dev/sda"; + content = { + type = "gpt"; + partitions = { + ESP = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; + }; + }; +} diff --git a/hosts/telefonmann/hardware.nix b/hosts/telefonmann/hardware.nix new file mode 100644 index 0000000..75eafc4 --- /dev/null +++ b/hosts/telefonmann/hardware.nix @@ -0,0 +1,10 @@ +{ modulesPath, ... }: { + imports = [ (modulesPath + "/profiles/qemu-guest.nix") ]; + + boot.initrd.availableKernelModules = [ + "virtio_pci" + "virtio_scsi" # use "virtio_blk" instead if disk is /dev/vda + "ahci" + "sd_mod" + ]; +} diff --git a/modules/common.nix b/modules/common.nix new file mode 100644 index 0000000..3f7c36b --- /dev/null +++ b/modules/common.nix @@ -0,0 +1,19 @@ +{ pkgs, ... }: { + time.timeZone = "Europe/Berlin"; + + nix.settings = { + experimental-features = [ "nix-command" "flakes" ]; + trusted-users = [ "root" ]; + }; + + services.openssh = { + enable = true; + settings.PermitRootLogin = "prohibit-password"; + }; + + users.users.root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH7v2e1uLxfqu7zuWLgUdsxE+fBxkjxYNuwhfKduO34U offis\\jbruhn@it1002077" + ]; + + system.stateVersion = "25.11"; +} diff --git a/modules/vm-guest.nix b/modules/vm-guest.nix new file mode 100644 index 0000000..108913f --- /dev/null +++ b/modules/vm-guest.nix @@ -0,0 +1,7 @@ +{ ... }: { + services.qemuGuest.enable = true; + boot.loader = { + systemd-boot.enable = true; + efi.canTouchEfiVariables = true; + }; +} diff --git a/modules/voip/asterisk.nix b/modules/voip/asterisk.nix new file mode 100644 index 0000000..949546e --- /dev/null +++ b/modules/voip/asterisk.nix @@ -0,0 +1,140 @@ +{ lib, cfg, models, intercomEntries }: + +let + # Phones that have voicemail enabled + vmPhones = lib.filterAttrs (_: phone: phone.voicemailTimeout != null) cfg.phones; + hasVoicemail = vmPhones != {}; + + pjsip = '' + [transport-tcp] + type = transport + protocol = tcp + bind = 0.0.0.0:${toString cfg.sipPort} + [transport-udp] + type = transport + protocol = udp + bind = 0.0.0.0:${toString cfg.sipPort} + ; --- templates --- + + [endpoint-cisco-8961](!) + type = endpoint + context = internal + transport = transport-tcp + disallow = all + allow = ulaw + allow = alaw + allow = g722 + allow = g726 + allow = ilbc + allow = gsm + direct_media = no + trust_id_inbound = yes + + [endpoint-generic](!) + type = endpoint + context = internal + transport = transport-tcp + disallow = all + allow = ulaw + allow = alaw + allow = g722 + direct_media = no + + [auth-userpass](!) + type = auth + auth_type = userpass + + ; --- phones --- + + '' + lib.concatStringsSep "\n" (lib.mapAttrsToList (key: phone: + let m = models.${phone.model}; in '' + [${key}](${m.endpointTemplate}) + auth = auth-${key} + aors = ${key} + ${lib.optionalString (phone.voicemailTimeout != null) "mailboxes = ${phone.extension}@voicemail\n set_var=VOICEMAIL_MAILBOX=${phone.extension}"} + + [auth-${key}](auth-userpass) + username = ${key} + password = ${phone.password} + + [${key}] + type = aor + max_contacts = ${toString m.maxContacts} + remove_existing = yes + '') cfg.phones) + + + lib.concatMapStringsSep "\n" (ic: '' + + [${ic.endpoint}](${ic.endpointTemplate}) + auth = auth-${ic.endpoint} + aors = ${ic.endpoint} + + [auth-${ic.endpoint}](auth-userpass) + username = ${ic.endpoint} + password = ${ic.password} + + [${ic.endpoint}] + type = aor + max_contacts = ${toString ic.maxContacts} + remove_existing = yes + '') intercomEntries; + + # Reverse map: extension number -> pjsip endpoint key + extensionToEndpoint = lib.foldlAttrs (acc: key: phone: + acc // { ${phone.extension} = key; } + ) {} cfg.phones; + + # All page endpoints: intercom line for provisioned phones, regular for others + allPageEndpoints = lib.concatStringsSep "&" (lib.mapAttrsToList (key: phone: + let m = models.${phone.model}; in + if m.hasProvisioning && cfg.intercomPrefix != null + then "PJSIP/${key}-intercom" + else "PJSIP/${key}" + ) cfg.phones); + + extensions = '' + [internal] + '' + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: extCfg: + if extCfg.mode == "app" + then "exten => ${ext},1,${extCfg.app}" + else if extCfg.mode == "page" + then "exten => ${ext},1,Page(${allPageEndpoints},i,120)" + else if lib.hasAttr ext extensionToEndpoint + then + let + phoneKey = extensionToEndpoint.${ext}; + phone = cfg.phones.${phoneKey}; + in if phone.voicemailTimeout != null + then ''exten => ${ext},1,Dial(PJSIP/${phoneKey},${toString phone.voicemailTimeout}) + same => n,VoiceMail(${ext}@voicemail,u)'' + else "exten => ${ext},1,Dial(PJSIP/${phoneKey})" + else "exten => ${ext},1,Hangup() ; WARNING: no endpoint assigned to extension ${ext}" + ) cfg.extensions) + + "\n" + lib.concatMapStringsSep "\n" (ic: + "exten => ${ic.extension},1,Dial(PJSIP/${ic.endpoint},30)" + ) intercomEntries + + lib.optionalString hasVoicemail "\nexten => *97,1,VoiceMailMain(\${VOICEMAIL_MAILBOX}@voicemail,sa(0))"; + + rtp = '' + [general] + rtpstart = ${toString cfg.rtpStart} + rtpend = ${toString cfg.rtpEnd} + ''; + + voicemail = '' + [general] + format = ulaw + + [voicemail] + '' + lib.concatStringsSep "\n" (lib.mapAttrsToList (_: phone: + let displayName = cfg.extensions.${phone.extension}.displayName; in + " ${phone.extension} => ,${displayName},,attach=no" + ) vmPhones); + +in { + "pjsip.conf" = pjsip; + "extensions.conf" = extensions; + "rtp.conf" = rtp; +} // lib.optionalAttrs hasVoicemail { + "voicemail.conf" = voicemail; +} diff --git a/modules/voip/backgrounds.nix b/modules/voip/backgrounds.nix new file mode 100644 index 0000000..1435234 --- /dev/null +++ b/modules/voip/backgrounds.nix @@ -0,0 +1,46 @@ +{ lib, pkgs, cfg, models }: + +let + # Collect unique (desktopSize, thumbnailSize) pairs from provisioned phone models + sizeConfigs = lib.unique (lib.filter (s: s.desktop != null) + (lib.mapAttrsToList (_: phone: + let m = models.${phone.model}; in + { desktop = m.desktopSize; thumbnail = m.thumbnailSize; } + ) cfg.phones)); + + # Parse "WxH" or "WxHxD" into width and height + parseDimensions = size: + let parts = lib.splitString "x" size; + in { w = lib.elemAt parts 0; h = lib.elemAt parts 1; }; + + # Build a resized image derivation + resizeImage = { src, width, height, name }: + pkgs.runCommand name { nativeBuildInputs = [ pkgs.imagemagick ]; } '' + convert "${src}" -resize ${width}x${height}! -strip PNG24:$out + ''; + +in lib.concatMap (sc: + let + size = sc.desktop; + dim = parseDimensions size; + tn = parseDimensions sc.thumbnail; + listXml = '' + + '' + lib.concatStringsSep "\n" (lib.imap1 (i: name: '' + + '') (lib.attrNames cfg.backgroundImages)) + + '' + + ''; + listFile = pkgs.writeText "List.xml" listXml; + in + [{ name = "Desktops/${size}/List.xml"; path = listFile; }] + ++ lib.concatLists (lib.imap1 (i: name: + let src = cfg.backgroundImages.${name}; in [ + { name = "Desktops/${size}/bg-${toString i}.png"; + path = resizeImage { inherit src; width = dim.w; height = dim.h; name = "bg-${toString i}.png"; }; } + { name = "Desktops/${size}/tn-${toString i}.png"; + path = resizeImage { inherit src; width = tn.w; height = tn.h; name = "tn-${toString i}.png"; }; } + ] + ) (lib.attrNames cfg.backgroundImages)) +) sizeConfigs diff --git a/modules/voip/default.nix b/modules/voip/default.nix new file mode 100644 index 0000000..59cd73b --- /dev/null +++ b/modules/voip/default.nix @@ -0,0 +1,244 @@ +{ lib, pkgs, config, ... }: + +let + cfg = config.services.voip; + + # Per-model config. Adding a new hardware model: + # 1. Add an entry here with all required fields + # 2. For provisioned models, add a template in ./templates/.nix + # + # Template interface — all provisioned model templates receive these args: + # Required: mac, label, password, displayName, serverAddress, ntpServer + # Optional: sipPort (default 5060), directoryPort (default 8080), + # intercomEnabled (default false), intercomPassword (default "") + models = { + "cisco-8961" = { + endpointTemplate = "endpoint-cisco-8961"; + maxContacts = 1; + hasProvisioning = true; + desktopSize = "640x480x24"; + thumbnailSize = "123x111"; + template = import ./templates/cisco-8961.nix; + }; + "sip-client" = { + endpointTemplate = "endpoint-generic"; + maxContacts = 1; + hasProvisioning = false; + desktopSize = null; + thumbnailSize = null; + template = null; + }; + }; + + intercomEntries = import ./intercom.nix { inherit lib cfg models; }; + confFiles = import ./asterisk.nix { inherit lib cfg models intercomEntries; }; + directory = import ./directory.nix { inherit lib pkgs cfg intercomEntries; }; + backgroundEntries = import ./backgrounds.nix { inherit lib pkgs cfg models; }; + tftpRoot = import ./tftp.nix { inherit lib pkgs cfg models backgroundEntries; }; + +in { + options.services.voip = { + enable = lib.mkEnableOption "VoIP provisioning (Asterisk + TFTP)"; + + serverAddress = lib.mkOption { + type = lib.types.str; + description = "IP address or hostname of this server (used in phone configs and SIP)."; + }; + + ntpServer = lib.mkOption { + type = lib.types.str; + description = "NTP server for phones. Defaults to serverAddress."; + default = ""; + }; + + sipPort = lib.mkOption { + type = lib.types.port; + default = 5060; + }; + + rtpStart = lib.mkOption { + type = lib.types.port; + default = 10000; + }; + + rtpEnd = lib.mkOption { + type = lib.types.port; + default = 20000; + }; + + directoryName = lib.mkOption { + type = lib.types.str; + default = "tel.baubs.net"; + description = "Name shown in the phone directory title."; + }; + + directoryPort = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "HTTP port for the phone directory and services."; + }; + + backgroundImages = lib.mkOption { + default = {}; + description = '' + Attrset of background images keyed by display name. + Value is a path to any image file — it will be resized automatically + to the correct dimensions for each phone model during build. + For best results, use a 4:3 aspect ratio source image. + ''; + type = lib.types.attrsOf lib.types.path; + }; + + phones = lib.mkOption { + default = {}; + description = '' + Attrset of phones/clients keyed by SIP identity (username). + For hardware phones (cisco-8961), the key must be the lowercase MAC address (no colons). + For sip-client, the key is a free-form username. + ''; + type = lib.types.attrsOf (lib.types.submodule { + options = { + model = lib.mkOption { + type = lib.types.enum (lib.attrNames models); + description = "Phone model. Use \"sip-client\" for software SIP clients (no provisioning file)."; + }; + extension = lib.mkOption { + type = lib.types.str; + description = "Extension number this phone registers as."; + }; + label = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Label shown on the phone screen. Required for provisioned hardware phones."; + }; + password = lib.mkOption { + type = lib.types.str; + description = "SIP registration password."; + }; + voicemailTimeout = lib.mkOption { + type = lib.types.nullOr lib.types.ints.positive; + default = null; + description = "Seconds to ring before sending to voicemail. null disables voicemail for this phone."; + }; + }; + }); + }; + + intercomPrefix = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Dial prefix for auto-generated intercom extensions. e.g. \"*80\" generates *80100 for ext 100. Only intercom-capable (provisioned) phones get entries."; + }; + + extensions = lib.mkOption { + default = {}; + description = "Attrset of extensions keyed by extension number."; + type = lib.types.attrsOf (lib.types.submodule { + options = { + mode = lib.mkOption { + type = lib.types.enum [ "line" "page" "app" ]; + default = "line"; + description = '' + Extension mode: + - "line": dials the phone assigned to this extension + - "page": one-way announcement to all phones + - "app": custom Asterisk dialplan application + Intercom extensions are auto-generated when intercomPrefix is set. + ''; + }; + displayName = lib.mkOption { + type = lib.types.str; + default = ""; + }; + app = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Verbatim Asterisk dialplan app. Required for mode = \"app\"."; + }; + }; + }); + }; + }; + + config = lib.mkIf cfg.enable { + + services.voip.ntpServer = lib.mkDefault cfg.serverAddress; + + + assertions = + # Every phone's extension must be declared + (lib.mapAttrsToList (key: phone: { + assertion = lib.hasAttr phone.extension cfg.extensions; + message = "services.voip: phone \"${key}\" references extension ${phone.extension} which is not declared in services.voip.extensions"; + }) cfg.phones) + ++ + # Provisioned phones require a MAC address key + (lib.mapAttrsToList (key: phone: { + assertion = !models.${phone.model}.hasProvisioning || builtins.match "[0-9a-f]{12}" key != null; + message = "services.voip: phone \"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key"; + }) cfg.phones) + ++ + # Provisioned phones require a non-empty label + (lib.mapAttrsToList (key: phone: { + assertion = !models.${phone.model}.hasProvisioning || phone.label != ""; + message = "services.voip: phone \"${key}\" (model ${phone.model}) requires a non-empty label"; + }) cfg.phones) + ++ + # Provisioned phones require a template + (lib.mapAttrsToList (key: phone: { + assertion = !models.${phone.model}.hasProvisioning || models.${phone.model}.template != null; + message = "services.voip: phone \"${key}\" model \"${phone.model}\" has hasProvisioning=true but no template defined"; + }) cfg.phones) + ++ + # intercomPrefix must not collide with user-declared extensions + (lib.optionals (cfg.intercomPrefix != null) + (lib.mapAttrsToList (key: phone: + let ext = "${cfg.intercomPrefix}${phone.extension}"; in { + assertion = !lib.hasAttr ext cfg.extensions; + message = "services.voip: auto-generated intercom extension \"${ext}\" collides with a declared extension; rename it or change intercomPrefix"; + } + ) (lib.filterAttrs (_: phone: models.${phone.model}.hasProvisioning) cfg.phones))); + + services.asterisk = { + enable = true; + confFiles = confFiles; + }; + + services.atftpd = { + enable = true; + root = "${tftpRoot}"; + extraOptions = [ "--verbose=7" ]; + }; + + services.nginx = { + enable = true; + virtualHosts."voip-directory" = { + listen = [{ addr = "0.0.0.0"; port = cfg.directoryPort; }]; + locations."= /directory.xml" = { + alias = "${directory.menuFile}"; + extraConfig = "default_type text/xml;"; + }; + locations."= /directory-list.xml" = { + alias = "${directory.listFile}"; + extraConfig = "default_type text/xml;"; + }; + locations."= /intercom.xml" = { + alias = "${directory.intercomFile}"; + extraConfig = "default_type text/xml;"; + }; + locations."= /voicemail.xml" = { + alias = "${directory.voicemailFile}"; + extraConfig = "default_type text/xml;"; + }; + }; + }; + + systemd.tmpfiles.rules = [ "d /var/log/nginx 0750 nginx nginx -" ]; + + networking.firewall = { + allowedTCPPorts = [ cfg.sipPort cfg.directoryPort ]; + allowedUDPPorts = [ 69 ]; + allowedUDPPortRanges = [{ from = cfg.rtpStart; to = cfg.rtpEnd; }]; + }; + }; +} diff --git a/modules/voip/directory.nix b/modules/voip/directory.nix new file mode 100644 index 0000000..4a84463 --- /dev/null +++ b/modules/voip/directory.nix @@ -0,0 +1,72 @@ +{ lib, pkgs, cfg, intercomEntries }: + +let + baseUrl = "http://${cfg.serverAddress}:${toString cfg.directoryPort}"; + + hasPageExtensions = lib.any (e: e.mode == "page") (lib.attrValues cfg.extensions); + + menuXml = '' + + ${cfg.directoryName} Telefonbuch + Ihre Wahl + + Internes Telefonbuch + ${baseUrl}/directory-list.xml + + '' + lib.optionalString (intercomEntries != [] || hasPageExtensions) '' + + Intercom / Durchsage + ${baseUrl}/intercom.xml + + '' + '' + + ''; + + listXml = '' + + ${cfg.directoryName} Telefonbuch + Ihre Wahl + '' + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: extCfg: + lib.optionalString (extCfg.mode == "line" && extCfg.displayName != "") '' + + ${extCfg.displayName} + ${ext} + + '') cfg.extensions) + + '' + + ''; + + intercomXml = '' + + Intercom / Durchsage + Ihre Wahl + '' + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: extCfg: + lib.optionalString (extCfg.mode == "page" && extCfg.displayName != "") '' + + ${extCfg.displayName} + ${ext} + + '') cfg.extensions) + + lib.concatMapStringsSep "\n" (ic: '' + + ${ic.displayName} + ${ic.extension} + + '') intercomEntries + + '' + + ''; + + voicemailMenuXml = '' + + + + ''; + +in { + menuFile = pkgs.writeText "directory.xml" menuXml; + listFile = pkgs.writeText "directory-list.xml" listXml; + intercomFile = pkgs.writeText "intercom.xml" intercomXml; + voicemailFile = pkgs.writeText "voicemail.xml" voicemailMenuXml; +} diff --git a/modules/voip/intercom.nix b/modules/voip/intercom.nix new file mode 100644 index 0000000..abf19e1 --- /dev/null +++ b/modules/voip/intercom.nix @@ -0,0 +1,20 @@ +{ lib, cfg, models }: + +if cfg.intercomPrefix == null then [] +else lib.concatLists (lib.mapAttrsToList (key: phone: + let + m = models.${phone.model}; + ext = phone.extension; + # cfg.extensions.${ext} is guaranteed to exist by the phone→extension assertion + extCfg = cfg.extensions.${ext}; + in lib.optional m.hasProvisioning { + extension = "${cfg.intercomPrefix}${ext}"; + endpoint = "${key}-intercom"; + phoneKey = key; + target = ext; + displayName = "Intercom ${extCfg.displayName}"; + password = phone.password; + endpointTemplate = m.endpointTemplate; + maxContacts = m.maxContacts; + } +) cfg.phones) diff --git a/modules/voip/templates/cisco-8961.nix b/modules/voip/templates/cisco-8961.nix new file mode 100644 index 0000000..2e7dda1 --- /dev/null +++ b/modules/voip/templates/cisco-8961.nix @@ -0,0 +1,172 @@ +{ mac, label, displayName, password, serverAddress, ntpServer, sipPort ? 5060, directoryPort ? 8080, intercomEnabled ? false, intercomPassword ? "" }: +'' + + SIP + admin + password + + + D.M.YA + Central Europe Standard/Daylight Time + + + ${ntpServer} + + + + + + + + + ${toString sipPort} + + ${serverAddress} + + + + + + + + + 5060 + + + + + true + + + true + x-serviceuri-cfwdall + x-cisco-serviceuri-pickup + x-cisco-serviceuri-opickup + x-cisco-serviceuri-gpickup + x-cisco-serviceuri-meetme + x-cisco-serviceuri-abbrdial + 2 + 2 + 2 + 0 + true + + + 6 + 10 + 180 + 3600 + 5 + 120 + 120 + 5 + 500 + 4000 + 70 + false + None + + false + 3 + ${builtins.substring 0 12 label} + 1 + false + + + 9 + ${displayName} + USECALLMANAGER + ${toString sipPort} + ${mac} + ${displayName} + + 2 + + 3 + ${mac} + ${password} + 1 + *97 + ${mac} + + true + true + true + true + + +${if intercomEnabled then '' + + 23 + Intercom + USECALLMANAGER + ${toString sipPort} + ${mac}-intercom + Intercom + + 3 + Auto Answer with Speakerphone + + 3 + ${mac}-intercom + ${intercomPassword} + 1 + 1 + ${mac}-intercom + +'' else ""} + ${toString sipPort} + 16348 + 20134 + 184 + dialplan.xml + + + + true + 2 + + sip8961.9-4-2ES-14 + + 0 + 1 + 0 + 1 + 1,2,3,4,5,6,7 + 0 + 00:00 + 00:00 + 00:05 + 1 + + + en_US + utf-8 + + 1 + + http://${serverAddress}:${toString directoryPort}/directory.xml + + + + + + 2 + + Voicemail + Application:Cisco/Voicemail + + + + + + 1 + 0 + + + 3804 + + + false + +'' diff --git a/modules/voip/tftp.nix b/modules/voip/tftp.nix new file mode 100644 index 0000000..52449c6 --- /dev/null +++ b/modules/voip/tftp.nix @@ -0,0 +1,29 @@ +{ lib, pkgs, cfg, models, backgroundEntries }: + +pkgs.linkFarm "voip-tftp-root" ( + lib.concatLists (lib.mapAttrsToList (key: phone: + let m = models.${phone.model}; in + lib.optional m.hasProvisioning ( + let + upperKey = lib.toUpper key; + ext = cfg.extensions.${phone.extension}; + xml = m.template ({ + mac = key; + inherit (phone) label password; + displayName = ext.displayName; + serverAddress = cfg.serverAddress; + ntpServer = cfg.ntpServer; + sipPort = cfg.sipPort; + directoryPort = cfg.directoryPort; + } // lib.optionalAttrs (cfg.intercomPrefix != null) { + intercomEnabled = true; + intercomPassword = phone.password; + }); + in { + name = "SEP${upperKey}.cnf.xml"; + path = pkgs.writeText "SEP${upperKey}.cnf.xml" xml; + } + ) + ) cfg.phones) + ++ backgroundEntries +)