VOOZH about

URL: https://dev.to/moisi_trungu_31647b7ac300/building-a-custom-cli-management-tool-for-voip-servers-2ae2

⇱ Building a Custom CLI Management Tool for VoIP Servers - DEV Community


Building a Custom CLI Management Tool for VoIP Servers

Shell Script Wrapper for Asterisk + ViciDial + MySQL


Table of Contents

  1. Introduction
  2. Architecture Overview
  3. Prerequisites
  4. The Complete Script
  5. Installation
  6. Command Reference
  7. Bash Completion
  8. Usage Examples
  9. Adding New Commands
  10. Usage Logging & Audit Trail
  11. Team Standardization
  12. Adapting for Other PBX Systems
  13. Security Considerations
  14. 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 calls is faster than asterisk -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 help and 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 agents show 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 asterisk group

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:protech@10.20.30.40: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

  1. Write a function named cmd_yourcommand.
  2. Add a case in the main dispatcher.
  3. Update the help text.
  4. 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:

  1. A configuration block at the top with credentials and paths.
  2. Helper functions that abstract CLI and database access.
  3. Command functions that implement each subcommand.
  4. A case dispatcher that routes the first argument to the right function.
  5. A help function that documents everything.
  6. 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:

  1. The script runs as root on the server where the database is local.
  2. The file permissions should restrict access to root only.
  3. 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 query subcommand 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 help displays correctly
  • [ ] vinit status runs without errors

Functionality

  • [ ] vinit sip shows SIP peers
  • [ ] vinit calls shows active calls
  • [ ] vinit agents queries the database successfully
  • [ ] vinit log tails the correct log file
  • [ ] vinit db opens a MySQL console
  • [ ] vinit mail reports spool size

Completion

  • [ ] Completion script installed in /etc/bash_completion.d/vinit
  • [ ] vinit <TAB> lists all commands
  • [ ] vinit sip <TAB> shows reload

Operations

  • [ ] Usage logging is enabled (optional)
  • [ ] Logrotate is configured for /var/log/vinit.log (if logging)
  • [ ] Configuration file /etc/vinit.conf is 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.