137 lines
6.3 KiB
Nix
137 lines
6.3 KiB
Nix
{ lib, cfg, models, allPhones }:
|
|
|
|
# Provisioned sharedPhones 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: sharedPhone \"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key";
|
|
}) cfg.sharedPhones)
|
|
++
|
|
# Provisioned sharedPhones require a non-empty label
|
|
(lib.mapAttrsToList (key: phone: {
|
|
assertion = !models.${phone.model}.hasProvisioning || phone.label != "";
|
|
message = "services.voip: sharedPhone \"${key}\" (model ${phone.model}) requires a non-empty label";
|
|
}) cfg.sharedPhones)
|
|
++
|
|
# Provisioned person phones require a MAC address key
|
|
(lib.concatLists (lib.mapAttrsToList (personKey: person:
|
|
lib.mapAttrsToList (key: phone: {
|
|
assertion = !models.${phone.model}.hasProvisioning || builtins.match "[0-9a-f]{12}" key != null;
|
|
message = "services.voip: persons.${personKey}.phones.\"${key}\" (model ${phone.model}) requires a lowercase 12-char hex MAC address as key";
|
|
}) person.phones
|
|
) cfg.persons))
|
|
++
|
|
# Provisioned person phones require a non-empty label
|
|
(lib.concatLists (lib.mapAttrsToList (personKey: person:
|
|
lib.mapAttrsToList (key: phone: {
|
|
assertion = !models.${phone.model}.hasProvisioning || phone.label != "";
|
|
message = "services.voip: persons.${personKey}.phones.\"${key}\" (model ${phone.model}) requires a non-empty label";
|
|
}) person.phones
|
|
) cfg.persons))
|
|
++
|
|
# No duplicate phone keys across sharedPhones and persons.*.phones
|
|
[{
|
|
assertion =
|
|
let keys = lib.attrNames allPhones;
|
|
in lib.length keys == lib.length (lib.unique keys);
|
|
message = "services.voip: duplicate phone key detected across sharedPhones and persons.*.phones";
|
|
}]
|
|
++
|
|
# No duplicate extensions across sharedPhones and persons
|
|
[{
|
|
assertion =
|
|
let
|
|
exts = (lib.mapAttrsToList (_: p: p.extension) cfg.sharedPhones)
|
|
++ (lib.mapAttrsToList (_: p: p.extension) cfg.persons);
|
|
in lib.length exts == lib.length (lib.unique exts);
|
|
message = "services.voip: duplicate extension number across sharedPhones and persons";
|
|
}]
|
|
++
|
|
# dids require at least one sipTrunk
|
|
(lib.optionals (cfg.dids != {}) [{
|
|
assertion = cfg.sipTrunks != {};
|
|
message = "services.voip: dids are configured but sipTrunks is empty";
|
|
}])
|
|
++
|
|
# each DID must reference an existing trunk
|
|
(lib.mapAttrsToList (did: didCfg: {
|
|
assertion = lib.hasAttr didCfg.trunk cfg.sipTrunks;
|
|
message = "services.voip: DID ${did} references trunk \"${didCfg.trunk}\" which is not in services.voip.sipTrunks";
|
|
}) cfg.dids)
|
|
++
|
|
# dids with mailbox="shared" require sharedMailbox
|
|
(lib.mapAttrsToList (did: didCfg: {
|
|
assertion = didCfg.mailbox != "shared" || cfg.sharedMailbox != null;
|
|
message = "services.voip: DID ${did} has mailbox=\"shared\" but sharedMailbox is not configured";
|
|
}) cfg.dids)
|
|
++
|
|
# dids with mailbox="person" require routing.type="person"
|
|
(lib.mapAttrsToList (did: didCfg: {
|
|
assertion = didCfg.mailbox != "person" || didCfg.routing.type == "person";
|
|
message = "services.voip: DID ${did} has mailbox=\"person\" but routing.type is not \"person\"";
|
|
}) cfg.dids)
|
|
++
|
|
# dids routing.type="person" — person key must be non-empty
|
|
(lib.mapAttrsToList (did: didCfg: {
|
|
assertion = didCfg.routing.type != "person" || didCfg.routing.person != "";
|
|
message = "services.voip: DID ${did} has routing.type=\"person\" but routing.person is not set";
|
|
}) cfg.dids)
|
|
++
|
|
# dids routing.type="person" — referenced person must exist
|
|
(lib.mapAttrsToList (did: didCfg: {
|
|
assertion = didCfg.routing.type != "person" || didCfg.routing.person == "" || lib.hasAttr didCfg.routing.person cfg.persons;
|
|
message = "services.voip: DID ${did} references person \"${didCfg.routing.person}\" which is not in services.voip.persons";
|
|
}) cfg.dids)
|
|
++
|
|
# dids routing.type="persons" — persons list must be non-empty
|
|
(lib.mapAttrsToList (did: didCfg: {
|
|
assertion = didCfg.routing.type != "persons" || didCfg.routing.persons != [];
|
|
message = "services.voip: DID ${did} has routing.type=\"persons\" but routing.persons is empty";
|
|
}) cfg.dids)
|
|
++
|
|
# dids routing.type="persons" — all referenced persons must exist
|
|
(lib.concatLists (lib.mapAttrsToList (did: didCfg:
|
|
lib.optionals (didCfg.routing.type == "persons")
|
|
(map (p: {
|
|
assertion = lib.hasAttr p cfg.persons;
|
|
message = "services.voip: DID ${did} references person \"${p}\" which is not in services.voip.persons";
|
|
}) didCfg.routing.persons)
|
|
) cfg.dids))
|
|
++
|
|
# dids musicOnHold must reference an existing mohClass
|
|
(lib.concatLists (lib.mapAttrsToList (did: didCfg:
|
|
lib.optional (didCfg.musicOnHold != null) {
|
|
assertion = lib.hasAttr didCfg.musicOnHold cfg.mohClasses;
|
|
message = "services.voip: DID ${did} references mohClass \"${didCfg.musicOnHold}\" which is not in services.voip.mohClasses";
|
|
}
|
|
) cfg.dids))
|
|
++
|
|
# sipTrunks: each required field needs either a literal or a file
|
|
(lib.concatLists (lib.mapAttrsToList (name: t: [
|
|
{ assertion = t.host != "" || t.hostFile != null;
|
|
message = "services.voip: sipTrunks.\"${name}\" requires host or hostFile"; }
|
|
{ assertion = t.username != "" || t.usernameFile != null;
|
|
message = "services.voip: sipTrunks.\"${name}\" requires username or usernameFile"; }
|
|
{ assertion = t.password != "" || t.passwordFile != null;
|
|
message = "services.voip: sipTrunks.\"${name}\" requires password or passwordFile"; }
|
|
]) cfg.sipTrunks))
|
|
++
|
|
# dids: each DID needs a number either inline or from a file
|
|
(lib.mapAttrsToList (id: d: {
|
|
assertion = d.number != "" || d.numberFile != null;
|
|
message = "services.voip: dids.\"${id}\" requires number or numberFile";
|
|
}) cfg.dids)
|
|
++
|
|
# extensions with mode="app" must have a non-null app field
|
|
(lib.mapAttrsToList (ext: extCfg: {
|
|
assertion = extCfg.mode != "app" || extCfg.app != null;
|
|
message = "services.voip: extension \"${ext}\" has mode=\"app\" but app is not set";
|
|
}) cfg.extensions)
|
|
++
|
|
# intercomPrefix must not collide with any declared extension
|
|
(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";
|
|
}
|
|
) allPhones))
|