Add Ansible template

This commit is contained in:
Mike Jones 2025-05-09 23:45:18 +01:00
parent 1115e86296
commit fa1a1138d6
Signed by: mike
GPG key ID: 1C2C25F83140EF49
22 changed files with 699 additions and 3 deletions

4
.gitignore vendored
View file

@ -1,3 +1,7 @@
# ---> Ansible
*.retry
# Cisco's licensing disallows sharing of SIP firmware, so unfortunately I cannot
# provide it.
roles/tftp/files/firmware/
!roles/tftp/files/firmware/README.md

View file

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 mike
Copyright (c) 2025 Infrastructure
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View file

@ -1,3 +1,86 @@
# ansible-asterisk-cisco-intercom
# intercom
This is a template repository to assist with configuring a home intercom system using Cisco 7491 phones in SIP mode.
This repository contains a **template** for creating a home intercom system
using Cisco 7941G phones and Asterisk.
* [Asterisk](https://www.asterisk.org/) is used as the SIP server.
* `tftpd-hpa` is used for serving configuration and firmware for the phones.
* [NGINX](https://nginx.org/) is used for serving the phone directory.
## Prerequisites
* **A basic knowledge of computer networking.**
* At least two Cisco 7941G phones. These are fairly cheap on eBay and often come
up in office clearance sales.
* A way of powering the phones (PoE is preferred - the mains adapters are often
more expensive than the phones).
* A machine to run Asterisk, TFTP and NGINX. I use a spare Raspberry Pi 4B,
which is overpowered for the task. The machine should be running Debian 12.
Ideally it should be a fresh image, and it definitely should not already run
NGINX, TFTP or Asterisk.
## Guide
A few steps are required to deploy the system.
### Find the Cisco SIP firmware
See [`roles/tftp/files/firmware/README.md`](roles/tftp/files/firmware/README.md).
### Configure static IP addresses
Both the phones and your server(s) for hosting Asterisk/TFTP/NGINX should have
static IP addresses configured on your router, and you should know them.
This step depends on your router (but I configure it in the interface's DHCP in
OPNsense).
### Set up `hosts.ini`
Add your hosts to the inventory in [`inventories/production/hosts.ini`](inventories/production/hosts.ini).
### Configure variables
You will need to edit the following files, adjusting them for your own configuration:
* [`inventories/production/group_vars/all.yml`](inventories/production/group_vars/all.yml)
### Deploy the configuration to your server(s)
```bash
ansible-playbook -i inventories/production/hosts.ini site.yml
```
## Debugging
### Phone logs
The phones this playbook configures have HTTP web interfaces, which contain
log files ("Console Logs" in the left hand menu).
The "log_n_" files sometimes include useful information, such as:
```
NOT 04:07:19.594520 JVM: Startup Module Loader|cip.cfg.h:? - Config handleTftpResponse, status=0 for file=ram/SEP001234.cnf.xml
ERR 04:07:19.596632 JVM: Startup Module Loader|cip.xml.at: - XML Parser Exception: Too few 'provisioning' elements in '/device/phoneServices': occurs=0 minOccurs=1 (line=50)
ERR 04:07:19.598909 JVM: Startup Module Loader|cip.cfg.h:? - ERROR PARSING CONFIG file:ram/SEP001234.cnf.xml
NOT 04:07:19.601557 JVM: Startup Module Loader|cip.cfg.h:? - Config processConfigNoError() result code=CONFIG_FILE_BAD_FORMAT
```
### Observing traffic on the Asterisk server
It can be useful to watch network traffic on the Asterisk server using `tcpdump`:
```bash
tcpdump -i any -n port 5060 and udp
```
You might look for information such as "401 Unauthorized" errors, for example:
```
22:21:34.732807 wlan0 In IP 192.168.1.50.49157 > 192.168.1.49.5060: SIP: INVITE sip:1@192.168.1.49;user=phone SIP/2.0
22:21:34.735313 wlan0 Out IP 192.168.1.49.5060 > 192.168.1.50.5060: SIP: SIP/2.0 401 Unauthorized
```
The error above was due to an old dialplan causing the phone in question to
immediately ring "1" when the "1" button was pressed.

View file

@ -0,0 +1,23 @@
# You should configure this for your own area, or use your own NTP server
ntp_server: uk.pool.ntp.org
# Each item in this list will become a PJSIP phone in Asterisk with a SEPMAC
# configuration file hosted in TFTP and an entry in the phone directory served
# by NGINX.
extensions:
- number: 1001
callername: Phone1
context: phones
voicemail: 1001
password: 1234
mac: SEP00000000
ipv4_address: 192.168.1.50
- number: 1002
callername: Phone2
context: phones
voicemail: 1002
password: 1234
mac: SEP00000001
ipv4_address: 192.168.1.51

View file

@ -0,0 +1,3 @@
asterisk_version: "22.3.0"
asterisk_src_dir: "/usr/local/src/asterisk-{{ asterisk_version }}"
asterisk_config_dir: "/etc/asterisk"

View file

@ -0,0 +1,9 @@
tftp_root_dir: /srv/tftp
firmware_files:
- term41.default.loads
- jar41sip.8-5-2TH1-9.sbn
- cnu41.8-5-2TH1-9.sbn
- apps41.8-5-2TH1-9.sbn
- dsp41.8-5-2TH1-9.sbn
- cvm41sip.8-5-2TH1-9.sbn

View file

@ -0,0 +1,11 @@
[asterisk]
# Replace with your actual Asterisk server IP or hostname
your-asterisk-hostname ansible_host=your-asterisk-ip-or-hostname
[nginx]
# Replace with your actual Nginx server IP or hostname
your-nginx-hostname ansible_host=your-nginx-ip-or-hostname
[tftp]
# Replace with your actual TFTP server IP or hostname
your-tftp-hostname ansible_host=your-tftp-ip-or-hostname

View file

@ -0,0 +1,4 @@
- name: Reload Asterisk
ansible.builtin.service:
name: asterisk
state: reloaded

View file

@ -0,0 +1,140 @@
- name: Check if Asterisk is already installed
ansible.builtin.stat:
path: /usr/sbin/asterisk
register: asterisk_installed
- name: Set external signaling address
ansible.builtin.set_fact:
external_signaling_address: "{{ hostvars[groups['asterisk'][0]].ansible_host }}"
when: external_signaling_address is not defined or external_signaling_address == ""
- name: Install build dependencies for Asterisk
ansible.builtin.apt:
name:
- build-essential
- libasound2-dev
- libedit-dev
- libjansson-dev
- libncurses5-dev
- libsqlite3-dev
- libssl-dev
- libxml2-dev
- uuid-dev
- wget
state: present
update_cache: true
- name: Create a directory for PJSIP source
ansible.builtin.file:
path: /usr/local/src/pjsip
state: directory
mode: '0755'
when: not asterisk_installed.stat.exists
- name: Download PJSIP source
ansible.builtin.get_url:
url: https://github.com/pjsip/pjproject/archive/refs/tags/2.14.tar.gz
dest: /usr/local/src/pjsip/pjproject-2.14.tar.gz
mode: '0644'
when: not asterisk_installed.stat.exists
- name: Extract PJSIP source
ansible.builtin.unarchive:
src: /usr/local/src/pjsip/pjproject-2.14.tar.gz
dest: /usr/local/src/pjsip/
remote_src: yes
when: not asterisk_installed.stat.exists
- name: Build and install PJSIP
ansible.builtin.shell:
cmd: |
./configure CFLAGS="-DNDEBUG -DPJ_HAS_IPV6=1" --prefix=/usr --libdir=/usr/lib
make -j$(nproc)
make install
chdir: /usr/local/src/pjsip/pjproject-2.14
args:
creates: /usr/lib/libpj.so
when: not asterisk_installed.stat.exists
- name: Download Asterisk source
ansible.builtin.get_url:
url: "https://downloads.asterisk.org/pub/telephony/asterisk/asterisk-{{ asterisk_version }}.tar.gz"
dest: "/usr/local/src/asterisk-{{ asterisk_version }}.tar.gz"
mode: '0644'
when: not asterisk_installed.stat.exists
- name: Extract Asterisk source
ansible.builtin.unarchive:
src: "/usr/local/src/asterisk-{{ asterisk_version }}.tar.gz"
dest: /usr/local/src/
remote_src: true
creates: "/usr/local/src/asterisk-{{ asterisk_version }}"
when: not asterisk_installed.stat.exists
- name: Configure Asterisk
ansible.builtin.command:
cmd: ./configure
chdir: "{{ asterisk_src_dir }}"
when: not asterisk_installed.stat.exists
- name: Enable PJSIP modules
ansible.builtin.command:
cmd: make menuselect.makeopts
chdir: "{{ asterisk_src_dir }}"
when: not asterisk_installed.stat.exists
- name: Compile Asterisk
ansible.builtin.command:
cmd: make
chdir: "{{ asterisk_src_dir }}"
when: not asterisk_installed.stat.exists
- name: Install Asterisk
ansible.builtin.command:
cmd: make install
chdir: "{{ asterisk_src_dir }}"
when: not asterisk_installed.stat.exists
- name: Verify that PJSIP modules are installed
ansible.builtin.stat:
path: /usr/lib/asterisk/modules/chan_pjsip.so
register: pjsip_module
- name: Fail if PJSIP modules are missing
ansible.builtin.fail:
msg: "PJSIP module not found! Check if libpjsip-dev was installed before building."
when: not pjsip_module.stat.exists
- name: Install Asterisk samples
ansible.builtin.command:
cmd: make samples
chdir: "{{ asterisk_src_dir }}"
- name: Install Asterisk startup scripts
ansible.builtin.command:
cmd: make config
chdir: "{{ asterisk_src_dir }}"
- name: Enable and start the Asterisk service
ansible.builtin.service:
name: asterisk
state: started
enabled: true
- name: Deploy PJSIP configuration
ansible.builtin.template:
src: pjsip.conf.j2
dest: "{{ asterisk_config_dir }}/pjsip.conf"
owner: root
group: root
mode: '0644'
notify: Reload Asterisk
- name: Deploy extensions configuration
ansible.builtin.template:
src: extensions.conf.j2
dest: "{{ asterisk_config_dir }}/extensions.conf"
owner: root
group: root
mode: '0644'
notify: Reload Asterisk

View file

@ -0,0 +1,7 @@
[phones]
{% for extension in extensions %}
exten => {{ extension.number }},1,Dial(PJSIP/{{ extension.number }})
exten => {{ extension.number }},n,Voicemail({{ extension.voicemail }})
exten => {{ extension.number }},n,Hangup()
exten => {{ extension.number }},hint,PJSIP/{{ extension.number }}
{% endfor %}

View file

@ -0,0 +1,42 @@
[global]
realm=asterisk
external_signaling_port=5060
[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060
{% for extension in extensions %}
[{{ extension.number }}]
type=endpoint
transport=transport-udp
context=phones
disallow=all
allow=ulaw,alaw
aors={{ extension.number }}
auth={{ extension.number }}-auth
direct_media=no
rtp_symmetric=yes
force_rport=no
rewrite_contact=no
[{{ extension.number }}]
type=aor
max_contacts=1
remove_existing=yes
contact=sip:{{ extension.number }}@{{ extension.ipv4_address }}
qualify_frequency=10
[{{ extension.number }}-auth]
type=auth
auth_type=userpass
username={{ extension.number }}
password={{ extension.password }}
[{{ extension.number }}-identify]
type=identify
endpoint={{ extension.number }}
match={{ extension.ipv4_address }}
{% endfor %}

View file

@ -0,0 +1,4 @@
- name: Run APT updates
ansible.builtin.apt:
update_cache: yes
upgrade: dist

View file

@ -0,0 +1,4 @@
- name: Reload NGINX
ansible.builtin.service:
name: nginx
state: reloaded

View file

@ -0,0 +1,31 @@
- name: Install NGINX
ansible.builtin.package:
name: nginx
state: present
- name: Enable and start the NGINX service
ansible.builtin.service:
name: nginx
state: started
enabled: true
- name: Deploy NGINX site configuration
ansible.builtin.template:
src: default
dest: /etc/nginx/sites-available/default
mode: '0644'
notify: Reload NGINX
- name: Ensure phone XML directory exists
ansible.builtin.file:
path: /var/www/html/phones
state: directory
mode: '0755'
owner: root
group: root
- name: Upload directory.xml
ansible.builtin.template:
src: directory.xml.j2
dest: /var/www/html/phones/directory.xml
mode: '0644'

View file

@ -0,0 +1,96 @@
##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##
# Default server configuration
#
server {
listen 80 default_server;
listen [::]:80 default_server;
# SSL configuration
#
# listen 443 ssl default_server;
# listen [::]:443 ssl default_server;
#
# Note: You should disable gzip for SSL traffic.
# See: https://bugs.debian.org/773332
#
# Read up on ssl_ciphers to ensure a secure configuration.
# See: https://bugs.debian.org/765782
#
# Self signed certs generated by the ssl-cert package
# Don't use them in a production server!
#
# include snippets/snakeoil.conf;
root /var/www/html;
# Add index.php to the list if you are using PHP
index index.html index.htm index.nginx-debian.html;
server_name _;
location / {
# First attempt to serve request as file, then
# as directory, then fall back to displaying a 404.
try_files $uri $uri/ =404;
}
location /phone/ {
default_type text/xml;
autoindex off;
}
# pass PHP scripts to FastCGI server
#
#location ~ \.php$ {
# include snippets/fastcgi-php.conf;
#
# # With php-fpm (or other unix sockets):
# fastcgi_pass unix:/run/php/php7.4-fpm.sock;
# # With php-cgi (or other tcp sockets):
# fastcgi_pass 127.0.0.1:9000;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}
# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
# listen 80;
# listen [::]:80;
#
# server_name example.com;
#
# root /var/www/example.com;
# index index.html;
#
# location / {
# try_files $uri $uri/ =404;
# }
#}

View file

@ -0,0 +1,10 @@
<CiscoIPPhoneDirectory>
<Title>Company Directory</Title>
<Prompt>Select a contact</Prompt>
{% for extension in extensions %}
<DirectoryEntry>
<Name>{{ extension.callername }}</Name>
<Telephone>{{ extension.number }}</Telephone>
</DirectoryEntry>
{% endfor %}
</CiscoIPPhoneDirectory>

View file

@ -0,0 +1,3 @@
<DIALTEMPLATE>
<TEMPLATE MATCH="*" Timeout="2"/>
</DIALTEMPLATE>

View file

@ -0,0 +1,4 @@
- name: Restart TFTP
ansible.builtin.service:
name: tftpd-hpa
state: restarted

46
roles/tftp/tasks/main.yml Normal file
View file

@ -0,0 +1,46 @@
- name: Fail if firmware files are missing
ansible.builtin.stat:
path: "roles/tftp/files/firmware/{{ item }}"
register: firmware_stat
loop: "{{ firmware_files }}"
failed_when: not firmware_stat.stat.exists
- name: Install TFTP server
ansible.builtin.package:
name: tftpd-hpa
state: present
- name: Enable and start TFTP service
ansible.builtin.service:
name: tftpd-hpa
state: started
enabled: true
- name: Configure TFTP server
ansible.builtin.template:
src: tftpd-hpa.j2
dest: /etc/default/tftpd-hpa
owner: root
group: root
mode: '0644'
notify: Restart TFTP
- name: Upload SEPMAC configuration files
ansible.builtin.template:
src: SEPMAC.cnf.xml.j2
dest: "{{ tftp_root_dir }}/{{ item.mac }}.cnf.xml"
mode: '0644'
loop: "{{ extensions }}"
- name: Upload firmware files
ansible.builtin.copy:
src: "firmware/{{ item }}"
dest: "{{ tftp_root_dir }}/{{ item }}"
mode: '0644'
loop: "{{ firmware_files }}"
- name: Upload dialplan file
ansible.builtin.copy:
src: "dialplan.xml"
dest: "{{ tftp_root_dir }}/dialplan.xml"
mode: '0644'

View file

@ -0,0 +1,159 @@
<device>
<deviceProtocol>SIP</deviceProtocol>
<sshUserId>cisco</sshUserId>
<sshPassword>cisco</sshPassword>
<ipAddressMode>0</ipAddressMode>
<directoryURL>http://{{ hostvars[groups['nginx'][0]].ansible_host }}/phones/directory.xml</directoryURL>
<servicesURL>http://{{ hostvars[groups['nginx'][0]].ansible_host }}/phones/directory.xml</servicesURL>
<devicePool>
<dateTimeSetting>
<dateTemplate>D/M/Ya</dateTemplate>
<timeZone>GMT Standard/Daylight Time</timeZone>
<ntps>
<ntp>
<name>{{ ntp_server }}</name>
<ntpMode>Unicast</ntpMode>
</ntp>
</ntps>
</dateTimeSetting>
<callManagerGroup>
<members>
<member priority="0">
<callManager>
<ports>
<ethernetPhonePort>2000</ethernetPhonePort>
<sipPort>5060</sipPort>
</ports>
<processNodeName>{{ hostvars[groups['asterisk'][0]].ansible_host }}</processNodeName>
</callManager>
</member>
</members>
</callManagerGroup>
</devicePool>
<sipProfile>
<sipProxies>
<registerWithProxy>true</registerWithProxy>
</sipProxies>
<sipCallFeatures>
<cnfJoinEnabled>true</cnfJoinEnabled>
<rfc2543Hold>false</rfc2543Hold>
<callHoldRingback>2</callHoldRingback>
<localCfwdEnable>true</localCfwdEnable>
<semiAttendedTransfer>true</semiAttendedTransfer>
<anonymousCallBlock>2</anonymousCallBlock>
<callerIdBlocking>2</callerIdBlocking>
<dndControl>0</dndControl>
<remoteCcEnable>true</remoteCcEnable>
</sipCallFeatures>
<sipStack>
<sipInviteRetx>6</sipInviteRetx>
<sipRetx>10</sipRetx>
<timerInviteExpires>180</timerInviteExpires>
<timerRegisterExpires>300</timerRegisterExpires>
<timerRegisterDelta>5</timerRegisterDelta>
<timerKeepAliveExpires>120</timerKeepAliveExpires>
<timerSubscribeExpires>120</timerSubscribeExpires>
<timerSubscribeDelta>5</timerSubscribeDelta>
<timerT1>500</timerT1>
<timerT2>4000</timerT2>
<maxRedirects>70</maxRedirects>
<remotePartyID>true</remotePartyID>
<userInfo>None</userInfo>
</sipStack>
<autoAnswerTimer>1</autoAnswerTimer>
<autoAnswerAltBehavior>false</autoAnswerAltBehavior>
<autoAnswerOverride>true</autoAnswerOverride>
<transferOnhookEnabled>false</transferOnhookEnabled>
<enableVad>false</enableVad>
<preferredCodec>g711ulaw</preferredCodec>
<dtmfAvtPayload>101</dtmfAvtPayload>
<dtmfDbLevel>3</dtmfDbLevel>
<dtmfOutofBand>avt</dtmfOutofBand>
<alwaysUsePrimeLine>false</alwaysUsePrimeLine>
<alwaysUsePrimeLineVoiceMail>false</alwaysUsePrimeLineVoiceMail>
<kpml>3</kpml>
<natEnabled>false</natEnabled>
<phoneLabel>{{ item.number }}</phoneLabel>
<stutterMsgWaiting>0</stutterMsgWaiting>
<callStats>false</callStats>
<silentPeriodBetweenCallWaitingBursts>10</silentPeriodBetweenCallWaitingBursts>
<disableLocalSpeedDialConfig>false</disableLocalSpeedDialConfig>
<startMediaPort>10000</startMediaPort>
<stopMediaPort>20000</stopMediaPort>
<sipLines>
<line button="1">
<featureID>9</featureID>
<featureLabel>{{ item.number }}</featureLabel>
<proxy>USECALLMANAGER</proxy>
<port>5060</port>
<name>{{ item.number }}</name>
<displayName>{{ item.number }}</displayName>
<autoAnswer>
<autoAnswerEnabled>2</autoAnswerEnabled>
</autoAnswer>
<callWaiting>3</callWaiting>
<authName>{{ item.number }}</authName>
<authPassword>{{ item.password }}</authPassword>
<sharedLine>false</sharedLine>
<messageWaitingLampPolicy>1</messageWaitingLampPolicy>
<messagesNumber>*98</messagesNumber>
<ringSettingIdle>4</ringSettingIdle>
<ringSettingActive>5</ringSettingActive>
<contact>{{ item.number }}</contact>
<forwardCallInfoDisplay>
<callerName>true</callerName>
<callerNumber>true</callerNumber>
<redirectedNumber>false</redirectedNumber>
<dialedNumber>true</dialedNumber>
</forwardCallInfoDisplay>
</line>
</sipLines>
<voipControlPort>5060</voipControlPort>
<dscpForAudio>184</dscpForAudio>
<ringSettingBusyStationPolicy>0</ringSettingBusyStationPolicy>
<dialTemplate>dialplan.xml</dialTemplate>
</sipProfile>
<commonProfile>
<phonePassword></phonePassword>
<backgroundImageAccess>true</backgroundImageAccess>
<callLogBlfEnabled>1</callLogBlfEnabled>
</commonProfile>
<loadInformation>SIP41.8-5-2S</loadInformation>
<vendorConfig>
<disableSpeaker>false</disableSpeaker>
<disableSpeakerAndHeadset>false</disableSpeakerAndHeadset>
<pcPort>0</pcPort>
<settingsAccess>1</settingsAccess>
<garp>0</garp>
<voiceVlanAccess>0</voiceVlanAccess>
<videoCapability>0</videoCapability>
<autoSelectLineEnable>0</autoSelectLineEnable>
<webAccess>0</webAccess>
<spanToPCPort>1</spanToPCPort>
<loggingDisplay>1</loggingDisplay>
<loadServer></loadServer>
</vendorConfig>
<versionStamp>001</versionStamp>
<networkLocale>United_Kingdom</networkLocale>
<networkLocaleInfo>
<name>United_Kingdom</name>
<version>1.0.0.0-4</version>
</networkLocaleInfo>
<deviceSecurityMode>1</deviceSecurityMode>
<authenticationURL></authenticationURL>
<transportLayerProtocol>2</transportLayerProtocol>
<certHash></certHash>
<encrConfig>false</encrConfig>
<dialToneSetting>2</dialToneSetting>
</device>

View file

@ -0,0 +1,4 @@
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="{{ tftp_root_dir }}"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure --create"

9
site.yml Normal file
View file

@ -0,0 +1,9 @@
---
- name: Deploy Asterisk, NGINX and TFTP services
hosts: all
become: true
roles:
- common
- asterisk
- nginx
- tftp