initial commit§
This commit is contained in:
commit
aa22874883
15 changed files with 954 additions and 0 deletions
48
flake.lock
Normal file
48
flake.lock
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
51
flake.nix
Normal file
51
flake.nix
Normal file
|
|
@ -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/<name>/
|
||||||
|
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/<name>/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;
|
||||||
|
};
|
||||||
|
}
|
||||||
BIN
hosts/telefonmann/backgrounds/wombel.png
Normal file
BIN
hosts/telefonmann/backgrounds/wombel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
61
hosts/telefonmann/default.nix
Normal file
61
hosts/telefonmann/default.nix
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
35
hosts/telefonmann/disko.nix
Normal file
35
hosts/telefonmann/disko.nix
Normal file
|
|
@ -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 = "/";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
10
hosts/telefonmann/hardware.nix
Normal file
10
hosts/telefonmann/hardware.nix
Normal file
|
|
@ -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"
|
||||||
|
];
|
||||||
|
}
|
||||||
19
modules/common.nix
Normal file
19
modules/common.nix
Normal file
|
|
@ -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";
|
||||||
|
}
|
||||||
7
modules/vm-guest.nix
Normal file
7
modules/vm-guest.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{ ... }: {
|
||||||
|
services.qemuGuest.enable = true;
|
||||||
|
boot.loader = {
|
||||||
|
systemd-boot.enable = true;
|
||||||
|
efi.canTouchEfiVariables = true;
|
||||||
|
};
|
||||||
|
}
|
||||||
140
modules/voip/asterisk.nix
Normal file
140
modules/voip/asterisk.nix
Normal file
|
|
@ -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;
|
||||||
|
}
|
||||||
46
modules/voip/backgrounds.nix
Normal file
46
modules/voip/backgrounds.nix
Normal file
|
|
@ -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 = ''
|
||||||
|
<CiscoIPPhoneImageList>
|
||||||
|
'' + lib.concatStringsSep "\n" (lib.imap1 (i: name: ''
|
||||||
|
<ImageItem Image="TFTP:Desktops/${size}/tn-${toString i}.png" URL="TFTP:Desktops/${size}/bg-${toString i}.png"/>
|
||||||
|
'') (lib.attrNames cfg.backgroundImages))
|
||||||
|
+ ''
|
||||||
|
</CiscoIPPhoneImageList>
|
||||||
|
'';
|
||||||
|
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
|
||||||
244
modules/voip/default.nix
Normal file
244
modules/voip/default.nix
Normal file
|
|
@ -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/<model>.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; }];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
72
modules/voip/directory.nix
Normal file
72
modules/voip/directory.nix
Normal file
|
|
@ -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 = ''
|
||||||
|
<CiscoIPPhoneMenu>
|
||||||
|
<Title>${cfg.directoryName} Telefonbuch</Title>
|
||||||
|
<Prompt>Ihre Wahl</Prompt>
|
||||||
|
<MenuItem>
|
||||||
|
<Name>Internes Telefonbuch</Name>
|
||||||
|
<URL>${baseUrl}/directory-list.xml</URL>
|
||||||
|
</MenuItem>
|
||||||
|
'' + lib.optionalString (intercomEntries != [] || hasPageExtensions) ''
|
||||||
|
<MenuItem>
|
||||||
|
<Name>Intercom / Durchsage</Name>
|
||||||
|
<URL>${baseUrl}/intercom.xml</URL>
|
||||||
|
</MenuItem>
|
||||||
|
'' + ''
|
||||||
|
</CiscoIPPhoneMenu>
|
||||||
|
'';
|
||||||
|
|
||||||
|
listXml = ''
|
||||||
|
<CiscoIPPhoneDirectory>
|
||||||
|
<Title>${cfg.directoryName} Telefonbuch</Title>
|
||||||
|
<Prompt>Ihre Wahl</Prompt>
|
||||||
|
'' + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: extCfg:
|
||||||
|
lib.optionalString (extCfg.mode == "line" && extCfg.displayName != "") ''
|
||||||
|
<DirectoryEntry>
|
||||||
|
<Name>${extCfg.displayName}</Name>
|
||||||
|
<Telephone>${ext}</Telephone>
|
||||||
|
</DirectoryEntry>
|
||||||
|
'') cfg.extensions)
|
||||||
|
+ ''
|
||||||
|
</CiscoIPPhoneDirectory>
|
||||||
|
'';
|
||||||
|
|
||||||
|
intercomXml = ''
|
||||||
|
<CiscoIPPhoneDirectory>
|
||||||
|
<Title>Intercom / Durchsage</Title>
|
||||||
|
<Prompt>Ihre Wahl</Prompt>
|
||||||
|
'' + lib.concatStringsSep "\n" (lib.mapAttrsToList (ext: extCfg:
|
||||||
|
lib.optionalString (extCfg.mode == "page" && extCfg.displayName != "") ''
|
||||||
|
<DirectoryEntry>
|
||||||
|
<Name>${extCfg.displayName}</Name>
|
||||||
|
<Telephone>${ext}</Telephone>
|
||||||
|
</DirectoryEntry>
|
||||||
|
'') cfg.extensions)
|
||||||
|
+ lib.concatMapStringsSep "\n" (ic: ''
|
||||||
|
<DirectoryEntry>
|
||||||
|
<Name>${ic.displayName}</Name>
|
||||||
|
<Telephone>${ic.extension}</Telephone>
|
||||||
|
</DirectoryEntry>
|
||||||
|
'') intercomEntries
|
||||||
|
+ ''
|
||||||
|
</CiscoIPPhoneDirectory>
|
||||||
|
'';
|
||||||
|
|
||||||
|
voicemailMenuXml = ''
|
||||||
|
<CiscoIPPhoneExecute>
|
||||||
|
<ExecuteItem Priority="0" URL="Dial:997"/>
|
||||||
|
</CiscoIPPhoneExecute>
|
||||||
|
'';
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
20
modules/voip/intercom.nix
Normal file
20
modules/voip/intercom.nix
Normal file
|
|
@ -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)
|
||||||
172
modules/voip/templates/cisco-8961.nix
Normal file
172
modules/voip/templates/cisco-8961.nix
Normal file
|
|
@ -0,0 +1,172 @@
|
||||||
|
{ mac, label, displayName, password, serverAddress, ntpServer, sipPort ? 5060, directoryPort ? 8080, intercomEnabled ? false, intercomPassword ? "" }:
|
||||||
|
''
|
||||||
|
<device>
|
||||||
|
<deviceProtocol>SIP</deviceProtocol>
|
||||||
|
<sshUserId>admin</sshUserId>
|
||||||
|
<sshPassword>password</sshPassword>
|
||||||
|
<devicePool>
|
||||||
|
<dateTimeSetting>
|
||||||
|
<dateTemplate>D.M.YA</dateTemplate>
|
||||||
|
<timeZone>Central Europe Standard/Daylight Time</timeZone>
|
||||||
|
<ntps>
|
||||||
|
<ntp>
|
||||||
|
<name>${ntpServer}</name>
|
||||||
|
</ntp>
|
||||||
|
</ntps>
|
||||||
|
</dateTimeSetting>
|
||||||
|
<callManagerGroup>
|
||||||
|
<members>
|
||||||
|
<member priority="0">
|
||||||
|
<callManager>
|
||||||
|
<ports>
|
||||||
|
<sipPort>${toString sipPort}</sipPort>
|
||||||
|
</ports>
|
||||||
|
<processNodeName>${serverAddress}</processNodeName>
|
||||||
|
</callManager>
|
||||||
|
</member>
|
||||||
|
</members>
|
||||||
|
</callManagerGroup>
|
||||||
|
</devicePool>
|
||||||
|
<sipProfile>
|
||||||
|
<sipProxies>
|
||||||
|
<backupProxy></backupProxy>
|
||||||
|
<backupProxyPort>5060</backupProxyPort>
|
||||||
|
<emergencyProxy></emergencyProxy>
|
||||||
|
<emergencyProxyPort></emergencyProxyPort>
|
||||||
|
<outboundProxy></outboundProxy>
|
||||||
|
<outboundProxyPort></outboundProxyPort>
|
||||||
|
<registerWithProxy>true</registerWithProxy>
|
||||||
|
</sipProxies>
|
||||||
|
<sipCallFeatures>
|
||||||
|
<cnfJoinEnabled>true</cnfJoinEnabled>
|
||||||
|
<callForwardURI>x-serviceuri-cfwdall</callForwardURI>
|
||||||
|
<callPickupURI>x-cisco-serviceuri-pickup</callPickupURI>
|
||||||
|
<callPickupListURI>x-cisco-serviceuri-opickup</callPickupListURI>
|
||||||
|
<callPickupGroupURI>x-cisco-serviceuri-gpickup</callPickupGroupURI>
|
||||||
|
<meetMeServiceURI>x-cisco-serviceuri-meetme</meetMeServiceURI>
|
||||||
|
<abbreviatedDialURI>x-cisco-serviceuri-abbrdial</abbreviatedDialURI>
|
||||||
|
<callHoldRingback>2</callHoldRingback>
|
||||||
|
<anonymousCallBlock>2</anonymousCallBlock>
|
||||||
|
<callerIdBlocking>2</callerIdBlocking>
|
||||||
|
<dndControl>0</dndControl>
|
||||||
|
<remoteCcEnable>true</remoteCcEnable>
|
||||||
|
</sipCallFeatures>
|
||||||
|
<sipStack>
|
||||||
|
<sipInviteRetx>6</sipInviteRetx>
|
||||||
|
<sipRetx>10</sipRetx>
|
||||||
|
<timerInviteExpires>180</timerInviteExpires>
|
||||||
|
<timerRegisterExpires>3600</timerRegisterExpires>
|
||||||
|
<timerRegisterDelta>5</timerRegisterDelta>
|
||||||
|
<timerKeepAliveExpires>120</timerKeepAliveExpires>
|
||||||
|
<timerSubscribeExpires>120</timerSubscribeExpires>
|
||||||
|
<timerSubscribeDelta>5</timerSubscribeDelta>
|
||||||
|
<timerT1>500</timerT1>
|
||||||
|
<timerT2>4000</timerT2>
|
||||||
|
<maxRedirects>70</maxRedirects>
|
||||||
|
<remotePartyID>false</remotePartyID>
|
||||||
|
<userInfo>None</userInfo>
|
||||||
|
</sipStack>
|
||||||
|
<transferOnhookEnabled>false</transferOnhookEnabled>
|
||||||
|
<kpml>3</kpml>
|
||||||
|
<phoneLabel>${builtins.substring 0 12 label}</phoneLabel>
|
||||||
|
<stutterMsgWaiting>1</stutterMsgWaiting>
|
||||||
|
<callStats>false</callStats>
|
||||||
|
<sipLines>
|
||||||
|
<line button="1" lineIndex="1">
|
||||||
|
<featureID>9</featureID>
|
||||||
|
<featureLabel>${displayName}</featureLabel>
|
||||||
|
<proxy>USECALLMANAGER</proxy>
|
||||||
|
<port>${toString sipPort}</port>
|
||||||
|
<name>${mac}</name>
|
||||||
|
<displayName>${displayName}</displayName>
|
||||||
|
<autoAnswer>
|
||||||
|
<autoAnswerEnabled>2</autoAnswerEnabled>
|
||||||
|
</autoAnswer>
|
||||||
|
<callWaiting>3</callWaiting>
|
||||||
|
<authName>${mac}</authName>
|
||||||
|
<authPassword>${password}</authPassword>
|
||||||
|
<messageWaitingLampPolicy>1</messageWaitingLampPolicy>
|
||||||
|
<messagesNumber>*97</messagesNumber>
|
||||||
|
<contact>${mac}</contact>
|
||||||
|
<forwardCallInfoDisplay>
|
||||||
|
<callerName>true</callerName>
|
||||||
|
<callerNumber>true</callerNumber>
|
||||||
|
<redirectedNumber>true</redirectedNumber>
|
||||||
|
<dialedNumber>true</dialedNumber>
|
||||||
|
</forwardCallInfoDisplay>
|
||||||
|
</line>
|
||||||
|
${if intercomEnabled then ''
|
||||||
|
<line button="2" lineIndex="2">
|
||||||
|
<featureID>23</featureID>
|
||||||
|
<featureLabel>Intercom</featureLabel>
|
||||||
|
<proxy>USECALLMANAGER</proxy>
|
||||||
|
<port>${toString sipPort}</port>
|
||||||
|
<name>${mac}-intercom</name>
|
||||||
|
<displayName>Intercom</displayName>
|
||||||
|
<autoAnswer>
|
||||||
|
<autoAnswerEnabled>3</autoAnswerEnabled>
|
||||||
|
<autoAnswerMode>Auto Answer with Speakerphone</autoAnswerMode>
|
||||||
|
</autoAnswer>
|
||||||
|
<callWaiting>3</callWaiting>
|
||||||
|
<authName>${mac}-intercom</authName>
|
||||||
|
<authPassword>${intercomPassword}</authPassword>
|
||||||
|
<maxNumCalls>1</maxNumCalls>
|
||||||
|
<busyTrigger>1</busyTrigger>
|
||||||
|
<contact>${mac}-intercom</contact>
|
||||||
|
</line>
|
||||||
|
'' else ""} </sipLines>
|
||||||
|
<voipControlPort>${toString sipPort}</voipControlPort>
|
||||||
|
<startMediaPort>16348</startMediaPort>
|
||||||
|
<stopMediaPort>20134</stopMediaPort>
|
||||||
|
<dscpForAudio>184</dscpForAudio>
|
||||||
|
<dialTemplate>dialplan.xml</dialTemplate>
|
||||||
|
</sipProfile>
|
||||||
|
<commonProfile>
|
||||||
|
<phonePassword></phonePassword>
|
||||||
|
<backgroundImageAccess>true</backgroundImageAccess>
|
||||||
|
<callLogBlfEnabled>2</callLogBlfEnabled>
|
||||||
|
</commonProfile>
|
||||||
|
<loadInformation>sip8961.9-4-2ES-14</loadInformation>
|
||||||
|
<vendorConfig>
|
||||||
|
<webAccess>0</webAccess>
|
||||||
|
<settingsAccess>1</settingsAccess>
|
||||||
|
<autoSelectLineEnable>0</autoSelectLineEnable>
|
||||||
|
<loggingDisplay>1</loggingDisplay>
|
||||||
|
<daysDisplayNotActive>1,2,3,4,5,6,7</daysDisplayNotActive>
|
||||||
|
<sshAccess>0</sshAccess>
|
||||||
|
<displayOnTime>00:00</displayOnTime>
|
||||||
|
<displayOnDuration>00:00</displayOnDuration>
|
||||||
|
<displayIdleTimeout>00:05</displayIdleTimeout>
|
||||||
|
<displayOnWhenIncomingCall>1</displayOnWhenIncomingCall>
|
||||||
|
</vendorConfig>
|
||||||
|
<userLocale>
|
||||||
|
<langCode>en_US</langCode>
|
||||||
|
<winCharSet>utf-8</winCharSet>
|
||||||
|
</userLocale>
|
||||||
|
<deviceSecurityMode>1</deviceSecurityMode>
|
||||||
|
<authenticationURL></authenticationURL>
|
||||||
|
<directoryURL>http://${serverAddress}:${toString directoryPort}/directory.xml</directoryURL>
|
||||||
|
<messagesURL />
|
||||||
|
<servicesURL></servicesURL>
|
||||||
|
<idleURL></idleURL>
|
||||||
|
<informationURL></informationURL>
|
||||||
|
<phoneServices useHTTPS="false">
|
||||||
|
<provisioning>2</provisioning>
|
||||||
|
<phoneService type="2" category="0">
|
||||||
|
<name>Voicemail</name>
|
||||||
|
<url>Application:Cisco/Voicemail</url>
|
||||||
|
<vendor></vendor>
|
||||||
|
<version></version>
|
||||||
|
</phoneService>
|
||||||
|
</phoneServices>
|
||||||
|
<proxyServerURL></proxyServerURL>
|
||||||
|
<transportLayerProtocol>1</transportLayerProtocol>
|
||||||
|
<capfAuthMode>0</capfAuthMode>
|
||||||
|
<capfList>
|
||||||
|
<capf>
|
||||||
|
<phonePort>3804</phonePort>
|
||||||
|
</capf>
|
||||||
|
</capfList>
|
||||||
|
<encrConfig>false</encrConfig>
|
||||||
|
</device>
|
||||||
|
''
|
||||||
29
modules/voip/tftp.nix
Normal file
29
modules/voip/tftp.nix
Normal file
|
|
@ -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
|
||||||
|
)
|
||||||
Loading…
Reference in a new issue