← All Tutorials

Building a Custom CLI Management Tool for VoIP Servers

Infrastructure & DevOps Intermediate 31 min read #20

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:

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

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

  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:

# 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

Functionality

Completion

Operations

Multi-Server


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.

Need expert help with your setup?

VoIP infrastructure consulting, AI voice agent integration, monitoring stacks, scaling — I've done it all in production.

Get a Free Consultation