Commit 2d6aaeae authored by Andrey Filippov's avatar Andrey Filippov

Initialize PBX migration repo

parents
Pipeline #3699 failed with stages
# elphel-pbx
Local Git repo for PBX migration, admin scripts, and runbooks.
Current target system:
- Host: `elphel-pbx`
- Service IP: `192.168.1.16`
- OS: Debian 12
- PBX stack: FreePBX 17 / Asterisk 22
Repo layout:
- `notes/`: cutover notes and operational runbooks
- `scripts/`: reusable migration/admin scripts
- `attic/`: local-only sensitive or bulky artifacts, ignored by Git
Useful existing source artifacts currently outside this repo:
- `../pbx/old-pbx-databases.sql`
- `../pbx/old-pbx-config-media.tgz`
- `../pbx/router01.log`
- `../pbx/asterisk01.log`
- `../pbx/debian-12.13.0-amd64-netinst.iso`
# 2026-03-20 Cutover Notes
## Result
The new PBX was cut over successfully to `192.168.1.16`.
Verified after cutover:
- extension `106` registered from `192.168.1.51:5060`
- extension `107` registered from `192.168.1.51:5061`
- internal call `106 -> 107` worked
- outbound Bandwidth call worked after E.164 route fix
- inbound DID call worked
## Key fixes during cutover
1. Corrected FreePBX external SIP/media address to `166.70.117.159`.
2. Removed the temporary `192.168.1.17` service address from the PBX.
3. Confirmed router NAT already mapped `192.168.1.16 -> 166.70.117.159`.
4. Updated Bandwidth-facing outbound routes to send `+E.164` numbers.
## Current routing assumptions
- Bandwidth: inbound DID and local Utah outbound
- binfone: interstate / long-distance via IAX2
- SPA112 ATA: `106` and `107`, proxy `192.168.1.16`
## Follow-up
- Re-test binfone route with an interstate destination
- Clean up any stale `192.168.1.17` hostname identity seen in some SIP headers
- Export or document the final FreePBX configuration state
#!/usr/bin/env php
<?php
declare(strict_types=1);
use FreePBX\modules\Core\Components\Outboundrouting;
require_once '/etc/freepbx.conf';
$freepbx = FreePBX::create();
$core = $freepbx->Core();
$voicemail = $freepbx->Voicemail();
$recordings = $freepbx->Recordings();
$ringgroups = $freepbx->Ringgroups();
$disa = $freepbx->Disa();
$ivr = $freepbx->Ivr();
$conferences = $freepbx->Conferences();
$routes = new Outboundrouting($freepbx);
$extensions = [
'100' => [
'tech' => 'pjsip',
'name' => 'Home Filippov',
'secret' => '31952614',
'voicemail' => null,
'ringtimer' => 0,
'sipname' => '',
'recording' => 'out=Never|in=Never',
],
'101' => [
'tech' => 'pjsip',
'name' => 'Filippovs',
'secret' => '56738405',
'voicemail' => [
'pwd' => '4466',
'email' => 'olgafilippova@gmail.com',
'pager' => '',
'options' => 'attach=yes|saycid=yes|envelope=yes|delete=yes',
],
'ringtimer' => 17,
'sipname' => '',
'recording' => 'out=Never|in=Never',
],
'102' => [
'tech' => 'pjsip',
'name' => 'Olga Filippova - home',
'secret' => '92182865',
'voicemail' => null,
'ringtimer' => 0,
'sipname' => 'o.filippova',
'recording' => 'out=Never|in=Never',
],
'106' => [
'tech' => 'pjsip',
'name' => 'A Filippov',
'secret' => 'tieR2EiB4',
'voicemail' => [
'pwd' => '1213',
'email' => 'voip@elphel.com',
'pager' => '',
'options' => 'attach=yes|saycid=yes|envelope=yes|delete=no',
],
'ringtimer' => 0,
'sipname' => 'andrey',
'recording' => 'out=Always|in=Always',
],
'107' => [
'tech' => 'pjsip',
'name' => 'Olga Filippova',
'secret' => 'K90fsu98us93',
'voicemail' => [
'pwd' => '1213',
'email' => 'olga@elphel.com',
'pager' => '',
'options' => 'attach=yes|saycid=yes|envelope=yes|delete=no',
],
'ringtimer' => 30,
'sipname' => 'olga',
'recording' => 'out=Always|in=Always',
],
'119' => [
'tech' => 'iax2',
'name' => 'RV ElphelRV',
'secret' => 'Ka9s0u382',
'voicemail' => null,
'ringtimer' => 0,
'sipname' => '',
'recording' => 'out=Never|in=Never',
],
'124' => [
'tech' => 'iax2',
'name' => 'Oleg',
'secret' => 'wqeqw21',
'voicemail' => null,
'ringtimer' => 0,
'sipname' => '',
'recording' => 'out=Never|in=Never',
],
'6216' => [
'tech' => 'pjsip',
'name' => 'Olga Filippova',
'secret' => 'K90fsu98us93',
'voicemail' => [
'pwd' => '1213',
'email' => 'olga@elphel.com',
'pager' => '',
'options' => 'attach=yes|saycid=yes|envelope=yes|delete=no',
],
'ringtimer' => 0,
'sipname' => 'olgaf',
'recording' => 'out=Never|in=Never',
],
];
$ringGroupData = [
['grpnum' => '600', 'description' => 'All home SIP', 'grplist' => '102-100-101-119-6216', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'ext-local,vmu101,1'],
['grpnum' => '601', 'description' => 'Andrey mobile', 'grplist' => '8562089#', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => ''],
['grpnum' => '602', 'description' => 'Joan US', 'grplist' => '8016949759#', 'strategy' => 'ringall', 'grptime' => '12', 'postdest' => 'ext-local,vmu107,1'],
['grpnum' => '603', 'description' => 'Olga mobile', 'grplist' => '5996216#', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => ''],
['grpnum' => '604', 'description' => 'Olga SIP mobile', 'grplist' => '102', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'ext-group,603,1'],
['grpnum' => '605', 'description' => 'Olga SIP', 'grplist' => '100', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'ext-group,603,1'],
['grpnum' => '606', 'description' => 'Elphel main', 'grplist' => '106-107-119-6216', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'app-blackhole,hangup,1'],
['grpnum' => '607', 'description' => 'Andrey sip mobile', 'grplist' => '100106', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'ext-group,601,1'],
['grpnum' => '608', 'description' => 'All home SIP Andrey mobile', 'grplist' => '102101100', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'ext-group,601,1'],
['grpnum' => '609', 'description' => 'Sales', 'grplist' => '107-119-106-6216', 'strategy' => 'ringall', 'grptime' => '22', 'postdest' => 'ext-local,vmu107,1'],
['grpnum' => '610', 'description' => 'Technical', 'grplist' => '106-119-8018562089#', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'ext-local,vms106,1'],
['grpnum' => '611', 'description' => 'Accounting', 'grplist' => '107-119-106-6216', 'strategy' => 'ringall', 'grptime' => '20', 'postdest' => 'ext-local,vmu107,1'],
];
$incomingRoutes = [
['extension' => '', 'cidnum' => '', 'destination' => 'ivr-4,s,1', 'description' => 'Default'],
['extension' => '18015216210', 'cidnum' => '', 'destination' => 'from-did-direct,106,1', 'description' => 'Utah State Gov'],
['extension' => '18017835555', 'cidnum' => '', 'destination' => 'ivr-4,s,1', 'description' => '8017835555'],
['extension' => '2501547', 'cidnum' => '', 'destination' => 'ext-group,600,1', 'description' => '2501547'],
['extension' => '2527457', 'cidnum' => '', 'destination' => 'ivr-4,s,1', 'description' => '2527457'],
['extension' => '2527464', 'cidnum' => '', 'destination' => 'ivr-4,s,1', 'description' => '2527464'],
['extension' => '7005420204', 'cidnum' => '', 'destination' => 'ivr-4,s,1', 'description' => '7005420204'],
['extension' => '8017835555', 'cidnum' => '', 'destination' => 'ivr-4,s,1', 'description' => "Elphel's number"],
];
$cleanupExtensions = ['5000', '5001', '5002', '5003', '5999', '9000'];
$managedTrunks = ['bw1out', 'bw2out', 'binfone'];
$managedRoutes = [
'elphel-911',
'elphel-manual-binfone',
'elphel-manual-bandwidth',
'elphel-utah-local',
'elphel-tollfree',
'elphel-us-national',
'elphel-international',
];
function logmsg(string $message): void
{
fwrite(STDERR, $message . PHP_EOL);
}
function setSetting(array &$settings, string $key, string $value): void
{
if (isset($settings[$key])) {
$settings[$key]['value'] = $value;
} else {
$settings[$key] = ['value' => $value, 'flag' => 999];
}
}
function recordingMode(string $legacy, bool $always): string
{
if ($always) {
return 'force';
}
return 'dontcare';
}
function removeMailbox($voicemail, string|int $mailbox): void
{
$mailbox = (string) $mailbox;
try {
$existing = $voicemail->getMailbox($mailbox);
if (!empty($existing)) {
$voicemail->delMailbox($mailbox);
}
} catch (Throwable $e) {
logmsg("voicemail cleanup skipped for {$mailbox}: " . $e->getMessage());
}
}
function removeUserAndDevice($core, string|int $extension): void
{
$extension = (string) $extension;
try {
$device = $core->getDevice($extension);
if (!empty($device)) {
$core->delDevice($extension);
}
} catch (Throwable $e) {
logmsg("device cleanup skipped for {$extension}: " . $e->getMessage());
}
try {
$user = $core->getUser($extension);
if (!empty($user)) {
$core->delUser($extension);
}
} catch (Throwable $e) {
logmsg("user cleanup skipped for {$extension}: " . $e->getMessage());
}
}
function addMailboxIfNeeded($voicemail, string|int $extension, string $name, ?array $mailbox): void
{
$extension = (string) $extension;
if ($mailbox === null) {
return;
}
$voicemail->addMailbox($extension, [
'vm' => 'enabled',
'vmcontext' => 'default',
'vmpwd' => $mailbox['pwd'],
'pwd' => $mailbox['pwd'],
'name' => $name,
'email' => $mailbox['email'],
'pager' => $mailbox['pager'],
'options' => $mailbox['options'],
]);
}
function addExtension($core, $voicemail, string|int $extension, array $config): void
{
$extension = (string) $extension;
$hasVoicemail = $config['voicemail'] !== null;
$alwaysRecord = str_contains($config['recording'], 'Always');
addMailboxIfNeeded($voicemail, $extension, $config['name'], $config['voicemail']);
$core->addUser($extension, [
'password' => '',
'name' => $config['name'],
'ringtimer' => (string) $config['ringtimer'],
'noanswer' => '',
'recording' => $config['recording'],
'outboundcid' => '',
'sipname' => $config['sipname'],
'recording_in_external' => recordingMode($config['recording'], $alwaysRecord),
'recording_out_external' => recordingMode($config['recording'], $alwaysRecord),
'recording_in_internal' => recordingMode($config['recording'], $alwaysRecord),
'recording_out_internal' => recordingMode($config['recording'], $alwaysRecord),
'recording_ondemand' => 'disabled',
'mohclass' => 'default',
'noanswer_dest' => '',
'busy_dest' => '',
'chanunavail_dest' => '',
'voicemail' => $hasVoicemail ? 'default' : 'novm',
'callwaiting' => 'enabled',
'pinless' => 'disabled',
'device' => $extension,
]);
$settings = $core->generateDefaultDeviceSettings($config['tech'], $extension, $config['name']);
setSetting($settings, 'secret', $config['secret']);
setSetting($settings, 'callerid', $config['name'] . ' <' . $extension . '>');
setSetting($settings, 'mailbox', $extension . '@device');
setSetting($settings, 'context', 'from-internal');
setSetting($settings, 'account', $extension);
setSetting($settings, 'description', $config['name']);
if ($config['tech'] === 'iax2') {
setSetting($settings, 'transfer', 'no');
setSetting($settings, 'host', 'dynamic');
setSetting($settings, 'type', 'friend');
setSetting($settings, 'port', '4569');
setSetting($settings, 'qualify', 'yes');
setSetting($settings, 'setvar', 'REALCALLERIDNUM=' . $extension);
} else {
setSetting($settings, 'defaultuser', $extension);
setSetting($settings, 'dtmfmode', 'rfc4733');
setSetting($settings, 'allow', 'ulaw,alaw,gsm');
setSetting($settings, 'max_contacts', '1');
setSetting($settings, 'remove_existing', 'yes');
setSetting($settings, 'rewrite_contact', 'yes');
setSetting($settings, 'rtp_symmetric', 'yes');
setSetting($settings, 'force_rport', 'yes');
setSetting($settings, 'direct_media', 'no');
}
$core->addDevice($extension, $config['tech'], $settings);
}
function deleteTrunkByName($core, string $name): void
{
foreach ($core->listTrunks() as $trunk) {
if (($trunk['name'] ?? '') === $name) {
$core->deleteTrunk((string) $trunk['trunkid'], $trunk['tech'] ?? null);
}
}
}
function deleteRoutesByName($routes, array $names): void
{
foreach ($routes->listAll() as $route) {
if (in_array($route['name'] ?? '', $names, true)) {
$routes->deleteById((string) $route['route_id']);
}
}
}
logmsg('Cleaning temporary and managed extension state');
foreach (array_merge($cleanupExtensions, array_keys($extensions)) as $extension) {
removeUserAndDevice($core, $extension);
removeMailbox($voicemail, $extension);
}
logmsg('Cleaning managed trunks, routes, ring groups, conferences, and DIDs');
foreach ($managedTrunks as $name) {
deleteTrunkByName($core, $name);
}
deleteRoutesByName($routes, $managedRoutes);
foreach ($ringGroupData as $group) {
try {
$ringgroups->delete($group['grpnum']);
} catch (Throwable $e) {
}
}
foreach (['810', '811'] as $room) {
try {
if (!empty($conferences->getConference($room))) {
$conferences->deleteConference($room);
}
} catch (Throwable $e) {
}
}
foreach ($incomingRoutes as $did) {
$core->delDID($did['extension'], $did['cidnum']);
}
try {
$disa->delete(1);
} catch (Throwable $e) {
}
$ivr->deleteEntriesById(4);
$ivr->deleteDetailsById(4);
logmsg('Creating trunks');
$bw1Id = $core->addTrunk('bw1out', 'sip', [
'channelid' => 'bw1out',
'outcid' => '+18017835555',
'dialoutprefix' => '',
'usercontext' => '7835555-in',
'userconfig' => implode("\n", [
'host=216.82.224.202',
'type=user',
'context=from-trunk',
'nat=yes',
]),
'peerdetails' => implode("\n", [
'host=216.82.224.202',
'type=peer',
'nat=yes',
]),
'register' => '',
]);
$bw2Id = $core->addTrunk('bw2out', 'sip', [
'channelid' => 'bw2out',
'outcid' => '+18017835555',
'dialoutprefix' => '',
'usercontext' => '',
'userconfig' => '',
'peerdetails' => implode("\n", [
'host=216.82.225.202',
'type=peer',
'nat=yes',
]),
'register' => '',
]);
$binfoneId = $core->addTrunk('binfone', 'iax2', [
'channelid' => 'binfone',
'outcid' => '8017835555',
'dialoutprefix' => '',
'usercontext' => '',
'userconfig' => '',
'peerdetails' => implode("\n", [
'host=iax-2.binfone.com',
'username=101667',
'secret=6pvb27vv',
'type=peer',
]),
'register' => '',
]);
$core->chansipToPJSIP('', (string) $bw1Id);
$core->chansipToPJSIP('', (string) $bw2Id);
logmsg('Creating extensions');
foreach ($extensions as $extension => $config) {
addExtension($core, $voicemail, $extension, $config);
}
logmsg('Creating recording and conferences');
$existingRecording = $recordings->getRecordingsById(5);
if (!empty($existingRecording)) {
$recordings->updateRecording(5, 'Welcome-to-Elphel', 'No long description available', 'custom/Welcome-to-Elphel');
} else {
$recordings->addRecordingWithId(5, 'Welcome-to-Elphel', 'No long description available', 'custom/Welcome-to-Elphel');
}
$conferences->addConference('810', 'Elphel conference', '', '', 'ciMsr', null, '', 0, '', 21600);
$conferences->addConference('811', 'Elphel public conf', '', '', '', null, '', 0, '', 21600);
logmsg('Creating ring groups');
foreach ($ringGroupData as $group) {
$ringgroups->add(
$group['grpnum'],
$group['strategy'],
$group['grptime'],
$group['grplist'],
$group['postdest'],
$group['description']
);
}
logmsg('Creating DISA and IVR');
$disa->edit(1, [
'displayname' => 'elphel-disa',
'pin' => '8562089',
'cid' => '',
'context' => 'from-internal',
'digittimeout' => '5',
'resptimeout' => '20',
'needconf' => '',
'hangup' => '',
'keepcid' => 0,
'recording' => '',
]);
$ivr->saveDetails([
'id' => 4,
'name' => 'Elphel-main-IVR',
'description' => '',
'announcement' => 5,
'directdial' => 'ext-local',
'invalid_loops' => 2,
'invalid_retry_recording' => 'default',
'invalid_destination' => 'ext-group,609,1',
'timeout_enabled' => 'on',
'invalid_recording' => '',
'retvm' => '',
'timeout_time' => 10,
'timeout_recording' => 'default',
'timeout_retry_recording' => 'default',
'timeout_destination' => 'ext-group,609,1',
'timeout_loops' => 2,
'timeout_append_announce' => 0,
'invalid_append_announce' => 0,
'timeout_ivr_ret' => 0,
'invalid_ivr_ret' => 0,
'alertinfo' => '',
'rvolume' => '',
'strict_dial_timeout' => 2,
'accept_pound_key' => 0,
]);
$ivr->saveEntries(4, [
'ext' => ['1', '2', '3', '5', '6', '7', 't'],
'goto' => [
'ext-group,609,1',
'ext-group,610,1',
'ext-group,611,1',
'disa,1,1',
'ext-meetme,811,1',
'ext-meetme,810,1',
'ext-group,609,1',
],
'ivr_ret' => ['0', '0', '0', '0', '0', '0', '0'],
]);
logmsg('Creating inbound routes');
foreach ($incomingRoutes as $did) {
$core->addDID($did);
}
logmsg('Creating outbound routes');
$routes->add(
'elphel-911',
'',
'',
'',
'yes',
'',
'default',
0,
[
['prepend_digits' => '', 'match_pattern_prefix' => '', 'match_pattern_pass' => '911', 'match_cid' => ''],
],
[(string) $bw1Id, (string) $bw2Id, (string) $binfoneId],
'bottom'
);
$routes->add(
'elphel-manual-binfone',
'',
'',
'',
'',
'',
'default',
0,
[
['prepend_digits' => '', 'match_pattern_prefix' => '92', 'match_pattern_pass' => 'XXXXXX.', 'match_cid' => ''],
],
[(string) $binfoneId],
'bottom'
);
$routes->add(
'elphel-manual-bandwidth',
'',
'',
'',
'',
'',
'default',
0,
[
['prepend_digits' => '+', 'match_pattern_prefix' => '94', 'match_pattern_pass' => 'XXXXXX.', 'match_cid' => ''],
],
[(string) $bw1Id, (string) $bw2Id],
'bottom'
);
$routes->add(
'elphel-utah-local',
'',
'',
'',
'',
'',
'default',
0,
[
['prepend_digits' => '+1801', 'match_pattern_prefix' => '', 'match_pattern_pass' => 'NXXXXXX', 'match_cid' => ''],
['prepend_digits' => '+1', 'match_pattern_prefix' => '', 'match_pattern_pass' => '801NXXXXXX', 'match_cid' => ''],
['prepend_digits' => '+', 'match_pattern_prefix' => '', 'match_pattern_pass' => '1801NXXXXXX', 'match_cid' => ''],
],
[(string) $bw1Id, (string) $bw2Id],
'bottom'
);
$routes->add(
'elphel-tollfree',
'',
'',
'',
'',
'',
'default',
0,
[
['prepend_digits' => '+', 'match_pattern_prefix' => '', 'match_pattern_pass' => '1800NXXXXXX', 'match_cid' => ''],
['prepend_digits' => '+', 'match_pattern_prefix' => '', 'match_pattern_pass' => '1866NXXXXXX', 'match_cid' => ''],
['prepend_digits' => '+', 'match_pattern_prefix' => '', 'match_pattern_pass' => '1877NXXXXXX', 'match_cid' => ''],
['prepend_digits' => '+', 'match_pattern_prefix' => '', 'match_pattern_pass' => '1888NXXXXXX', 'match_cid' => ''],
],
[(string) $binfoneId, (string) $bw1Id, (string) $bw2Id],
'bottom'
);
$routes->add(
'elphel-us-national',
'',
'',
'',
'',
'',
'default',
0,
[
['prepend_digits' => '1', 'match_pattern_prefix' => '', 'match_pattern_pass' => 'NXXNXXXXXX', 'match_cid' => ''],
['prepend_digits' => '', 'match_pattern_prefix' => '', 'match_pattern_pass' => '1NXXNXXXXXX', 'match_cid' => ''],
],
[(string) $binfoneId, (string) $bw1Id, (string) $bw2Id],
'bottom'
);
$routes->add(
'elphel-international',
'',
'',
'',
'',
'',
'default',
0,
[
['prepend_digits' => '', 'match_pattern_prefix' => '', 'match_pattern_pass' => '011.', 'match_cid' => ''],
],
[(string) $binfoneId],
'bottom'
);
logmsg('Migration objects created successfully');
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment