FusionPBX Module

This commit is contained in:
kat witch 2021-08-15 02:10:38 +01:00
parent 27d63cc2d0
commit a74450a8de
No known key found for this signature in database
GPG key ID: 1B477797DCA5EC72
6 changed files with 491 additions and 0 deletions

View file

@ -9,9 +9,11 @@ with lib;
profiles.hardware.rm-310
profiles.gui
users.kat.guiFull
services.fusionpbx
services.jellyfin
services.kattv-ingest
services.promtail
services.postgres
services.netdata
services.nfs
services.nginx

View file

@ -4,6 +4,7 @@
imports = with (import (sources.nixexprs + "/modules")).nixos; [ base16 base16-shared modprobe ] ++ [
./nftables.nix
./firewall.nix
./fusionpbx.nix
./deploy.nix
./dyndns.nix
./network.nix

View file

@ -0,0 +1,413 @@
{ config, pkgs, lib, ... }:
with lib;
let
cfg = config.services.fusionpbx;
toKeyValue = generators.toKeyValue {
mkKeyValue = generators.mkKeyValueDefault {} " = ";
};
php = "${pkgs.php74}/bin/php";
psql_base = "${pkgs.postgresql_11}/bin/psql";
psql = if ! cfg.useLocalPostgreSQL then
"${psql_base} --host=${cfg.postgres.host} --port=${cfg.postgres.port} --username=${cfg.postgres.db_username}"
else psql_base;
freeSwitchConfig = pkgs.writeShellScriptBin "copy_config" ''
set -exu
if [ ! -f "${cfg.home}/state/installed" ]; then
mkdir -p /etc/freeswitch
cp --no-preserve=mode,ownership -r ${cfg.package}/resources/templates/conf/* /etc/freeswitch
fi
'';
installerReplacement = pkgs.writeShellScriptBin "installer_replacement" ''
set -exu
if [ -f "${cfg.home}/state/installed" ]; then
if [ -f "${cfg.home}/state/lastversion" ]; then
lastversion=$(<${cfg.home}/state/lastversion)
if [ lastversion != "${cfg.package.version}" ]; then
${php} ${cfg.package}/core/upgrade/upgrade_schema.php
echo "${cfg.package.version}" >| ${cfg.home}/state/lastversion
fi
fi
else
mkdir -p /var/lib/fusionpbx
${if ! cfg.useLocalPostgreSQL then "PGPASSWORD=${cfg.postgres.db_password}" else ""}
${php} ${cfg.package}/core/upgrade/upgrade_schema.php
domain_uuid=$(${php} ${cfg.package}/resources/uuid.php);
domain_name=${cfg.domain}
${psql} -c "insert into v_domains (domain_uuid, domain_name, domain_enabled) values('$domain_uuid', '$domain_name', 'true');"
cd "${cfg.package}" && ${php} ${cfg.package}/core/upgrade/upgrade_domains.php
user_uuid=$(${php} ${cfg.package}/resources/uuid.php);
user_salt=$(${php} ${cfg.package}/resources/uuid.php);
password_hash=$(${php} -r "echo md5('$user_salt$USER_PASSWORD');");
${psql} -t -c "insert into v_users (user_uuid, domain_uuid, username, password, salt, user_enabled) values('$user_uuid', '$domain_uuid', '$USER_NAME', '$password_hash', '$user_salt', 'true');"
group_uuid=$(${psql} -qtAX -c "select group_uuid from v_groups where group_name = 'superadmin';");
group_uuid=$(echo $group_uuid | sed 's/^[[:blank:]]*//;s/[[:blank:]]*$//')
user_group_uuid=$(${php} ${cfg.package}/resources/uuid.php);
group_name=superadmin
#echo "insert into v_user_groups (user_group_uuid, domain_uuid, group_name, group_uuid, user_uuid) values('$user_group_uuid', '$domain_uuid', '$group_name', '$group_uuid', '$user_uuid');"
${psql} -c "insert into v_user_groups (user_group_uuid, domain_uuid, group_name, group_uuid, user_uuid) values('$user_group_uuid', '$domain_uuid', '$group_name', '$group_uuid', '$user_uuid');"
xml_cdr_username=$(dd if=/dev/urandom bs=1 count=20 2>/dev/null | base64 | sed 's/[=\+//]//g')
xml_cdr_password=$(dd if=/dev/urandom bs=1 count=20 2>/dev/null | base64 | sed 's/[=\+//]//g')
sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_http_protocol}:http:"
sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_project_path}::"
sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_user}:$xml_cdr_username:"
sed -i /etc/freeswitch/autoload_configs/xml_cdr.conf.xml -e s:"{v_pass}:$xml_cdr_password:"
cd "${cfg.package}" && ${php} ${cfg.package}/core/upgrade/upgrade_domains.php
mkdir -p ${cfg.home}/state
touch ${cfg.home}/state/installed
fi
'';
in {
options.services.fusionpbx = {
enable = mkEnableOption "Enable FusionPBX";
openFirewall = mkEnableOption "Open the firewall for FusionPBX" // { default = true; };
useLocalPostgreSQL = mkEnableOption "Use Local PostgreSQL for FusionPBX" // { default = true; };
postgres = {
host = mkOption {
type = types.nullOr types.str;
default = null;
};
port = mkOption {
type = types.nullOr types.port;
default = null;
};
db_name = mkOption {
type = types.nullOr types.str;
default = null;
};
db_username = mkOption {
type = types.nullOr types.str;
default = null;
};
db_password = mkOption {
type = types.nullOr types.str;
default = null;
};
};
environmentFile = mkOption {
type = types.str;
example = ''
USER_NAME="meow"
USER_PASSWORD="nya"
'';
};
hardphones = mkEnableOption "Are you going to use hardphones with FusionPBX?";
useWebrootACME = mkEnableOption "Do you want webroot-style ACME cert generation?";
useACMEHost = mkOption {
type = types.nullOr types.str;
default = null;
};
domain = mkOption {
type = types.str;
};
package = mkOption {
type = types.package;
description = "What package to use for FusionPBX?";
default = pkgs.fusionpbx;
relatedPackages = [
"fusionpbx"
];
};
freeSwitchPackage = mkOption {
type = types.package;
description = "What package to use for FreeSWITCH?";
default = pkgs.freeswitch;
relatedPackages = [
"freeswitch"
];
};
home = mkOption {
type = types.str;
default = "/var/lib/fusionpbx";
description = "Storage path for FusionPBX";
};
};
config = mkIf cfg.enable {
# User & Group Definition
users.users.fusionpbx = {
home = cfg.home;
group = "fusionpbx";
createHome = true;
isSystemUser = true;
};
users.groups.fusionpbx.members = [
"fusionpbx"
config.services.nginx.user
];
# PostgreSQL
services.postgresql = mkIf cfg.useLocalPostgreSQL {
ensureUsers = [
{
name = "fusionpbx";
ensurePermissions = {
"DATABASE fusionpbx" = "ALL PRIVILEGES";
};
}
{
name = "freeswitch";
ensurePermissions = {
"DATABASE fusionpbx" = "ALL PRIVILEGES";
"DATABASE freeswitch" = "ALL PRIVILEGES";
};
}
];
ensureDatabases = [ "fusionpbx" "freeswitch" ];
};
# ACME
security.acme.certs = mkIf cfg.useWebrootACME {
${cfg.domain} = {
group = "fusionpbx";
};
};
# NGINX
services.nginx = {
enable = mkDefault true;
virtualHosts.${cfg.domain} = {
enableACME = cfg.useWebrootACME;
useACMEHost = cfg.useACMEHost;
forceSSL = true;
# forceSSL = true; # This might not make sense due to SSL-incapable hardphones?
root = cfg.package;
locations = {
"/" = {
index = "index.php";
};
"~ .htaccess".extraConfig = "deny all;";
"~ .htpassword".extraConfig = "deny all;";
"~^.+.(db)$".extraConfig = "deny all;";
"~ \\.php$" = {
extraConfig = ''
include ${pkgs.nginx}/conf/fastcgi_params;
fastcgi_pass unix:${config.services.phpfpm.pools.fusionpbx.socket};
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME ${cfg.package}$fastcgi_script_name;
'';
};
" = /core/upgrade/index.php".extraConfig = ''
include ${pkgs.nginx}/conf/fastcgi_params;
fastcgi_pass unix:${config.services.phpfpm.pools.fusionpbx.socket};
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME ${cfg.package}$fastcgi_script_name;
fastcgi_read_timeout 15m;
'';
};
/*
if ($uri !~* ^.*(provision|xml_cdr).*$) {
rewrite ^(.*) https://$host$1 permanent;
break;
}
*/
extraConfig = ''
client_max_body_size 80M;
client_body_buffer_size 128k;
#REST api
if ($uri ~* ^.*/api/.*$) {
rewrite ^(.*)/api/(.*)$ $1/api/index.php?rewrite_uri=$2 last;
break;
}
'' + optionalString cfg.hardphones ''
#algo
rewrite "^.*/provision/algom([A-Fa-f0-9]{12})(\.(conf))?$" /app/provision/?mac=$1;
#mitel
rewrite "^.*/provision/MN_([A-Fa-f0-9]{12})\.cfg" /app/provision/index.php?mac=$1&file=MN_%7b%24mac%7d.cfg last;
rewrite "^.*/provision/MN_Generic.cfg" /app/provision/index.php?mac=08000f000000&file=MN_Generic.cfg last;
#grandstream
rewrite "^.*/provision/cfg([A-Fa-f0-9]{12})(\.(xml|cfg))?$" /app/provision/?mac=$1;
rewrite "^.*/provision/pb([A-Fa-f0-9-]{12,17})/phonebook\.xml$" /app/provision/?mac=$1&file=phonebook.xml;
#grandstream-wave softphone by ext because Android doesn't pass MAC.
rewrite "^.*/provision/([0-9]{5})/cfg([A-Fa-f0-9]{12}).xml$" /app/provision/?ext=$1;
#aastra
rewrite "^.*/provision/aastra.cfg$" /app/provision/?mac=$1&file=aastra.cfg;
#rewrite "^.*/provision/([A-Fa-f0-9]{12})(\.(cfg))?$" /app/provision/?mac=$1 last;
#yealink common
rewrite "^.*/provision/(y[0-9]{12})(\.cfg)?$" /app/provision/index.php?file=$1.cfg;
#yealink mac
rewrite "^.*/provision/([A-Fa-f0-9]{12})(\.(xml|cfg))?$" /app/provision/index.php?mac=$1 last;
#polycom
rewrite "^.*/provision/000000000000.cfg$" "/app/provision/?mac=$1&file={%24mac}.cfg";
#rewrite "^.*/provision/sip_330(\.(ld))$" /includes/firmware/sip_330.$2;
rewrite "^.*/provision/features.cfg$" /app/provision/?mac=$1&file=features.cfg;
rewrite "^.*/provision/([A-Fa-f0-9]{12})-sip.cfg$" /app/provision/?mac=$1&file=sip.cfg;
rewrite "^.*/provision/([A-Fa-f0-9]{12})-phone.cfg$" /app/provision/?mac=$1;
rewrite "^.*/provision/([A-Fa-f0-9]{12})-registration.cfg$" "/app/provision/?mac=$1&file={%24mac}-registration.cfg";
rewrite "^.*/provision/([A-Fa-f0-9]{12})-directory.xml$" "/app/provision/?mac=$1&file={%24mac}-directory.xml";
#cisco
rewrite "^.*/provision/file/(.*\.(xml|cfg))" /app/provision/?file=$1 last;
#Escene
rewrite "^.*/provision/([0-9]{1,11})_Extern.xml$" "/app/provision/?ext=$1&file={%24mac}_extern.xml" last;
rewrite "^.*/provision/([0-9]{1,11})_Phonebook.xml$" "/app/provision/?ext=$1&file={%24mac}_phonebook.xml" last;
#Vtech
rewrite "^.*/provision/VCS754_([A-Fa-f0-9]{12})\.cfg$" /app/provision/?mac=$1;
rewrite "^.*/provision/pb([A-Fa-f0-9-]{12,17})/directory\.xml$" /app/provision/?mac=$1&file=directory.xml;
#Digium
rewrite "^.*/provision/([A-Fa-f0-9]{12})-contacts\.cfg$" "/app/provision/?mac=$1&file={%24mac}-contacts.cfg";
rewrite "^.*/provision/([A-Fa-f0-9]{12})-smartblf\.cfg$" "/app/provision/?mac=$1&file={%24mac}-smartblf.cfg";
'';
};
};
# PHP 7.4
services.phpfpm = {
pools.fusionpbx = {
user = "fusionpbx";
group = "fusionpbx";
phpEnv = {
PATH = "/run/wrappers/bin:/nix/var/nix/profiles/default/bin:/run/current-system/sw/bin:/usr/bin:/bin";
};
settings = {
"pm" = "dynamic";
"pm.max_children" = "32";
"pm.start_servers" = "2";
"pm.min_spare_servers" = "2";
"pm.max_spare_servers" = "4";
"pm.max_requests" = "500";
"listen.owner" = "fusionpbx";
"listen.group" = config.services.nginx.group;
};
phpPackage = pkgs.php74.buildEnv {
extensions = { enabled, all }: (
with all;
enabled ++ [
imap
pgsql
curl
opcache
pdo
pdo_pgsql
soap
xmlrpc
gd
]
);
extraConfig = toKeyValue {
};
};
};
};
# FreeSWITCH
systemd.tmpfiles.rules = [
"v /etc/freeswitch 5777 fusionpbx fusionpbx"
];
systemd.services.freeswitch = let
pkg = cfg.freeSwitchPackage;
configPath = "/etc/freeswitch";
in {
description = "Free and open-source application server for real-time communication";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "fusionpbx";
Group = "fusionpbx";
StateDirectory = "freeswitch";
ExecStartPre = "${freeSwitchConfig}/bin/copy_config";
ExecStart = "${pkg}/bin/freeswitch -nf \\
-mod ${pkg}/lib/freeswitch/mod \\
-conf ${configPath} \\
-base /var/lib/freeswitch";
ExecReload = "${pkg}/bin/fs_cli -x reloadxml";
Restart = "on-failure";
RestartSec = "5s";
CPUSchedulingPolicy = "fifo";
};
};
systemd.services.fusionpbx = {
after = [ "network.target" ];
wantedBy = [ "freeswitch.service" ];
script = "${installerReplacement}/bin/installer_replacement";
serviceConfig = {
EnvironmentFile = cfg.environmentFile;
User = "fusionpbx";
Group = "fusionpbx";
Type = "oneshot";
StateDirectory = "fusionpbx";
};
};
# FusionPBX Config
environment.etc."fusionpbx/config.php" = {
user = "nginx";
group = "fusionpbx";
text = let
hostConfig = if cfg.useLocalPostgreSQL then ''
$db_type = 'pgsql';
$db_host = ''';
$db_port = ''';
$db_name = 'fusionpbx';
$db_username = 'fusionpbx';
$db_password = ''';
'' else ''
$db_type = 'pgsql';
$db_host = '${cfg.postgres.host}';
$db_port = '${toString cfg.postgres.port}';
$db_name = '${cfg.postgres.db_name}';
$db_username = '${cfg.postgres.db_username}';
$db_password = '${cfg.postgres.db_password}';
''; in ''
<?php
${hostConfig}
ini_set('display_errors', '1');
error_reporting(E_ALL ^ E_NOTICE ^ E_WARNING);
?>
'';
};
# Firewall
network.firewall = mkIf cfg.openFirewall {
public = {
tcp = {
ports = [ 5060 5160 ];
ranges = [
{
from = 10000;
to = 20000;
}
];
};
udp = {
ports = [ 5060 5160 ];
ranges = [
{
from = 10000;
to = 20000;
}
];
};
};
};
};
}

View file

@ -0,0 +1,43 @@
{ config, pkgs, tf, ... }:
{
deploy.tf.dns.records.services_fusionpbx = {
tld = config.network.dns.tld;
domain = "pbx";
cname.target = "${config.network.addresses.private.domain}.";
};
kw.secrets = [
"fusionpbx-username"
"fusionpbx-password"
];
secrets.files.fusionpbx_env = {
text = ''
USER_NAME=${tf.variables.fusionpbx-username.ref}
USER_PASSWORD=${tf.variables.fusionpbx-password.ref}
'';
owner = "fusionpbx";
group = "fusionpbx";
};
security.acme.certs.services_fusionpbx = {
domain = "pbx.${config.network.dns.domain}";
group = "fusionpbx";
dnsProvider = "rfc2136";
credentialsFile = config.secrets.files.dns_creds.path;
postRun = "systemctl restart nginx";
};
services.fusionpbx = {
enable = true;
openFirewall = true;
useLocalPostgreSQL = true;
environmentFile = config.secrets.files.fusionpbx_env.path;
hardphones = true;
useACMEHost = "services_fusionpbx";
domain = "pbx.${config.network.dns.domain}";
package = with pkgs; fusionpbx;
freeSwitchPackage = with pkgs; freeswitch;
};
}

View file

@ -90,6 +90,8 @@ let
kat-scrot = self.callPackage ./kat-scrot { };
fusionpbx = self.callPackage ./fusionpbx { };
} // super.lib.optionalAttrs (builtins.pathExists ../config/trusted/pkgs)
(import ../config/trusted/pkgs { inherit super self; });
pkgs = import sources.nixpkgs {

View file

@ -0,0 +1,30 @@
{
lib,
stdenv,
fetchFromGitHub
}:
stdenv.mkDerivation rec {
pname = "fusionpbx";
version = "master";
src = fetchFromGitHub {
owner = pname;
repo = pname;
rev = "2b8d011321ee5f2ffba967e38fcc8c542f378502";
sha256 = "0fsmf67hrddz6aqjrjjqxa72iw108z2skwhn9jb3p465xfq7a9ij";
};
installPhase = ''
mkdir -p $out
mv * $out
'';
meta = with lib; {
description = "A full-featured domain based multi-tenant PBX and voice switch for FreeSWITCH.";
homepage = "https://www.fusionpbx.com/";
license = with licenses; mpl11;
maintainers = with maintainers; [ kittywitch ];
platforms = with platforms; unix;
};
}