-- PBX Multi-Tenant Realtime Views for Asterisk ODBC -- Executed against the 'asterisk' database on PostgreSQL VIP 172.17.3.14 -- Prerequisites: postgres_fdw extension, asterisk_reader user -- ============================================================================ -- Step 1: Create asterisk_reader user (run as superuser on primary) -- ============================================================================ DO $$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'asterisk_reader') THEN CREATE ROLE asterisk_reader WITH LOGIN PASSWORD 'PLACEHOLDER_CHANGE_ME'; END IF; END $$; -- ============================================================================ -- Step 2: Install extensions -- ============================================================================ CREATE EXTENSION IF NOT EXISTS postgres_fdw; CREATE EXTENSION IF NOT EXISTS pgcrypto; -- ============================================================================ -- Step 3: Create foreign server pointing to gsc_admin database -- ============================================================================ DROP SERVER IF EXISTS gsc_admin_server CASCADE; CREATE SERVER gsc_admin_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (host '127.0.0.1', port '5432', dbname 'gsc_admin'); -- User mapping: asterisk DB superuser -> gsc_admin reader CREATE USER MAPPING IF NOT EXISTS FOR CURRENT_USER SERVER gsc_admin_server OPTIONS (user 'asterisk_reader', password 'PLACEHOLDER_CHANGE_ME'); -- ============================================================================ -- Step 4: Create foreign tables mirroring gsc_admin pbx_* tables -- ============================================================================ DROP SCHEMA IF EXISTS gsc CASCADE; CREATE SCHEMA gsc; -- pbx_trunks CREATE FOREIGN TABLE gsc.pbx_trunks ( id uuid, tenant_id uuid, provider_id uuid, name varchar(128), trunk_type varchar(16), status varchar(16), host varchar(256), port int, transport varchar(8), username varchar(128), password_encrypted bytea, auth_realm varchar(128), from_domain varchar(256), from_user varchar(128), register boolean, register_frequency int, codecs text[], dtmf_mode varchar(16), nat_mode varchar(16), max_channels int, context varchar(64), trust_id_inbound boolean, trust_id_outbound boolean, send_pai boolean, send_rpid boolean, send_diversion boolean, direct_media boolean, rtp_symmetric boolean, rewrite_contact boolean, ice_support boolean, force_rport boolean, timers varchar(16), timers_min_se int, timers_sess_expires int, media_encryption varchar(16), media_encryption_optimistic boolean, max_contacts int, default_expiration int, minimum_expiration int, maximum_expiration int, qualify_timeout numeric(5,2), match_by_ip boolean, match_ips text[], outbound_caller_id_name varchar(64), outbound_caller_id_number varchar(32), t38_udptl boolean, t38_udptl_ec varchar(16), t38_udptl_maxdatagram int, t38_udptl_nat boolean, fax_detect boolean, rtp_timeout int, rtp_timeout_hold int, rtp_keepalive int, inband_progress boolean, contact_user varchar(128), line boolean, client_uri varchar(256), server_uri varchar(256), outbound_proxy varchar(256), registration_expiration int, registration_retry_interval int, pjsip_options jsonb, is_active boolean, created_at timestamptz, updated_at timestamptz ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_trunks'); -- pbx_trunk_dids CREATE FOREIGN TABLE gsc.pbx_trunk_dids ( id uuid, trunk_id uuid, tenant_id uuid, did_number varchar(32), description varchar(256), destination_type varchar(32), destination_id uuid, is_active boolean ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_trunk_dids'); -- pbx_extensions CREATE FOREIGN TABLE gsc.pbx_extensions ( id uuid, tenant_id uuid, user_id uuid, extension varchar(16), name varchar(128), extension_type varchar(16), caller_id_name varchar(64), caller_id_number varchar(32), outbound_caller_id_number varchar(32), sip_username varchar(64), sip_password_encrypted bytea, transport varchar(8), codecs text[], dtmf_mode varchar(16), nat_mode varchar(16), ring_timeout int, call_waiting boolean, dnd_enabled boolean, forward_all_enabled boolean, forward_all_destination varchar(64), forward_busy_enabled boolean, forward_busy_destination varchar(64), forward_no_answer_enabled boolean, forward_no_answer_destination varchar(64), forward_no_answer_timeout int, voicemail_enabled boolean, voicemail_id uuid, recording_mode varchar(16), pjsip_options jsonb, is_active boolean, created_at timestamptz, updated_at timestamptz ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_extensions'); -- pbx_voicemail_boxes CREATE FOREIGN TABLE gsc.pbx_voicemail_boxes ( id uuid, tenant_id uuid, extension_id uuid, mailbox varchar(16), name varchar(128), pin_encrypted bytea, email varchar(256), email_attachment boolean, email_delete_after_send boolean, pager_email varchar(256), max_message_length int, max_messages int, say_caller_id boolean, say_duration boolean, review_enabled boolean, operator_enabled boolean, envelope_enabled boolean, timezone varchar(64), mwi_enabled boolean, is_active boolean, created_at timestamptz ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_voicemail_boxes'); -- pbx_inbound_routes CREATE FOREIGN TABLE gsc.pbx_inbound_routes ( id uuid, tenant_id uuid, name varchar(128), did_number varchar(32), caller_id_pattern varchar(64), destination_type varchar(32), destination_id uuid, destination_data varchar(256), priority int, is_active boolean, trunk_id uuid ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_inbound_routes'); -- pbx_outbound_routes CREATE FOREIGN TABLE gsc.pbx_outbound_routes ( id uuid, tenant_id uuid, name varchar(128), priority int, dial_patterns text[], prepend varchar(16), prefix varchar(16), strip_digits int, override_caller_id boolean, caller_id_name varchar(64), caller_id_number varchar(32), is_emergency boolean, is_active boolean ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_outbound_routes'); -- pbx_outbound_route_trunks CREATE FOREIGN TABLE gsc.pbx_outbound_route_trunks ( id uuid, outbound_route_id uuid, trunk_id uuid, priority int ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_outbound_route_trunks'); -- pbx_tenant_settings CREATE FOREIGN TABLE gsc.pbx_tenant_settings ( id uuid, tenant_id uuid, extension_length int, extension_start int, default_codecs text[], default_outbound_cid_name varchar(64), default_outbound_cid_number varchar(32), default_recording_mode varchar(16), voicemail_email_from varchar(256), default_moh_class varchar(64), default_language varchar(8), timezone varchar(64), max_extensions int, max_trunks int, max_concurrent_calls int ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_tenant_settings'); -- pbx_queues CREATE FOREIGN TABLE gsc.pbx_queues ( id uuid, tenant_id uuid, extension varchar(16), name varchar(128), strategy varchar(32), timeout int, retry int, wrapup_time int, max_wait_time int, max_callers int, moh_class varchar(64), is_active boolean ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_queues'); -- pbx_queue_members CREATE FOREIGN TABLE gsc.pbx_queue_members ( id uuid, queue_id uuid, extension_id uuid, penalty int, paused boolean, dynamic boolean, membername varchar(128), interface varchar(128), state_interface varchar(128) ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'pbx_queue_members'); -- tenants (for name resolution) CREATE FOREIGN TABLE gsc.tenants ( id uuid, code varchar(32), name varchar(128), is_active boolean ) SERVER gsc_admin_server OPTIONS (schema_name 'public', table_name 'tenants'); -- ============================================================================ -- Step 5: Create PJSIP Sorcery Views (Asterisk Realtime) -- ============================================================================ -- ps_endpoints: PJSIP endpoint objects for trunks CREATE OR REPLACE VIEW public.ps_endpoints AS -- Trunk endpoints SELECT t.id::text AS id, 'endpoint' AS type, COALESCE(t.transport, 'udp')::text AS transport, COALESCE(t.context, 'from-trunk-' || ten.code) AS context, CASE WHEN array_length(t.codecs, 1) > 0 THEN array_to_string(t.codecs, ',') ELSE 'ulaw,alaw' END AS allow, 'all' AS disallow, t.username AS auth, t.id::text AS aors, COALESCE(t.outbound_caller_id_name, '') || ' <' || COALESCE(t.outbound_caller_id_number, '') || '>' AS callerid, CASE WHEN t.direct_media THEN 'yes' ELSE 'no' END AS direct_media, CASE WHEN COALESCE(t.rtp_symmetric, true) THEN 'yes' ELSE 'no' END AS rtp_symmetric, CASE WHEN COALESCE(t.force_rport, true) THEN 'yes' ELSE 'no' END AS force_rport, CASE WHEN COALESCE(t.rewrite_contact, true) THEN 'yes' ELSE 'no' END AS rewrite_contact, CASE WHEN t.trust_id_inbound THEN 'yes' ELSE 'no' END AS trust_id_inbound, CASE WHEN t.send_rpid THEN 'yes' ELSE 'no' END AS send_rpid, CASE WHEN t.send_pai THEN 'yes' ELSE 'no' END AS send_pai, COALESCE(t.dtmf_mode, 'rfc4733')::text AS dtmf_mode, CASE WHEN t.ice_support THEN 'yes' ELSE 'no' END AS ice_support, CASE WHEN t.t38_udptl THEN 'yes' ELSE 'no' END AS t38_udptl, COALESCE(t.t38_udptl_ec, 'none')::text AS t38_udptl_ec, COALESCE(t.t38_udptl_maxdatagram, 0) AS t38_udptl_maxdatagram, CASE WHEN t.t38_udptl_nat THEN 'yes' ELSE 'no' END AS t38_udptl_nat, CASE WHEN COALESCE(t.inband_progress, false) THEN 'yes' ELSE 'no' END AS inband_progress, COALESCE(t.timers, 'yes')::text AS timers, COALESCE(t.timers_min_se, 90) AS timers_min_se, COALESCE(t.timers_sess_expires, 1800) AS timers_sess_expires, COALESCE(t.rtp_timeout, 60) AS rtp_timeout, COALESCE(t.rtp_timeout_hold, 300) AS rtp_timeout_hold, COALESCE(t.rtp_keepalive, 0) AS rtp_keepalive, COALESCE(t.media_encryption, 'no')::text AS media_encryption, CASE WHEN t.media_encryption_optimistic THEN 'yes' ELSE 'no' END AS media_encryption_optimistic, t.tenant_id FROM gsc.pbx_trunks t JOIN gsc.tenants ten ON ten.id = t.tenant_id WHERE t.is_active = true AND t.status = 'active'; -- ps_auths: PJSIP auth objects for trunks CREATE OR REPLACE VIEW public.ps_auths AS SELECT t.id::text AS id, 'auth' AS type, 'userpass' AS auth_type, t.username, -- Password: stored as AES-256-GCM encrypted bytea in gsc_admin. -- For ODBC realtime we store a plaintext copy in a separate field. -- This view returns the username; actual password set via ops-api sync. ''::text AS password, COALESCE(t.auth_realm, t.from_domain, t.host, '')::text AS realm, t.tenant_id FROM gsc.pbx_trunks t JOIN gsc.tenants ten ON ten.id = t.tenant_id WHERE t.is_active = true AND t.status = 'active' AND t.username IS NOT NULL AND t.username != ''; -- ps_aors: PJSIP AOR (Address of Record) objects for trunks CREATE OR REPLACE VIEW public.ps_aors AS SELECT t.id::text AS id, 'aor' AS type, 'sip:' || t.host || ':' || COALESCE(t.port, 5060) AS contact, COALESCE(t.max_contacts, 1) AS max_contacts, COALESCE(t.qualify_timeout, 3.0)::numeric(5,2) AS qualify_timeout, 30 AS qualify_frequency, COALESCE(t.default_expiration, 3600) AS default_expiration, COALESCE(t.minimum_expiration, 60) AS minimum_expiration, COALESCE(t.maximum_expiration, 7200) AS maximum_expiration, t.tenant_id FROM gsc.pbx_trunks t WHERE t.is_active = true AND t.status = 'active'; -- ps_endpoint_id_ips: IP-based endpoint identification for trunks CREATE OR REPLACE VIEW public.ps_endpoint_id_ips AS SELECT t.id::text AS endpoint, ip AS match, t.tenant_id FROM gsc.pbx_trunks t, LATERAL unnest(t.match_ips) AS ip WHERE t.is_active = true AND t.status = 'active' AND t.match_by_ip = true AND t.match_ips IS NOT NULL AND array_length(t.match_ips, 1) > 0; -- ps_registrations: outbound registration objects for trunks that register CREATE OR REPLACE VIEW public.ps_registrations AS SELECT t.id::text AS id, 'registration' AS type, COALESCE(t.transport, 'udp')::text AS transport, COALESCE(t.outbound_proxy, '')::text AS outbound_proxy, COALESCE(t.server_uri, 'sip:' || t.host || ':' || COALESCE(t.port, 5060))::text AS server_uri, COALESCE(t.client_uri, 'sip:' || t.username || '@' || COALESCE(t.from_domain, t.host))::text AS client_uri, COALESCE(t.contact_user, t.username, '')::text AS contact_user, COALESCE(t.registration_expiration, 3600) AS expiration, COALESCE(t.registration_retry_interval, 60) AS retry_interval, CASE WHEN COALESCE(t.line, false) THEN 'yes' ELSE 'no' END AS line, t.id::text AS outbound_auth, t.tenant_id FROM gsc.pbx_trunks t WHERE t.is_active = true AND t.status = 'active' AND t.register = true; -- voicemail: Asterisk voicemail boxes CREATE OR REPLACE VIEW public.voicemail AS SELECT vm.id::text AS uniqueid, ten.code AS context, vm.mailbox, ''::text AS password, vm.name AS fullname, COALESCE(vm.email, '') AS email, COALESCE(vm.pager_email, '') AS pager, CASE WHEN COALESCE(vm.email_attachment, true) THEN 'yes' ELSE 'no' END AS attach, CASE WHEN COALESCE(vm.email_delete_after_send, false) THEN 'yes' ELSE 'no' END AS "delete", COALESCE(vm.max_message_length, 180) AS maxsecs, COALESCE(vm.max_messages, 100) AS maxmsg, COALESCE(vm.timezone, 'UTC') AS tz, CASE WHEN COALESCE(vm.say_caller_id, true) THEN 'yes' ELSE 'no' END AS saycid, CASE WHEN COALESCE(vm.say_duration, true) THEN 'yes' ELSE 'no' END AS sayduration, CASE WHEN COALESCE(vm.review_enabled, true) THEN 'yes' ELSE 'no' END AS review, CASE WHEN COALESCE(vm.operator_enabled, true) THEN 'yes' ELSE 'no' END AS operator, CASE WHEN COALESCE(vm.envelope_enabled, true) THEN 'yes' ELSE 'no' END AS envelope, vm.tenant_id FROM gsc.pbx_voicemail_boxes vm JOIN gsc.tenants ten ON ten.id = vm.tenant_id WHERE vm.is_active = true; -- ============================================================================ -- Step 6: func_odbc lookup views -- ============================================================================ -- Tenant lookup by DID number CREATE OR REPLACE VIEW public.did_tenant_lookup AS SELECT td.did_number, td.tenant_id, ten.code AS tenant_code FROM gsc.pbx_trunk_dids td JOIN gsc.tenants ten ON ten.id = td.tenant_id WHERE td.is_active = true; -- Inbound route lookup by DID CREATE OR REPLACE VIEW public.inbound_route_lookup AS SELECT ir.did_number, ir.tenant_id, ten.code AS tenant_code, ir.destination_type, ir.destination_id, ir.destination_data, ir.priority FROM gsc.pbx_inbound_routes ir JOIN gsc.tenants ten ON ten.id = ir.tenant_id WHERE ir.is_active = true ORDER BY ir.priority ASC; -- Outbound route lookup with trunk priority CREATE OR REPLACE VIEW public.outbound_route_lookup AS SELECT obr.id AS route_id, obr.tenant_id, ten.code AS tenant_code, obr.dial_patterns, obr.prepend, obr.prefix, obr.strip_digits, obr.priority AS route_priority, obr.override_caller_id, obr.caller_id_name, obr.caller_id_number, obr.is_emergency, ort.trunk_id, ort.priority AS trunk_priority, t.host AS trunk_host, t.port AS trunk_port, t.username AS trunk_username FROM gsc.pbx_outbound_routes obr JOIN gsc.tenants ten ON ten.id = obr.tenant_id LEFT JOIN gsc.pbx_outbound_route_trunks ort ON ort.outbound_route_id = obr.id LEFT JOIN gsc.pbx_trunks t ON t.id = ort.trunk_id WHERE obr.is_active = true ORDER BY obr.priority ASC, ort.priority ASC; -- ============================================================================ -- Step 7: SIP password sync table (plaintext for Asterisk ODBC) -- ============================================================================ -- Asterisk cannot decrypt AES-256-GCM from the DB. We keep a sync table -- that ops-api populates with plaintext passwords. CREATE TABLE IF NOT EXISTS public.sip_passwords ( entity_id uuid PRIMARY KEY, entity_type varchar(16) NOT NULL DEFAULT 'trunk', -- 'trunk' or 'extension' password text NOT NULL, updated_at timestamptz NOT NULL DEFAULT NOW() ); -- Update ps_auths to use sip_passwords CREATE OR REPLACE VIEW public.ps_auths AS SELECT t.id::text AS id, 'auth' AS type, 'userpass' AS auth_type, t.username, COALESCE(sp.password, '')::text AS password, COALESCE(t.auth_realm, t.from_domain, t.host, '')::text AS realm, t.tenant_id FROM gsc.pbx_trunks t JOIN gsc.tenants ten ON ten.id = t.tenant_id LEFT JOIN public.sip_passwords sp ON sp.entity_id = t.id AND sp.entity_type = 'trunk' WHERE t.is_active = true AND t.status = 'active' AND t.username IS NOT NULL AND t.username != ''; -- ============================================================================ -- Step 8: Grant permissions -- ============================================================================ GRANT USAGE ON SCHEMA public TO asterisk_reader; GRANT USAGE ON SCHEMA gsc TO asterisk_reader; GRANT SELECT ON ALL TABLES IN SCHEMA public TO asterisk_reader; GRANT SELECT ON ALL TABLES IN SCHEMA gsc TO asterisk_reader; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO asterisk_reader; -- Allow asterisk_reader to read sip_passwords (and in future, insert/update via ops-api) GRANT SELECT, INSERT, UPDATE, DELETE ON public.sip_passwords TO asterisk_reader; -- ============================================================================ -- Step 9: pg_hba.conf additions (must be done via Patroni config) -- Add these lines to patroni.yml postgresql.pg_hba section: -- - host asterisk asterisk_reader 172.17.6.40/32 scram-sha-256 -- - host asterisk asterisk_reader 172.17.6.41/32 scram-sha-256 -- - host asterisk asterisk_reader 172.17.8.20/32 scram-sha-256 -- - host gsc_admin asterisk_reader 172.17.3.11/32 scram-sha-256 -- - host gsc_admin asterisk_reader 172.17.3.12/32 scram-sha-256 -- - host gsc_admin asterisk_reader 172.17.3.13/32 scram-sha-256 -- ============================================================================