Skip to content

Instantly share code, notes, and snippets.

@md-riaz
Created December 8, 2025 08:44
Show Gist options
  • Select an option

  • Save md-riaz/a5e8a181511b14ba4302d70246583dd5 to your computer and use it in GitHub Desktop.

Select an option

Save md-riaz/a5e8a181511b14ba4302d70246583dd5 to your computer and use it in GitHub Desktop.
FusionPBX Single Domain Migration Tool
#!/usr/bin/env php
<?php
/**
* FusionPBX Single Domain Migration Tool
*
* Features:
* - Export one domain’s data (extensions, ring groups, IVRs, voicemail, destinations, gateways, etc.)
* - Filter out default dialplans by app_uuid (read from FusionPBX XML templates)
* - Import into a new domain, only changing domain_uuid
* - Keeps all other UUIDs unchanged (extensions, gateways, ivrs, ring groups, dialplans, etc.)
*
* USAGE:
* Export on source server:
* php domain_migrate.php export \
* --domain=old.example.com \
* --out=/tmp/fpbx-old.example.com-export \
* --fusionpbx-path=/var/www/fusionpbx
*
* Import on target server (domain must already exist in GUI):
* php domain_migrate.php import \
* --domain=new.example.com \
* --in=/tmp/fpbx-old.example.com-export
*
* DB CONFIG:
* Edit the DB_* constants below to match your FusionPBX PostgreSQL database.
*/
// ----------------- DB CONFIG -----------------
const DB_DSN = 'pgsql:host=127.0.0.1;port=5432;dbname=fusionpbx;';
const DB_USER = 'fusionpbx';
const DB_PASS = 'fusionpbx';
// -------------------------------------------------
// CLI bootstrap
// -------------------------------------------------
if (php_sapi_name() !== 'cli') {
fwrite(STDERR, "This script must be run from the command line.\n");
exit(1);
}
$args = parseArguments($argv);
$command = $args['command'] ?? null;
if (!$command || !in_array($command, ['export', 'import'], true)) {
usage();
exit(1);
}
// Create DB connection
try {
$pdo = new PDO(DB_DSN, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
} catch (Exception $e) {
fwrite(STDERR, "DB connection failed: " . $e->getMessage() . "\n");
exit(1);
}
// -------------------------------------------------
// Commands
// -------------------------------------------------
if ($command === 'export') {
$domainName = requireArg($args, 'domain');
$outputDir = requireArg($args, 'out');
$fusionpbxPath = requireArg($args, 'fusionpbx-path');
exportDomain($pdo, $domainName, $outputDir, $fusionpbxPath);
exit(0);
}
if ($command === 'import') {
$domainName = requireArg($args, 'domain');
$inputDir = requireArg($args, 'in');
importDomain($pdo, $domainName, $inputDir);
exit(0);
}
// -------------------------------------------------
// Core functions
// -------------------------------------------------
function exportDomain(PDO $pdo, string $domainName, string $outputDir, string $fusionpbxPath): void
{
echo "=== EXPORT DOMAIN: {$domainName} ===\n";
// Resolve domain_uuid
$oldDomainUuid = getDomainUuid($pdo, $domainName);
if (!$oldDomainUuid) {
fwrite(STDERR, "Domain not found in v_domains: {$domainName}\n");
exit(1);
}
echo "Found domain_uuid: {$oldDomainUuid}\n";
// Ensure output dir
if (!is_dir($outputDir)) {
if (!mkdir($outputDir, 0775, true)) {
fwrite(STDERR, "Failed to create output directory: {$outputDir}\n");
exit(1);
}
}
// Collect default app_uuid list from FusionPBX XML templates
$defaultAppUuids = collectDefaultAppUuids($fusionpbxPath);
if (empty($defaultAppUuids)) {
fwrite(STDERR, "ERROR: Could not collect any default app_uuid from XML under {$fusionpbxPath}\n");
fwrite(STDERR, "Check --fusionpbx-path. Aborting to avoid exporting default dialplans.\n");
exit(1);
}
$meta = [
'domain_name' => $domainName,
'domain_uuid' => $oldDomainUuid,
'exported_at' => date('c'),
'default_app_uuids' => $defaultAppUuids,
];
file_put_contents(rtrim($outputDir, '/') . '/_meta.json', json_encode($meta, JSON_PRETTY_PRINT));
// Tables to export directly (filtered only by domain_uuid)
// NOTE: v_destinations and v_gateways are included here.
$tables = [
'v_domain_settings',
'v_extensions',
'v_extension_users',
'v_ring_groups',
'v_ring_group_destinations',
'v_ivr_menus',
'v_ivr_menu_options',
'v_destinations',
'v_voicemails',
'v_voicemail_greetings',
'v_voicemail_messages',
'v_recordings',
'v_gateways',
];
foreach ($tables as $table) {
echo "Exporting table {$table}...\n";
$stmt = $pdo->prepare("SELECT * FROM {$table} WHERE domain_uuid = :domain_uuid");
$stmt->execute([':domain_uuid' => $oldDomainUuid]);
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
file_put_contents("{$outputDir}/{$table}.json", json_encode($rows, JSON_PRETTY_PRINT));
}
// Export dialplans (excluding default app_uuid)
echo "Exporting v_dialplans (non-default) ...\n";
$placeholders = [];
$params = [':domain_uuid' => $oldDomainUuid];
foreach ($defaultAppUuids as $idx => $uuid) {
$ph = ":app_uuid_$idx";
$placeholders[] = $ph;
$params[$ph] = $uuid;
}
$sqlDialplans = "
SELECT *
FROM v_dialplans
WHERE domain_uuid = :domain_uuid
AND (
app_uuid IS NULL
OR app_uuid NOT IN (" . implode(', ', $placeholders) . ")
)
";
$stmt = $pdo->prepare($sqlDialplans);
$stmt->execute($params);
$dialplans = $stmt->fetchAll(PDO::FETCH_ASSOC);
file_put_contents("{$outputDir}/v_dialplans.json", json_encode($dialplans, JSON_PRETTY_PRINT));
echo "Exporting v_dialplan_details (matching exported v_dialplans) ...\n";
$dialplanUuids = array_column($dialplans, 'dialplan_uuid');
$dialplanDetails = [];
if (!empty($dialplanUuids)) {
// Build IN clause for dialplan_uuid
$phs = [];
$paramsDetails = [];
foreach ($dialplanUuids as $i => $dpUuid) {
$ph = ":dp_$i";
$phs[] = $ph;
$paramsDetails[$ph] = $dpUuid;
}
$sqlDetails = "
SELECT *
FROM v_dialplan_details
WHERE dialplan_uuid IN (" . implode(', ', $phs) . ")
";
$stmt = $pdo->prepare($sqlDetails);
$stmt->execute($paramsDetails);
$dialplanDetails = $stmt->fetchAll(PDO::FETCH_ASSOC);
}
file_put_contents("{$outputDir}/v_dialplan_details.json", json_encode($dialplanDetails, JSON_PRETTY_PRINT));
echo "\n=== EXPORT COMPLETE ===\n";
echo "Output directory: {$outputDir}\n\n";
echo "Remember to copy filesystem data (voicemail, recordings, etc.) manually, e.g.:\n";
echo " recordings: /var/lib/freeswitch/recordings/{$domainName}/\n";
echo " voicemail : /var/lib/freeswitch/storage/voicemail/default/{$domainName}/\n";
}
function importDomain(PDO $pdo, string $newDomainName, string $inputDir): void
{
echo "=== IMPORT INTO DOMAIN: {$newDomainName} ===\n";
if (!is_dir($inputDir)) {
fwrite(STDERR, "Input directory does not exist: {$inputDir}\n");
exit(1);
}
$metaPath = rtrim($inputDir, '/') . '/_meta.json';
if (!file_exists($metaPath)) {
fwrite(STDERR, "Missing _meta.json in input directory. Is this a valid export?\n");
exit(1);
}
$meta = json_decode(file_get_contents($metaPath), true);
if (!$meta || empty($meta['domain_uuid']) || empty($meta['domain_name'])) {
fwrite(STDERR, "Invalid _meta.json file.\n");
exit(1);
}
$oldDomainName = $meta['domain_name'];
$oldDomainUuid = $meta['domain_uuid'];
echo "Exported from domain: {$oldDomainName} ({$oldDomainUuid})\n";
// Resolve new domain_uuid
$newDomainUuid = getDomainUuid($pdo, $newDomainName);
if (!$newDomainUuid) {
fwrite(STDERR, "Target domain not found in v_domains: {$newDomainName}\n");
fwrite(STDERR, "Create the domain first in FusionPBX GUI, then re-run.\n");
exit(1);
}
echo "Target domain_uuid: {$newDomainUuid}\n";
// Import order matters for foreign keys a bit; gateways can be early.
$tables = [
'v_domain_settings',
'v_extensions',
'v_extension_users',
'v_ring_groups',
'v_ring_group_destinations',
'v_ivr_menus',
'v_ivr_menu_options',
'v_destinations',
'v_voicemails',
'v_voicemail_greetings',
'v_voicemail_messages',
'v_recordings',
'v_gateways',
'v_dialplans',
'v_dialplan_details',
];
foreach ($tables as $table) {
$path = "{$inputDir}/{$table}.json";
if (!file_exists($path)) {
echo "Skipping {$table} (no export file found).\n";
continue;
}
echo "Importing {$table}...\n";
$rows = json_decode(file_get_contents($path), true);
if (!is_array($rows) || empty($rows)) {
echo " No rows to import for {$table}.\n";
continue;
}
foreach ($rows as $row) {
if (!isset($row['domain_uuid'])) {
// Some tables might not have domain_uuid – we skip those by design here.
continue;
}
// Rewrite domain_uuid only
$row['domain_uuid'] = $newDomainUuid;
insertRow($pdo, $table, $row);
}
}
echo "\n=== IMPORT COMPLETE ===\n";
echo "Now reload XML/dialplan on the new server, e.g.:\n";
echo " fs_cli -x \"reloadxml\"\n";
echo " fs_cli -x \"reload mod_dialplan_xml\"\n";
echo "\nTest calls, ring groups, IVR, voicemail, destinations, gateways, etc. for domain: {$newDomainName}\n";
}
// -------------------------------------------------
// Helper: DB operations
// -------------------------------------------------
function getDomainUuid(PDO $pdo, string $domainName): ?string
{
$stmt = $pdo->prepare("SELECT domain_uuid FROM v_domains WHERE domain_name = :domain_name");
$stmt->execute([':domain_name' => $domainName]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row['domain_uuid'] ?? null;
}
function insertRow(PDO $pdo, string $table, array $row): void
{
// Build INSERT
$cols = array_keys($row);
$placeholders = array_map(fn($c) => ':' . $c, $cols);
$sql = "INSERT INTO {$table} (" . implode(', ', $cols) . ")
VALUES (" . implode(', ', $placeholders) . ")";
$stmt = $pdo->prepare($sql);
// Bind params
foreach ($row as $col => $val) {
$stmt->bindValue(':' . $col, $val);
}
try {
$stmt->execute();
} catch (Exception $e) {
// Most likely duplicate UUID in target DB or constraint issue.
fwrite(STDERR, " [WARN] Failed to insert into {$table}: " . $e->getMessage() . "\n");
}
}
// -------------------------------------------------
// Helper: Collect default app_uuid from XML
// -------------------------------------------------
function collectDefaultAppUuids(string $fusionpbxPath): array
{
$fusionpbxPath = rtrim($fusionpbxPath, '/');
$paths = [];
// Core dialplan templates
$paths[] = $fusionpbxPath . '/resources/templates/xml/dialplan';
// App-specific dialplans
$appDir = $fusionpbxPath . '/app';
if (is_dir($appDir)) {
$dirIterator = new RecursiveDirectoryIterator($appDir, FilesystemIterator::SKIP_DOTS);
$iterator = new RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
/** @var SplFileInfo $file */
if (!$file->isFile()) continue;
if (substr($file->getFilename(), -4) !== '.xml') continue;
$path = $file->getPathname();
if (strpos($path, '/resources/switch/conf/dialplan/') !== false) {
$paths[] = $path;
}
}
}
$uuids = [];
foreach ($paths as $path) {
if (is_dir($path)) {
$dirIter = new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS);
$iter = new RecursiveIteratorIterator($dirIter);
foreach ($iter as $file) {
/** @var SplFileInfo $file */
if (!$file->isFile()) continue;
if (substr($file->getFilename(), -4) !== '.xml') continue;
$content = @file_get_contents($file->getPathname());
if ($content === false) continue;
// Look for app_uuid="..."
if (preg_match_all('/app_uuid="([0-9a-fA-F-]+)"/', $content, $matches)) {
foreach ($matches[1] as $uuid) {
$uuids[$uuid] = true;
}
}
}
} elseif (is_file($path)) {
$content = @file_get_contents($path);
if ($content === false) continue;
if (preg_match_all('/app_uuid="([0-9a-fA-F-]+)"/', $content, $matches)) {
foreach ($matches[1] as $uuid) {
$uuids[$uuid] = true;
}
}
}
}
$result = array_keys($uuids);
sort($result);
echo "Collected " . count($result) . " default app_uuid values from XML.\n";
return $result;
}
// -------------------------------------------------
// Helper: CLI parsing
// -------------------------------------------------
function parseArguments(array $argv): array
{
$out = [
'command' => null,
];
if (isset($argv[1])) {
$out['command'] = $argv[1];
}
for ($i = 2; $i < count($argv); $i++) {
$arg = $argv[$i];
if (strpos($arg, '--') === 0) {
$eqPos = strpos($arg, '=');
if ($eqPos !== false) {
$key = substr($arg, 2, $eqPos - 2);
$val = substr($arg, $eqPos + 1);
$out[$key] = $val;
} else {
$key = substr($arg, 2);
$out[$key] = true;
}
}
}
return $out;
}
function requireArg(array $args, string $name): string
{
if (empty($args[$name])) {
fwrite(STDERR, "Missing required argument --{$name}\n");
usage();
exit(1);
}
return (string)$args[$name];
}
function usage(): void
{
$self = basename(__FILE__);
echo "FusionPBX Single-Domain Migration\n";
echo "\n";
echo "Usage:\n";
echo " Export from source server:\n";
echo " php {$self} export \\\n";
echo " --domain=old.example.com \\\n";
echo " --out=/tmp/fpbx-old.example.com-export \\\n";
echo " --fusionpbx-path=/var/www/fusionpbx\n";
echo "\n";
echo " Import into target server (domain must exist already):\n";
echo " php {$self} import \\\n";
echo " --domain=new.example.com \\\n";
echo " --in=/tmp/fpbx-old.example.com-export\n";
echo "\n";
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment