diff --git a/.gitignore b/.gitignore
index 5c199eb..3de66d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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
diff --git a/LICENSE b/LICENSE
index edca963..b87d5fa 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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:
diff --git a/README.md b/README.md
index 97dedbd..36229ae 100644
--- a/README.md
+++ b/README.md
@@ -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.
\ No newline at end of file
+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.
diff --git a/inventories/production/group_vars/all.yml b/inventories/production/group_vars/all.yml
new file mode 100644
index 0000000..84cda56
--- /dev/null
+++ b/inventories/production/group_vars/all.yml
@@ -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
+
diff --git a/inventories/production/group_vars/asterisk.yml b/inventories/production/group_vars/asterisk.yml
new file mode 100644
index 0000000..0e64e33
--- /dev/null
+++ b/inventories/production/group_vars/asterisk.yml
@@ -0,0 +1,3 @@
+asterisk_version: "22.3.0"
+asterisk_src_dir: "/usr/local/src/asterisk-{{ asterisk_version }}"
+asterisk_config_dir: "/etc/asterisk"
diff --git a/inventories/production/group_vars/tftp.yml b/inventories/production/group_vars/tftp.yml
new file mode 100644
index 0000000..e8471df
--- /dev/null
+++ b/inventories/production/group_vars/tftp.yml
@@ -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
diff --git a/inventories/production/hosts.ini b/inventories/production/hosts.ini
new file mode 100644
index 0000000..8ec758c
--- /dev/null
+++ b/inventories/production/hosts.ini
@@ -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
diff --git a/roles/asterisk/handlers/main.yml b/roles/asterisk/handlers/main.yml
new file mode 100644
index 0000000..d2bcc47
--- /dev/null
+++ b/roles/asterisk/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: Reload Asterisk
+ ansible.builtin.service:
+ name: asterisk
+ state: reloaded
diff --git a/roles/asterisk/tasks/main.yml b/roles/asterisk/tasks/main.yml
new file mode 100644
index 0000000..0cfe9b2
--- /dev/null
+++ b/roles/asterisk/tasks/main.yml
@@ -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
diff --git a/roles/asterisk/templates/extensions.conf.j2 b/roles/asterisk/templates/extensions.conf.j2
new file mode 100644
index 0000000..14fa947
--- /dev/null
+++ b/roles/asterisk/templates/extensions.conf.j2
@@ -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 %}
diff --git a/roles/asterisk/templates/pjsip.conf.j2 b/roles/asterisk/templates/pjsip.conf.j2
new file mode 100644
index 0000000..56b9c33
--- /dev/null
+++ b/roles/asterisk/templates/pjsip.conf.j2
@@ -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 %}
diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml
new file mode 100644
index 0000000..cf14745
--- /dev/null
+++ b/roles/common/tasks/main.yml
@@ -0,0 +1,4 @@
+- name: Run APT updates
+ ansible.builtin.apt:
+ update_cache: yes
+ upgrade: dist
diff --git a/roles/nginx/handlers/main.yml b/roles/nginx/handlers/main.yml
new file mode 100644
index 0000000..8fd4376
--- /dev/null
+++ b/roles/nginx/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: Reload NGINX
+ ansible.builtin.service:
+ name: nginx
+ state: reloaded
diff --git a/roles/nginx/tasks/main.yml b/roles/nginx/tasks/main.yml
new file mode 100644
index 0000000..e3cd8ca
--- /dev/null
+++ b/roles/nginx/tasks/main.yml
@@ -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'
diff --git a/roles/nginx/templates/default b/roles/nginx/templates/default
new file mode 100644
index 0000000..f01944b
--- /dev/null
+++ b/roles/nginx/templates/default
@@ -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;
+# }
+#}
diff --git a/roles/nginx/templates/directory.xml.j2 b/roles/nginx/templates/directory.xml.j2
new file mode 100644
index 0000000..b01f498
--- /dev/null
+++ b/roles/nginx/templates/directory.xml.j2
@@ -0,0 +1,10 @@
+
+ Company Directory
+ Select a contact
+ {% for extension in extensions %}
+
+ {{ extension.callername }}
+ {{ extension.number }}
+
+ {% endfor %}
+
diff --git a/roles/tftp/files/dialplan.xml b/roles/tftp/files/dialplan.xml
new file mode 100644
index 0000000..bd800f3
--- /dev/null
+++ b/roles/tftp/files/dialplan.xml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/roles/tftp/handlers/main.yml b/roles/tftp/handlers/main.yml
new file mode 100644
index 0000000..74f59b9
--- /dev/null
+++ b/roles/tftp/handlers/main.yml
@@ -0,0 +1,4 @@
+- name: Restart TFTP
+ ansible.builtin.service:
+ name: tftpd-hpa
+ state: restarted
diff --git a/roles/tftp/tasks/main.yml b/roles/tftp/tasks/main.yml
new file mode 100644
index 0000000..825ebbb
--- /dev/null
+++ b/roles/tftp/tasks/main.yml
@@ -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'
diff --git a/roles/tftp/templates/SEPMAC.cnf.xml.j2 b/roles/tftp/templates/SEPMAC.cnf.xml.j2
new file mode 100644
index 0000000..c2ae1c8
--- /dev/null
+++ b/roles/tftp/templates/SEPMAC.cnf.xml.j2
@@ -0,0 +1,159 @@
+
+ SIP
+ cisco
+ cisco
+ 0
+ http://{{ hostvars[groups['nginx'][0]].ansible_host }}/phones/directory.xml
+ http://{{ hostvars[groups['nginx'][0]].ansible_host }}/phones/directory.xml
+
+
+
+ D/M/Ya
+ GMT Standard/Daylight Time
+
+
+ {{ ntp_server }}
+ Unicast
+
+
+
+
+
+
+
+
+
+ 2000
+ 5060
+
+ {{ hostvars[groups['asterisk'][0]].ansible_host }}
+
+
+
+
+
+
+
+
+ true
+
+
+ true
+ false
+ 2
+ true
+ true
+ 2
+ 2
+ 0
+ true
+
+
+
+ 6
+ 10
+ 180
+ 300
+ 5
+ 120
+ 120
+ 5
+ 500
+ 4000
+ 70
+ true
+ None
+
+
+ 1
+ false
+ true
+ false
+ false
+ g711ulaw
+ 101
+ 3
+ avt
+ false
+ false
+ 3
+ false
+ {{ item.number }}
+ 0
+ false
+ 10
+ false
+ 10000
+ 20000
+
+
+
+ 9
+ {{ item.number }}
+ USECALLMANAGER
+ 5060
+ {{ item.number }}
+ {{ item.number }}
+
+ 2
+
+ 3
+ {{ item.number }}
+ {{ item.password }}
+ false
+ 1
+ *98
+ 4
+ 5
+ {{ item.number }}
+
+ true
+ true
+ false
+ true
+
+
+
+
+ 5060
+ 184
+ 0
+ dialplan.xml
+
+
+
+
+ true
+ 1
+
+
+ SIP41.8-5-2S
+
+ false
+ false
+ 0
+ 1
+ 0
+ 0
+ 0
+ 0
+ 0
+ 1
+ 1
+
+
+
+ 001
+ United_Kingdom
+
+ United_Kingdom
+ 1.0.0.0-4
+
+
+ 1
+
+ 2
+
+ false
+ 2
+
diff --git a/roles/tftp/templates/tftpd-hpa.j2 b/roles/tftp/templates/tftpd-hpa.j2
new file mode 100644
index 0000000..971f968
--- /dev/null
+++ b/roles/tftp/templates/tftpd-hpa.j2
@@ -0,0 +1,4 @@
+TFTP_USERNAME="tftp"
+TFTP_DIRECTORY="{{ tftp_root_dir }}"
+TFTP_ADDRESS="0.0.0.0:69"
+TFTP_OPTIONS="--secure --create"
diff --git a/site.yml b/site.yml
new file mode 100644
index 0000000..b7a9c98
--- /dev/null
+++ b/site.yml
@@ -0,0 +1,9 @@
+---
+- name: Deploy Asterisk, NGINX and TFTP services
+ hosts: all
+ become: true
+ roles:
+ - common
+ - asterisk
+ - nginx
+ - tftp