Building a Custom CLI Management Tool for VoIP Servers
Shell Script Wrapper for Asterisk + ViciDial + MySQL
Table of Contents
- Introduction
- Architecture Overview
- Prerequisites
- The Complete Script
- Installation
- Command Reference
- Bash Completion
- Usage Examples
- Adding New Commands
- Usage Logging & Audit Trail
- Team Standardization
- Adapting for Other PBX Systems
- Security Considerations
- Production Checklist
Introduction
The Problem
Managing a VoIP server means constantly switching between Asterisk CLI commands, MySQL queries, log files, system utilities, and service management tools. A typical troubleshooting session might look like this:
# Check if SIP trunks are up
asterisk -rx "sip show peers" | grep -E "(trunk|provider)"
# How many calls are active?
asterisk -rx "core show channels concise" | wc -l
# Which agents are logged in?
mysql -u cron -pSECRET asterisk -e "SELECT user, full_name FROM vicidial_live_agents;"
# Tail the Asterisk log for errors
tail -f /var/log/asterisk/messages | grep ERROR
# Check disk space
df -h
# Check mail spool size (Asterisk loves generating mail)
du -sh /var/mail/root
Every one of these commands requires you to remember exact syntax, table names, log paths, and credentials. Multiply this by four or five servers and a team of three admins, and you have a recipe for wasted time, inconsistent procedures, and avoidable mistakes.
The Solution
Build a single command-line tool -- a shell script wrapper -- that encapsulates every common operation behind short, memorable subcommands:
vinit sip # SIP peer status
vinit calls # Active call count + details
vinit agents # Logged-in agents
vinit log # Tail Asterisk log
vinit db # MySQL console
vinit status # System overview
One command. No credentials to remember. No syntax to look up. Consistent across every server in your fleet.
Why It Works
This is not a complex orchestration framework or a web dashboard. It is a 200-line Bash script that solves a real daily problem:
- Speed. Typing
vinit callsis faster thanasterisk -rx "core show channels concise" | wc -l. Over hundreds of daily invocations, the time savings compound. - Accuracy. The MySQL queries are written once, tested once, and used forever. No more typos in table names or forgotten WHERE clauses.
- Onboarding. A new admin can run
vinit helpand immediately see every available operation. No tribal knowledge required. - Consistency. Every team member runs the same commands and gets the same output format. Troubleshooting conversations become easier: "What does
vinit agentsshow on Charlie?" - Safety. Dangerous operations (database writes, service restarts) are either omitted or gated behind confirmation prompts. Read-only queries are the default.
This tool has been running in production across a multi-server ViciDial deployment, used daily by admins managing SIP trunks, agents, campaigns, and call routing. It handles the 80% of operations that you perform repeatedly, and gets out of your way for the other 20%.
Architecture Overview
+------------------+
| Admin types: |
| vinit <cmd> |
+--------+---------+
|
v
+------------------+
| /usr/local/bin |
| /vinit |
| (Bash script) |
+--------+---------+
|
+--------------+--------------+
| | |
v v v
+-------+----+ +-----+------+ +----+-------+
| Asterisk | | MySQL | | System |
| CLI (AMI) | | (asterisk | | Utilities |
| | | database) | | |
+------------+ +------------+ +------------+
- sip show - vicidial_ - tail, df
peers live_agents - du, uptime
- core show - vicidial_ - systemctl
channels users - mailq
- sip reload - system_ - postfix
settings
The script acts as a thin dispatcher. Each subcommand maps to one or more underlying operations: an Asterisk CLI command via asterisk -rx, a MySQL query, a systemctl call, or a standard Unix utility. The script adds no abstraction layers, daemons, or dependencies beyond what is already on the server.
Prerequisites
- Operating System: Any Linux distribution (CentOS, openSUSE/ViciBox, Debian, Ubuntu)
- Asterisk: Version 11+ (tested through Asterisk 20)
- ViciDial: Any version with standard database schema
- MySQL/MariaDB: With a read-only or limited-privilege user for queries
- Bash: Version 4+ (standard on all modern Linux)
- Root access: The script runs Asterisk CLI commands that require root or the
asteriskgroup
No additional packages, languages, or frameworks are needed.
The Complete Script
Save this as /usr/local/bin/vinit. The script is presented in full, ready to deploy. Every placeholder is marked clearly.
#!/bin/bash
#============================================================================
# vinit - VoIP Server Management CLI
#
# A single command-line tool that wraps common Asterisk, ViciDial, MySQL,
# and system management tasks into quick subcommands.
#
# Usage: vinit <command> [arguments]
# Help: vinit help
#
# Installation:
# cp vinit /usr/local/bin/vinit
# chmod +x /usr/local/bin/vinit
#
# Author: Your Name / Your Team
# Version: 1.0
# License: MIT
#============================================================================
VERSION="1.0"
#============================================================================
# CONFIGURATION
# Edit these variables to match your server environment.
#============================================================================
# MySQL credentials for read-only queries.
# Use a dedicated user with SELECT-only privileges.
DB_USER="your_db_user"
DB_PASS="your_db_password"
DB_NAME="asterisk"
# Log file paths (vary by distribution)
AST_LOG="/var/log/asterisk/messages"
AST_LOG_FULL="/var/log/asterisk/full"
VICI_LOG_DIR="/var/log/astguiclient"
# Mail spool path
MAIL_SPOOL="/var/mail/root"
# SmokePing data directory (if installed)
SMOKEPING_DATA="/opt/smokeping/data"
# Server IP (used in URL output). Replace with your server IP or hostname.
SERVER_IP="YOUR_SERVER_IP"
#============================================================================
# HELPER FUNCTIONS
#============================================================================
# Run a MySQL query and suppress the password warning.
# Usage: db_query "SELECT ..."
db_query() {
mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -e "$1" 2>/dev/null
}
# Run a MySQL query, return raw value (no headers, no table formatting).
# Usage: db_value "SELECT COUNT(*) FROM ..."
db_value() {
mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME" -N -e "$1" 2>/dev/null
}
# Run an Asterisk CLI command.
# Usage: ast_cmd "sip show peers"
ast_cmd() {
asterisk -rx "$1"
}
# Print a section header.
header() {
echo "=== $1 ==="
echo ""
}
#============================================================================
# HELP
#============================================================================
show_help() {
cat <<'HELPTEXT'
vinit - VoIP Server Management CLI (v1.0)
=============================================
Usage: vinit <command> [arguments]
SIP MANAGEMENT:
sip Show all SIP peer status
sip <peer> Show detailed info for a specific SIP peer
sip reload Reload SIP configuration (no restart)
CALL MONITORING:
calls Show active calls with details
calls count Show only the active call count
channels Show active channels with codec/duration info
AGENT MANAGEMENT:
agents Show all logged-in agents
agents <user> Show detailed info for a specific agent
LOG ACCESS:
log Tail Asterisk messages log (Ctrl+C to exit)
log full Tail Asterisk full log (Ctrl+C to exit)
log grep <pattern> Search Asterisk logs for a pattern
DATABASE:
db Open interactive MySQL console (asterisk database)
db query "<sql>" Run a single SQL query and display results
MAIL MANAGEMENT:
mail Show mail spool size and queue status
mail clean Truncate the mail spool to free disk space
SMOKEPING:
smokeping status Show SmokePing service status
smokeping start Start SmokePing service
smokeping stop Stop SmokePing service
SYSTEM:
status Show system overview (uptime, disk, memory, Asterisk)
GENERAL:
help Show this help message
version Show version information
EXAMPLES:
vinit sip Check all SIP trunk registrations
vinit sip my_trunk Debug a specific SIP peer
vinit calls See what is happening right now
vinit agents Who is logged in?
vinit agents 1001 Details for agent 1001
vinit log grep "INVITE" Search logs for SIP INVITEs
vinit db query "SELECT COUNT(*) Quick database query
FROM vicidial_log
WHERE call_date > NOW() - INTERVAL 1 HOUR;"
vinit status Quick server health check
HELPTEXT
}
#============================================================================
# COMMAND IMPLEMENTATIONS
#============================================================================
# --- SIP MANAGEMENT ---
cmd_sip() {
case "${1:-}" in
"")
header "SIP Peers"
ast_cmd "sip show peers"
;;
reload)
echo "Reloading SIP configuration..."
ast_cmd "sip reload"
echo "SIP config reloaded. Verify with: vinit sip"
;;
*)
header "SIP Peer: $1"
ast_cmd "sip show peer $1"
;;
esac
}
# --- CALL MONITORING ---
cmd_calls() {
case "${1:-}" in
count)
local count
count=$(ast_cmd "core show channels concise" | wc -l)
echo "Active calls: $count"
;;
"")
local count
count=$(ast_cmd "core show channels concise" | wc -l)
echo "Active calls: $count"
echo ""
ast_cmd "core show channels verbose"
;;
*)
echo "Usage: vinit calls [count]"
;;
esac
}
cmd_channels() {
header "Active Channels"
ast_cmd "core show channels"
}
# --- AGENT MANAGEMENT ---
cmd_agents() {
case "${1:-}" in
"")
header "Logged-In Agents"
db_query "
SELECT
la.user,
vu.full_name,
la.status,
la.campaign_id,
la.calls_today,
la.last_call_time
FROM vicidial_live_agents la
JOIN vicidial_users vu ON la.user = vu.user
ORDER BY la.campaign_id, la.user;
"
echo ""
echo "Total: $(db_value "SELECT COUNT(*) FROM vicidial_live_agents;") agents logged in"
;;
*)
header "Agent Details: $1"
db_query "
SELECT
la.user,
vu.full_name,
la.status,
la.server_ip,
la.campaign_id,
la.closer_campaigns,
la.calls_today,
la.last_call_time,
la.last_call_finish,
la.lead_id,
la.callerid,
la.channel,
la.uniqueid,
la.pause_code
FROM vicidial_live_agents la
JOIN vicidial_users vu ON la.user = vu.user
WHERE la.user = '$1';
"
if [ "$(db_value "SELECT COUNT(*) FROM vicidial_live_agents WHERE user='$1';")" -eq 0 ]; then
echo "Agent '$1' is not currently logged in."
echo ""
echo "User record:"
db_query "
SELECT user, full_name, user_level, user_group, active
FROM vicidial_users
WHERE user = '$1';
"
fi
;;
esac
}
# --- LOG ACCESS ---
cmd_log() {
case "${1:-}" in
"")
echo "Tailing $AST_LOG (Ctrl+C to exit)..."
tail -f "$AST_LOG"
;;
full)
if [ -f "$AST_LOG_FULL" ]; then
echo "Tailing $AST_LOG_FULL (Ctrl+C to exit)..."
tail -f "$AST_LOG_FULL"
else
echo "Full log not found at $AST_LOG_FULL"
echo "Falling back to messages log..."
tail -f "$AST_LOG"
fi
;;
grep)
if [ -z "${2:-}" ]; then
echo "Usage: vinit log grep <pattern>"
echo "Example: vinit log grep INVITE"
exit 1
fi
header "Log Search: $2"
grep -i "$2" "$AST_LOG" | tail -100
echo ""
echo "(Showing last 100 matches from $AST_LOG)"
;;
*)
echo "Usage: vinit log [full|grep <pattern>]"
;;
esac
}
# --- DATABASE ---
cmd_db() {
case "${1:-}" in
"")
echo "Connecting to MySQL (database: $DB_NAME)..."
mysql -u "$DB_USER" -p"$DB_PASS" "$DB_NAME"
;;
query)
if [ -z "${2:-}" ]; then
echo "Usage: vinit db query \"SELECT ...\""
exit 1
fi
db_query "$2"
;;
*)
echo "Usage: vinit db [query \"<sql>\"]"
;;
esac
}
# --- MAIL MANAGEMENT ---
cmd_mail() {
case "${1:-}" in
"")
header "Mail Status"
echo "Mail spool:"
if [ -f "$MAIL_SPOOL" ]; then
echo " Size: $(du -sh "$MAIL_SPOOL" 2>/dev/null | cut -f1)"
echo " Lines: $(wc -l < "$MAIL_SPOOL" 2>/dev/null)"
else
echo " (empty or not found)"
fi
echo ""
echo "Mail queue:"
mailq 2>/dev/null | tail -5
;;
clean)
if [ -f "$MAIL_SPOOL" ]; then
local size
size=$(du -sh "$MAIL_SPOOL" 2>/dev/null | cut -f1)
echo "Current spool size: $size"
echo -n "Truncating $MAIL_SPOOL... "
: > "$MAIL_SPOOL"
echo "done."
echo "Freed: $size"
else
echo "Mail spool not found at $MAIL_SPOOL"
fi
;;
*)
echo "Usage: vinit mail [clean]"
;;
esac
}
# --- SMOKEPING ---
cmd_smokeping() {
case "${1:-}" in
status)
header "SmokePing Status"
systemctl status smokeping --no-pager 2>&1 | head -10
echo ""
if [ -d "$SMOKEPING_DATA" ]; then
local targets
targets=$(find "$SMOKEPING_DATA" -name "*.rrd" 2>/dev/null | wc -l)
echo "Monitoring targets: $targets"
fi
echo "Web UI: http://$SERVER_IP/smokeping/"
;;
start)
echo "Starting SmokePing..."
systemctl start smokeping
systemctl status smokeping --no-pager | head -5
;;
stop)
echo "Stopping SmokePing..."
systemctl stop smokeping
echo "SmokePing stopped."
;;
*)
echo "Usage: vinit smokeping [status|start|stop]"
;;
esac
}
# --- SYSTEM OVERVIEW ---
cmd_status() {
header "System Overview"
echo "Hostname: $(hostname)"
echo "Uptime: $(uptime -p 2>/dev/null || uptime)"
echo "Load: $(cat /proc/loadavg | cut -d' ' -f1-3)"
echo ""
echo "Memory:"
free -h | grep -E "^(Mem|Swap):" | while read -r line; do
echo " $line"
done
echo ""
echo "Disk:"
df -h / | tail -1 | awk '{printf " Root: %s used of %s (%s)\n", $3, $2, $5}'
echo ""
echo "Asterisk:"
if pgrep -x asterisk > /dev/null; then
local ast_ver
ast_ver=$(ast_cmd "core show version" | head -1)
echo " Status: RUNNING"
echo " Version: $ast_ver"
echo " Calls: $(ast_cmd "core show channels concise" | wc -l) active"
echo " Peers: $(ast_cmd "sip show peers" | tail -1)"
else
echo " Status: NOT RUNNING"
fi
echo ""
echo "ViciDial:"
local agent_count
agent_count=$(db_value "SELECT COUNT(*) FROM vicidial_live_agents;" 2>/dev/null)
if [ -n "$agent_count" ]; then
echo " Agents: $agent_count logged in"
echo " Schema: $(db_value "SELECT db_schema_version FROM system_settings LIMIT 1;" 2>/dev/null)"
else
echo " Database: connection failed"
fi
echo ""
echo "Mail spool: $(du -sh "$MAIL_SPOOL" 2>/dev/null | cut -f1 || echo 'empty')"
}
#============================================================================
# MAIN DISPATCHER
#============================================================================
case "${1:-help}" in
sip) shift; cmd_sip "$@" ;;
calls) shift; cmd_calls "$@" ;;
channels) cmd_channels ;;
agents) shift; cmd_agents "$@" ;;
log) shift; cmd_log "$@" ;;
db) shift; cmd_db "$@" ;;
mail) shift; cmd_mail "$@" ;;
smokeping) shift; cmd_smokeping "$@" ;;
status) cmd_status ;;
help|--help|-h) show_help ;;
version|-v|--version)
echo "vinit - VoIP Server Management CLI v$VERSION"
;;
*)
echo "Unknown command: $1"
echo "Run 'vinit help' for usage information."
exit 1
;;
esac
Installation
Step 1: Create the Script
# Copy the script to the system binary path
sudo cp vinit /usr/local/bin/vinit
# Make it executable
sudo chmod +x /usr/local/bin/vinit
# Verify it is in PATH
which vinit
# Expected output: /usr/local/bin/vinit
Step 2: Configure Credentials
Open the script and edit the configuration block at the top:
sudo nano /usr/local/bin/vinit
Update these variables:
DB_USER="your_db_user" # MySQL user with SELECT privileges
DB_PASS="your_db_password" # Password for that user
DB_NAME="asterisk" # Database name (usually "asterisk")
SERVER_IP="YOUR_SERVER_IP" # This server's IP address
Step 3: Create a Dedicated MySQL User
Never use the root MySQL user in the script. Create a read-only user with only the permissions needed:
-- Connect to MySQL as root
mysql -u root -p
-- Create dedicated user for the CLI tool
CREATE USER 'vinit_ro'@'localhost' IDENTIFIED BY 'GENERATE_A_STRONG_PASSWORD';
-- Grant read-only access to the ViciDial database
GRANT SELECT ON asterisk.* TO 'vinit_ro'@'localhost';
-- Apply changes
FLUSH PRIVILEGES;
Step 4: Test
# Should display help text
vinit help
# Should show SIP peers (requires Asterisk running)
vinit sip
# Should show system overview
vinit status
Step 5: Deploy to Multiple Servers
For a fleet of servers, use a simple deploy script:
#!/bin/bash
# deploy-vinit.sh - Push vinit to all servers
SERVERS="alpha bravo charlie delta"
for server in $SERVERS; do
echo "Deploying to $server..."
scp /usr/local/bin/vinit "$server":/usr/local/bin/vinit
ssh "$server" "chmod +x /usr/local/bin/vinit"
echo " Done."
done
echo "Deployed to all servers."
echo "NOTE: Edit DB_USER/DB_PASS/SERVER_IP on each server."
After deploying, SSH into each server and update the configuration variables. Each server will have its own IP and potentially different database credentials.
Command Reference
SIP Management
vinit sip -- Show all SIP peer status.
Runs sip show peers on the Asterisk CLI. Shows every registered SIP device, trunk, and softphone with its IP address, port, and status (OK, UNREACHABLE, LAGGED, UNKNOWN).
$ vinit sip
=== SIP Peers ===
Name/username Host Dyn Forcerport Comedia ACL Port Status
protech/protech 10.20.30.40 D Auto (No) No A 5060 OK (24 ms)
agent_1001/1001 192.168.1.50 D Auto (Yes) Yes A 5060 OK (32 ms)
agent_1002/1002 192.168.1.51 D Auto (Yes) Yes A 5060 OK (28 ms)
zoiper_ext/301 (Unspecified) D Auto (No) No A 0 UNKNOWN
4 sip peers [Monitored: 3 online, 1 offline Unmonitored: 0 online, 0 offline]
vinit sip <peer> -- Show detailed information for a specific SIP peer.
Runs sip show peer <name>. Shows the full configuration: codecs, NAT settings, registration status, qualify time, ACLs, and more. Essential for debugging registration or audio issues.
$ vinit sip protech
=== SIP Peer: protech ===
* Name : protech
Secret : <Set>
Context : trunkinbound
Subscr.Cont. : <Not set>
Callerid : "" <>
Codecs : (alaw|ulaw)
Status : OK (24 ms)
Useragent : Odin/1.0
Reg. Contact : sip:[email protected]:5060
...
vinit sip reload -- Reload the SIP configuration without restarting Asterisk.
Runs sip reload. This re-reads sip.conf and sip-vicidial.conf without dropping active calls. Use this after adding or modifying SIP peers.
$ vinit sip reload
Reloading SIP configuration...
Peer 'protech' is now Reachable. (24ms / 2000ms)
SIP config reloaded. Verify with: vinit sip
Call Monitoring
vinit calls -- Show active calls with full details.
Displays the total call count, then runs core show channels verbose for detailed channel information including caller ID, connected line, duration, and application.
$ vinit calls
Active calls: 12
Channel Location State Application(Data)
SIP/protech-00001a s@from-trunk Up Dial(SIP/1001,30,trM)
SIP/1001-00001b 1001@agents Up MeetMe(8600101)
...
12 active channels
vinit calls count -- Show only the call count.
A quick one-liner for dashboards, scripts, or Prometheus exporters. Returns just the number.
$ vinit calls count
Active calls: 12
vinit channels -- Show active channels with codec and duration details.
Runs core show channels. Similar to vinit calls but with a slightly different output format showing channel technology, context, and duration.
Agent Management
vinit agents -- Show all currently logged-in agents.
Queries the vicidial_live_agents table joined with vicidial_users for human-readable names. Shows status, campaign, call count, and last call time.
$ vinit agents
=== Logged-In Agents ===
+------+--------------+-------+-------------+-------------+---------------------+
| user | full_name | status| campaign_id | calls_today | last_call_time |
+------+--------------+-------+-------------+-------------+---------------------+
| 1001 | John Smith | INCALL| SALES | 14 | 2026-03-12 10:23:45 |
| 1002 | Jane Doe | READY | SALES | 11 | 2026-03-12 10:21:30 |
| 1003 | Bob Wilson | PAUSED| SUPPORT | 8 | 2026-03-12 10:15:22 |
+------+--------------+-------+-------------+-------------+---------------------+
Total: 3 agents logged in
vinit agents <user> -- Show detailed information for a specific agent.
Displays every field from vicidial_live_agents for the given user: current channel, unique ID, lead ID, pause code, closer campaigns, and more. If the agent is not logged in, falls back to showing their vicidial_users record.
$ vinit agents 1001
=== Agent Details: 1001 ===
+------+--------------+--------+-----------+-------+----------+
| user | full_name | status | server_ip | calls | campaign |
+------+--------------+--------+-----------+-------+----------+
| 1001 | John Smith | INCALL | 10.0.0.1 | 14 | SALES |
+------+--------------+--------+-----------+-------+----------+
Channel: SIP/1001-00002f
UniqueID: 1741234567.12345
Lead ID: 98765
Log Access
vinit log -- Tail the Asterisk messages log in real time.
Opens a tail -f on /var/log/asterisk/messages. Press Ctrl+C to exit. This is the primary log for SIP registrations, call routing, warnings, and errors.
vinit log full -- Tail the Asterisk full log.
The full log includes verbose output and debug information. If the full log does not exist on your server (not all distributions enable it), the command falls back to the messages log automatically.
vinit log grep <pattern> -- Search logs for a specific pattern.
Runs a case-insensitive search and displays the last 100 matching lines. Useful for finding specific SIP peers, error messages, or call IDs.
$ vinit log grep "INVITE"
[2026-03-12 10:15:33] VERBOSE: -- Got SIP response 200 "OK" back from 10.20.30.40:5060
[2026-03-12 10:15:33] VERBOSE: -- SIP/protech-00001a is making progress passing it to SIP/1001-00001b
...
(Showing last 100 matches from /var/log/asterisk/messages)
Database
vinit db -- Open an interactive MySQL console.
Connects directly to the ViciDial database with the configured credentials. You land in a full MySQL shell where you can run any query. Exit with \q or Ctrl+D.
$ vinit db
Connecting to MySQL (database: asterisk)...
MariaDB [asterisk]>
vinit db query "<sql>" -- Run a single SQL query.
For quick one-off queries without entering the interactive console. The output is formatted as a standard MySQL table.
$ vinit db query "SELECT COUNT(*) AS total_calls FROM vicidial_log WHERE call_date > NOW() - INTERVAL 1 HOUR;"
+-------------+
| total_calls |
+-------------+
| 247 |
+-------------+
Note the SQL statement must be enclosed in quotes. For queries containing quotes themselves, use single quotes inside double quotes or escape them.
Mail Management
vinit mail -- Check mail spool size and queue status.
Asterisk and ViciDial cron jobs generate large volumes of email (error reports, cron output, system notifications). On servers without a properly configured mail relay, these accumulate in /var/mail/root and can consume gigabytes of disk space over months.
$ vinit mail
=== Mail Status ===
Mail spool:
Size: 3.2G
Lines: 4521893
Mail queue:
-- 0 Kbytes in 0 Requests.
vinit mail clean -- Truncate the mail spool.
Empties /var/mail/root by truncating it to zero bytes. This is a safe operation -- these are system notification emails, not customer data.
$ vinit mail clean
Current spool size: 3.2G
Truncating /var/mail/root... done.
Freed: 3.2G
SmokePing
vinit smokeping status -- Show SmokePing service status and target count.
$ vinit smokeping status
=== SmokePing Status ===
smokeping.service - SmokePing Network Monitoring
Loaded: loaded (/etc/systemd/system/smokeping.service; enabled)
Active: active (running) since Mon 2026-03-10 08:00:00 CET
Monitoring targets: 15
Web UI: http://YOUR_SERVER_IP/smokeping/
vinit smokeping start and vinit smokeping stop -- Start or stop the SmokePing service via systemd.
System Overview
vinit status -- Quick system health check.
A single command that shows everything you need to know at a glance: hostname, uptime, load, memory, disk, Asterisk status, ViciDial agent count, and mail spool size.
$ vinit status
=== System Overview ===
Hostname: voip-server-01
Uptime: up 47 days, 3 hours, 12 minutes
Load: 0.42 0.38 0.35
Memory:
Mem: 62Gi 12Gi 45Gi 1.2Gi 4.8Gi 48Gi
Swap: 4.0Gi 0.0Gi 4.0Gi
Disk:
Root: 45G used of 234G (20%)
Asterisk:
Status: RUNNING
Version: Asterisk 20.18.2
Calls: 12 active
Peers: 45 sip peers [Monitored: 38 online, 7 offline]
ViciDial:
Agents: 14 logged in
Schema: 1706
Mail spool: 128K
This is the command you run first thing in the morning or when SSH-ing into a server to check on it.
Bash Completion
Tab completion transforms the tool from useful to effortless. With completion enabled, typing vinit s<TAB> expands to show sip, smokeping, and status. Typing vinit sip <TAB> shows reload as a valid option.
The Completion Script
Save this as /etc/bash_completion.d/vinit:
#!/bin/bash
#============================================================================
# Bash completion for vinit - VoIP Server Management CLI
#
# Installation:
# sudo cp vinit-completion.bash /etc/bash_completion.d/vinit
# source /etc/bash_completion.d/vinit (or open a new shell)
#============================================================================
_vinit_completions() {
local cur prev commands
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Top-level commands
commands="sip calls channels agents log db mail smokeping status help version"
case "$prev" in
vinit)
COMPREPLY=( $(compgen -W "$commands" -- "$cur") )
return 0
;;
sip)
# "reload" is a subcommand; any other argument is a peer name
COMPREPLY=( $(compgen -W "reload" -- "$cur") )
return 0
;;
calls)
COMPREPLY=( $(compgen -W "count" -- "$cur") )
return 0
;;
log)
COMPREPLY=( $(compgen -W "full grep" -- "$cur") )
return 0
;;
db)
COMPREPLY=( $(compgen -W "query" -- "$cur") )
return 0
;;
mail)
COMPREPLY=( $(compgen -W "clean" -- "$cur") )
return 0
;;
smokeping)
COMPREPLY=( $(compgen -W "status start stop" -- "$cur") )
return 0
;;
esac
return 0
}
complete -F _vinit_completions vinit
Installation
# Copy the completion script
sudo cp vinit-completion.bash /etc/bash_completion.d/vinit
# Load it in the current shell
source /etc/bash_completion.d/vinit
# Verify: type "vinit " and press TAB
vinit <TAB>
# Output: agents calls channels db help log mail sip smokeping status version
The completion file is loaded automatically for all new shell sessions. No .bashrc modification needed -- /etc/bash_completion.d/ is sourced by the system bash completion framework.
Dynamic Peer Completion (Advanced)
For an advanced setup, you can make vinit sip <TAB> complete with actual SIP peer names from Asterisk:
sip)
# Offer "reload" plus actual SIP peer names from Asterisk
local peers
peers=$(asterisk -rx "sip show peers" 2>/dev/null \
| awk 'NR>1 && !/^[0-9]+ sip peers/' \
| awk -F'/' '{print $1}')
COMPREPLY=( $(compgen -W "reload $peers" -- "$cur") )
return 0
;;
This adds a slight delay on TAB (50-100ms to query Asterisk), but provides real peer names. Whether the tradeoff is worth it depends on how many peers you have and how often you use this subcommand.
Usage Examples
Morning Server Check
# Quick health check on every server
for server in alpha bravo charlie delta; do
echo "--- $server ---"
ssh $server "vinit status"
echo ""
done
Troubleshoot a SIP Trunk
# Is the trunk registered?
vinit sip | grep my_trunk
# Get full details
vinit sip my_trunk
# Check for related log entries
vinit log grep "my_trunk"
# Are calls flowing through it?
vinit calls | grep my_trunk
Find Why an Agent Cannot Log In
# Is the agent currently logged in somewhere else?
vinit agents 1001
# Check the SIP registration for their phone
vinit sip 1001
# Look for errors in the log
vinit log grep "1001"
Quick Call Count for Monitoring
# One-liner for a Prometheus textfile collector
echo "vicidial_active_calls $(vinit calls count | awk '{print $NF}')" \
> /var/lib/prometheus/node-exporter/calls.prom
Free Disk Space in an Emergency
# Check what is eating disk
vinit status
# If mail spool is large
vinit mail
vinit mail clean
# Verify
vinit status
Run a Report Query Without Leaving the Shell
# Calls per campaign in the last hour
vinit db query "
SELECT campaign_id, COUNT(*) as calls, AVG(length_in_sec) as avg_duration
FROM vicidial_log
WHERE call_date > NOW() - INTERVAL 1 HOUR
GROUP BY campaign_id
ORDER BY calls DESC;
"
Adding New Commands
The script's architecture makes adding new commands straightforward. Every command follows the same pattern:
Pattern
- Write a function named
cmd_yourcommand. - Add a case in the main dispatcher.
- Update the help text.
- Add tab completion.
Example: Adding a vinit trunks Command
This command shows only SIP trunks (filtering out agent phones):
Step 1: Write the function (add above the dispatcher section):
# --- TRUNK STATUS ---
cmd_trunks() {
header "SIP Trunks"
# Filter peers: trunks typically have context "trunkinbound" or similar
ast_cmd "sip show peers" | grep -E "(Name|trunk)" | head -30
echo ""
echo "Registrations:"
ast_cmd "sip show registry"
}
Step 2: Add to the dispatcher:
case "${1:-help}" in
# ... existing commands ...
trunks) cmd_trunks ;;
# ... rest of dispatcher ...
esac
Step 3: Update the help text:
SIP MANAGEMENT:
sip Show all SIP peer status
sip <peer> Show detailed info for a specific SIP peer
sip reload Reload SIP configuration (no restart)
trunks Show SIP trunk status and registrations # <-- new
Step 4: Update bash completion:
commands="sip calls channels agents log db mail smokeping status trunks help version"
# ^^^^^^ added
Example: Adding a vinit recording <uniqueid> Command
Play or locate a call recording by its unique ID:
cmd_recording() {
local uid="${1:-}"
if [ -z "$uid" ]; then
echo "Usage: vinit recording <uniqueid>"
exit 1
fi
header "Recording: $uid"
local rec_path
rec_path=$(db_value "
SELECT location
FROM recording_log
WHERE vicidial_id = '$uid'
ORDER BY start_time DESC
LIMIT 1;
")
if [ -n "$rec_path" ]; then
echo "File: $rec_path"
if [ -f "$rec_path" ]; then
echo "Size: $(du -sh "$rec_path" | cut -f1)"
echo "Duration: $(soxi -D "$rec_path" 2>/dev/null || echo 'unknown') seconds"
else
echo "WARNING: File not found on disk (may be archived)"
fi
else
echo "No recording found for uniqueid: $uid"
fi
}
Usage Logging & Audit Trail
On a shared server, knowing who ran what and when is valuable. Add logging to the script so every invocation is recorded.
Adding a Log Line
Insert this near the top of the script, after the configuration block:
#============================================================================
# USAGE LOGGING
# Every invocation is logged for audit purposes.
#============================================================================
LOG_FILE="/var/log/vinit.log"
log_usage() {
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local user
user=$(whoami)
echo "$timestamp | $user | vinit $*" >> "$LOG_FILE" 2>/dev/null
}
# Log this invocation
log_usage "$@"
Log Output
2026-03-12 10:15:33 | root | vinit sip
2026-03-12 10:15:45 | root | vinit agents
2026-03-12 10:16:02 | admin | vinit db query "SELECT COUNT(*) FROM vicidial_log;"
2026-03-12 10:20:11 | root | vinit mail clean
Logrotate Configuration
Create /etc/logrotate.d/vinit:
/var/log/vinit.log {
weekly
rotate 12
compress
delaycompress
missingok
notifempty
create 644 root root
}
This keeps 12 weeks of history (3 months) with compression, which is more than enough for audit purposes.
Team Standardization
When multiple admins manage the same servers, the tool becomes a shared language. Here is how to maximize that benefit.
Version Control the Script
Store vinit in a Git repository. Every change is reviewed, tested, and deployed consistently.
mkdir -p /opt/vinit && cd /opt/vinit
git init
cp /usr/local/bin/vinit .
cp /etc/bash_completion.d/vinit vinit-completion.bash
git add -A && git commit -m "Initial vinit script"
Per-Server Configuration
Instead of editing the script on each server, source a local configuration file:
# Add near the top of the vinit script, after the defaults:
CONFIG_FILE="/etc/vinit.conf"
if [ -f "$CONFIG_FILE" ]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
Then on each server, create /etc/vinit.conf:
# /etc/vinit.conf - Server-specific configuration
DB_USER="vinit_ro"
DB_PASS="this_servers_password"
SERVER_IP="10.0.0.1"
AST_LOG="/var/log/asterisk/messages"
Now the script itself is identical across all servers and can be deployed with a simple scp without worrying about overwriting credentials.
Standard Operating Procedures
Document which vinit commands correspond to each operational procedure:
| Situation | Commands to Run |
|---|---|
| Server health check | vinit status |
| SIP trunk down | vinit sip, vinit sip <trunk>, vinit log grep <trunk> |
| Agent cannot log in | vinit agents <user>, vinit sip <phone>, vinit log grep <user> |
| High disk usage | vinit status, vinit mail, vinit mail clean |
| After config change | vinit sip reload, vinit sip |
| Call quality issue | vinit calls, vinit sip <trunk>, vinit log grep WARNING |
Alias Across Servers
If you manage multiple servers via SSH, add aliases in your local ~/.bash_aliases:
# Run vinit on remote servers from your workstation
alias vinit-alpha='ssh alpha vinit'
alias vinit-bravo='ssh bravo vinit'
alias vinit-charlie='ssh charlie vinit'
# Usage: vinit-alpha calls
# Usage: vinit-bravo agents
Adapting for Other PBX Systems
The vinit pattern is not ViciDial-specific. The dispatcher-and-functions architecture works with any PBX that has a CLI and a database.
FreePBX / Asterisk (without ViciDial)
FreePBX uses Asterisk under the hood, so all Asterisk CLI commands work identically. The differences are in the database and configuration management:
# FreePBX uses its own database and fwconsole CLI
DB_NAME="asteriskcdrdb"
cmd_agents() {
# FreePBX does not have vicidial_live_agents.
# Show registered extensions instead.
header "Registered Extensions"
ast_cmd "sip show peers" | grep -v "^$" | grep -v "^Name"
}
cmd_reload() {
# FreePBX has its own reload mechanism
echo "Applying FreePBX configuration..."
fwconsole reload
}
cmd_modules() {
# List installed FreePBX modules
fwconsole ma list
}
FusionPBX / FreeSWITCH
FusionPBX uses FreeSWITCH instead of Asterisk. The CLI commands are completely different, but the wrapper pattern is the same:
# FreeSWITCH uses fs_cli instead of asterisk -rx
ast_cmd() {
fs_cli -x "$1"
}
cmd_sip() {
case "${1:-}" in
"")
header "SIP Registrations"
fs_cli -x "sofia status"
;;
*)
header "SIP Profile: $1"
fs_cli -x "sofia status profile $1"
;;
esac
}
cmd_calls() {
case "${1:-}" in
count)
fs_cli -x "show calls count"
;;
"")
header "Active Calls"
fs_cli -x "show calls"
;;
esac
}
cmd_channels() {
header "Active Channels"
fs_cli -x "show channels"
}
FusionPBX also uses PostgreSQL instead of MySQL:
# PostgreSQL query helper
db_query() {
sudo -u postgres psql -d fusionpbx -c "$1"
}
db_value() {
sudo -u postgres psql -d fusionpbx -t -A -c "$1"
}
Kamailio / OpenSIPS (SIP Proxy)
For SIP proxies without a media server, the focus shifts to registrations, dialogs, and routing:
cmd_sip() {
header "Registered Users"
kamctl ul show
}
cmd_calls() {
header "Active Dialogs"
kamctl dialog list
}
cmd_stats() {
header "Kamailio Statistics"
kamctl stats
}
The Common Pattern
Regardless of the PBX system, the structure is always the same:
- A configuration block at the top with credentials and paths.
- Helper functions that abstract CLI and database access.
- Command functions that implement each subcommand.
- A case dispatcher that routes the first argument to the right function.
- A help function that documents everything.
- A bash completion script for tab completion.
Change the helpers and command implementations; keep the architecture.
Security Considerations
Credential Storage
The script stores database credentials in plaintext. This is acceptable because:
- The script runs as root on the server where the database is local.
- The file permissions should restrict access to root only.
- The MySQL user has SELECT-only privileges.
Harden the file permissions:
# Only root can read or execute the script
sudo chmod 700 /usr/local/bin/vinit
sudo chown root:root /usr/local/bin/vinit
# If using a separate config file
sudo chmod 600 /etc/vinit.conf
sudo chown root:root /etc/vinit.conf
If you need non-root users to run the tool, use a MySQL option file instead of embedding credentials:
# Create /root/.vinit.cnf
[client]
user=vinit_ro
password=STRONG_PASSWORD_HERE
# Update the helper functions to use the option file
db_query() {
mysql --defaults-file=/root/.vinit.cnf "$DB_NAME" -e "$1" 2>/dev/null
}
SQL Injection
The vinit agents <user> and vinit db query commands pass user input into SQL queries. In this context (root access on a local server), SQL injection is not a meaningful attack vector -- the user already has full database access. However, if you extend the tool for use by non-root operators:
- Use parameterized queries (not possible in raw
mysql -e) - Validate input with pattern matching before query construction
- Restrict the
db querysubcommand to specific users
# Input validation example
cmd_agents() {
local user="$1"
# Allow only alphanumeric usernames
if [[ ! "$user" =~ ^[a-zA-Z0-9_]+$ ]]; then
echo "Invalid username format."
exit 1
fi
# ... query ...
}
Principle of Least Privilege
The MySQL user should have only SELECT privileges. Never use the ViciDial cron user or the MySQL root user in the script. If you add write operations in the future (e.g., a command to pause an agent), create a second MySQL user with narrow INSERT/UPDATE grants on specific tables only.
Production Checklist
Before deploying vinit to a production server, verify each item:
Script
- Credentials are configured (DB_USER, DB_PASS, SERVER_IP)
- MySQL user exists with SELECT-only privileges
- Script is executable:
chmod +x /usr/local/bin/vinit - Script is owned by root:
chown root:root /usr/local/bin/vinit - File permissions are restrictive:
chmod 700 /usr/local/bin/vinit -
vinit helpdisplays correctly -
vinit statusruns without errors
Functionality
-
vinit sipshows SIP peers -
vinit callsshows active calls -
vinit agentsqueries the database successfully -
vinit logtails the correct log file -
vinit dbopens a MySQL console -
vinit mailreports spool size
Completion
- Completion script installed in
/etc/bash_completion.d/vinit -
vinit <TAB>lists all commands -
vinit sip <TAB>showsreload
Operations
- Usage logging is enabled (optional)
- Logrotate is configured for
/var/log/vinit.log(if logging) - Configuration file
/etc/vinit.confis used (if multi-server) - Team members know the tool exists and have run
vinit help
Multi-Server
- Script deployed to all servers
- Credentials configured per-server
- Bash completion installed on all servers
- Deploy script (
deploy-vinit.sh) is saved for future updates
Summary
vinit is not a framework, a platform, or a product. It is a 200-line Bash script that saves you from typing the same long commands hundreds of times a week. It wraps Asterisk CLI, MySQL queries, log access, and system utilities behind short, memorable subcommands with tab completion.
The value is in the pattern, not the specific commands. Your VoIP environment has its own quirks, its own tables, its own log paths. Fork the script, add your commands, remove what you do not need. The architecture -- configuration block, helper functions, command functions, case dispatcher, help text, bash completion -- stays the same regardless of whether you run ViciDial, FreePBX, FusionPBX, or bare Asterisk.
Start with the commands you type most often. Deploy to one server. Use it for a week. Then deploy to the rest.