Small Office Services

Abstract: This document will outline how to provide various network services for a small remote office, using Linux containerization (LXC). It is designed to be a complement to the DMVPN spoke node.
The following services will be available in addition to the encrypted communications between offices provided by the DMVPN network:
* Internet browsing proxy server with domain filtering (wired clients on protected internal network)
* Separate proxy for wifi clients
* SIP phone system including web based provisioning and basic voicemail services
The assumption is made that the following VLANs and subnets are used as the DMVPN document did:
Interface 	Description 	Subnet
bond0.3 	Management 	10.1.0.129/26
bond0.101 	LAN 	10.1.0.0/25
bond0.256 	Internet from ISP1 	Allocated from ISP
bond0.257 	Internet from ISP2 	Allocated from ISP
bond0.620 	Transit between wifi proxy and dmvpn spoke node	10.1.0.252/30
bond0.701 	WiFi clients (no access to DMVPN network) 	172.17.48.0/24
bond0.1101	Voice 	10.2.0.0/24
= Hardware =
For an office that will serve under 20 people, the following containers can easily run on low-power hardware such as a Via Nano 1.6Ghz Jetway board with 8GB RAM with dual 500GB SATA hard drives running in RAID 1 (software).
= Setup LXC Host Box =
== Boot Alpine USB == 
Follow the instructions at Create a Bootable Device.
== Alpine Setup ==
You will be prompted something like this... 	Suggestion on what you could enter...
Select keyboard layout [none]: 	Type an appropriate layout for you
Select variant: 	Type an appropriate layout for you (if prompted)
Enter system hostname (short form, e.g. 'foo') [localhost]: 	Enter the hostname, e.g. lxc-host
Available interfaces are: eth0Enter '?' for help on bridges, bonding and vlans.Which one do you want to initialize? (or '?' done')	Enter bond0.3
Available bond slaves are: eth0 eth1Which slave(s) do you want to add to bond0? (or 'done') [eth0] 	eth0 eth1
IP address for bond0? (or 'dhcp', 'none', '?') [dhcp]: 	Press Enter confirming 'none'
IP address for bond0.3? (or 'dhcp', 'none', '?') [dhcp]: 	<%LXCHOST_MANAGEMENT_IP_ADDRESS%>
Netmask? [255.255.255.0]: 	<%DMVPN_MANAGEMENT_NETMASK%>
Gateway? (or 'none') [none]: 	<%DMVPN_MANAGEMENT_NET_IP%>
Do you want to do any manual network configuration? [no] 	no
DNS domain name? (e.g. 'bar.com') []: 	Enter the domain name of your intranet, e.g., office.example.net
DNS nameservers(s)? []: 	8.8.8.8 8.8.4.4 (we will change them later)
Changing password for rootNew password: 	Enter a secure password for the console
Retype password: 	Retype the above password
Which timezone are you in? ('?' for list) [UTC]: 	Press Enter confirming 'UTC'
HTTP/FTP proxy URL? (e.g. 'http://proxy:8080', or 'none') [none] 	http://<%DMVPN_LAN_IP%>:8080
Enter mirror number (1-9) or URL to add (or r/f/e/done) [f]: 	Select a mirror close to you and press Enter
Which SSH server? ('openssh', 'dropbear' or 'none') [openssh]: 	Press Enter confirming 'openssh'
Which NTP client to run? ('openntpd', 'chrony' or 'none') [chrony]: 	Press Enter confirming 'chrony'
Which disk(s) would you like to use? (or '?' for help or 'none') [none]: 	sda sdb
How would you like to use them? ('sys', 'data' or '?' for help): 	data
Enter where to store configs ('floppy', 'usb' or 'none') [usb]: 	Press Enter confirming 'usb'
Enter apk cache directory (or '?' or 'none') [/media/usb/cache]: 	Press Enter confirming '/media/usb/cache'
Upgrade packages
Save Changes 
Finish Setup with a reboot
== Setup Networking ==
With your favorite editor configure /etc/network/interfaces
Apply changes by restarting networking
== Enable IP Forwarding ==
== Setup Firewall ==
With your favorite editor, create the base policy for the firewall
Activate the firewall, and allow iptables to startup automatically at boot
== Install LXC ==
Install the LXC and Bridge packages
With your favorite editor configure /etc/lxc/default.conf
Finish Installation
= Install the Web Proxy Container =
== Create and Configure the container ==
Create the startup Script
Edit the container's config file found at /var/lib/lxc/webproxy/config, to reflect the network for the web proxy container
Start the container
Configure the container to automatically start
== Enter the webproxy container ==
Login as root
Remove obsolete /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Startup networking 
Add rule to DMVPN awall policy to allow this proxy out to the internet
Configure remote administration
Start ssh
Configure a passwd for the container
Setup acf for web administration
== Setup Firewall ==
With your favorite editor, create the policies for the firewall
Activate the firewall, and allow iptables to startup automatically at boot
== Install and Configure the Squid Web Proxy Service ==
Install the required packages 
Configure /etc/squid/squid.conf, replace <%WEBPROXY_IP_ADDRESS%>, <%HOSTNAME%>, and <%DOMAIN%>
Configure /etc/lighttpd/lighttpd.conf, replace <%WEBPROXY_IP_ADDRESS%>
Configure mod_cgi.conf
Link the Squark web pages to the Web server home directory
Create a Squark group
Make 'squid' and 'lighttpd' users member of the group squark
Start lighttpd, and configure the service to start on when container is booted
Start Squid, and configure to start at boot
= Install the DHCP and DNS server Container =
== Create and Configure the container ==
Create the startup Script
Edit the container's config file found at /var/lib/lxc/dhcpdns/config, to reflect the network for the web proxy container
Start the container
Configure the container to automatically start
== Enter the dhcpdns container ==
Login as root
Remove obsolete /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Startup networking 
Configure and enable proxy settings
Configure remote administration
Start ssh
Configure a passwd for the container
Setup acf for web administration
== Setup Firewall ==
With your favorite editor, create the policies for the firewall
Activate the firewall, and allow iptables to startup automatically at boot
== Install and Configure DHCP and DNS services ==
install the dhcpd package
Create a new dhcpd.conf file
Start DHCP service and add to runlevel default 
Install nsd and unbound packages
Remove unbound.conf
Create with your favorite editor a new configuration for unbound
Start Unbound and allow the container to use it
{{Cmd|rc-service unbound start
rc-update add unbound
echo nameserver 10.2.0.2 > /etc/resolv.conf
Install nsd
Configure nsd configuration
Configure Zone file for nsd
Check nsd configuration and start service
= Install the SIP Container =
== Create and Configure the container ==
Create the startup Script
Edit the container's config file found at /var/lib/lxc/sip/config, to reflect the network for the sip container
Start the container
Configure the container to automatically start
== Enter the sip container ==
Login as root
Remove obsolete /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Startup networking 
Configure and enable proxy settings
Configure remote administration
Start ssh
Configure a passwd for the container
Setup acf for web administration
== Setup Firewall ==
With your favorite editor, create the policies for the firewall
Activate the firewall, and allow iptables to startup automatically at boot
==Install and Configure Postgresql==
Install postgresql package
Prepare the database
Configure /var/lib/postgresql/9.3/data/postgresql.conf to set the 'log_destination' variable to show:
Start up the database and configure postgresql to start at boot up
== Install acf-provisioning ==
* vi /etc/kamailio/kamctlrc
SIP_DOMAIN=sip.office.example.net
DBENGINE=PGSQL
DBHOST=127.0.0.1
DBNAME=openser
DBRWUSER=openser
DBRWPW="openser"
DBROUSER=openserro
DBROPW=openserro
DBROOTUSER="postgres"
* yes | kamdbctl create openser
* apk add acf-provisioning lua-socket lua-expat
* Create /etc/provisioning/update_device_params.lua:
-- This is the script run after editing device params - basically only worried about extension and password
local functions, params, oldparams = ...
require("posix")
require("luasql.postgres")
local root = "/var/www/provisioning/htdocs/"
local b62 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
APP.logevent("got to update_device_params script")
local function generatepw()
	-- generate a random 12-character alphanumeric string
	local file = io.open("/dev/urandom")
	local str = ""
	if file == nil then return nil end
	local size = 12
	while (size > 0 ) do
		local offset = (string.byte(file:read(1)) % 62) + 1
		str = str .. string.sub (b62, offset, offset)
		size = size - 1
	end
	return str
end
local function findip(mac)
	if not mac or mac == "" then
		return nil
	end
	local ipaddr = functions.getselectresponse("SELECT ip FROM provisioning_requests WHERE mac~*'"..mac.."'")
	if ipaddr and ipaddr[1] then
		return ipaddr[1].ip
	end
end
local function addfuturenotify(ipaddr, extension)
	local res, err = pcall(function()
		functions.runsqlcommand("DELETE FROM notify WHERE ipaddr='"..ipaddr.."' AND extension='"..extension.."'")
	end)
	if not res and err then
		if string.match(err, "relation \"(%S+)\" does not exist") then
			functions.runsqlcommand("CREATE TABLE notify (ipaddr text, extension text, seasoned boolean DEFAULT false)")
		else
			assert(res, err)
		end
	end
	-- if table missing, create it and delete again
	functions.runsqlcommand("INSERT INTO notify VALUES('"..ipaddr.."', '"..extension.."')")
end
local notify_device = function(mac, extension)
	local ipaddr = findip(mac)
	if ipaddr then
		APP.logevent("Notifying "..ipaddr.." to update for "..(mac or ""))
		os.execute("/etc/provisioning/notify_device "..ipaddr.." "..extension)
		addfuturenotify(ipaddr, extension)
	else
		APP.logevent("Warning - could not find IP address for "..(mac or ""))
	end
end
local kam = APP:new("kamailio/kamailio")
-- A table of devices to notify to update
local devices = {}
-- First, we check the registration numbers / passwords to 1) set a random password (if necessary) 2) make sure password matches any other registrations to same extension 3) push changes (add / delete / or update) to Kam database
-- Because this script also handles when device classes change, we have to consider that extensions are added / removed
local regs = {}
local forwarding = {"forwardnoanswerenable", "forwardnoanswer", "forwardbusyenable", "forwardbusy", "forwardallenable", "forwardall"}
local forwardsettings = {} -- Collect forwarding settings
local passwords = {} -- Collect old extension/password pairs
for name,val in pairs(params.value) do
	if not regs[name] and string.match(name, "^reg") then
		regs[name] = true
	end
end
oldparams = oldparams or {value={}}
for name,val in pairs(oldparams.value) do
	if string.match(name, "^reg") then
		if val.value and val.value.extension and val.value.password then
			passwords[val.value.extension.value] = val.value.password.value
		end
		if val.value and val.value.extension then
			local fwd = {}
			for i,f in ipairs(forwarding) do
				if val.value[f] then
					fwd[f] = val.value[f].value
				end
			end
			forwardsettings[val.value.extension.value] = fwd
		end
		if not regs[name] then
			regs[name] = true
		end
	end
end
local extension_id = params.value.reg1.value.extension.param_id
local password_id = params.value.reg1.value.password.param_id
functions.runsqlcommand("BEGIN TRANSACTION")
for name,val in pairs(regs) do
	local new = ""
	local old = ""
	if params.value[name] then new = params.value[name].value.extension.value end
	if oldparams.value[name] then old = oldparams.value[name].value.extension.value end
	if new ~= old then
		-- Extension changed
		-- First, let's remove the stuff for the old extension
		if old ~= "" then
			local others = functions.getselectresponse("SELECT count(*) FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..old.."'")
			if others[1].count == "0" then
				APP.logevent("Removing old registration "..old)
				-- remove the registration
				kam.model.delete_user(old)
			end
		end
		-- Now, add the new extension
		if new ~= "" then
			local pass
			if params.value[name].value.password.value ~= "" and (not oldparams.value[name] or (oldparams.value[name].value.password.value ~= params.value[name].value.password.value)) then
				-- The password parameter was changed and not blank, so use it
				pass = params.value[name].value.password.value
				APP.logevent("Added a new registration "..new.." with specified password "..pass)
				-- There may be other devices with this extension
				local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
				for i,o in ipairs(others) do
					-- We'll notify whether it changed or not
					devices[o.value] = new
				end
			elseif passwords[new] then
				pass = passwords[new]
				APP.logevent("Added a new registration "..new.." with reused password "..pass)
			else
				local others = functions.getselectresponse("SELECT * FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."' AND device_id!='"..params.value.device_id.value.."'")
				-- If this is a new registration, use a new password
				if #others == 0 then
					pass = generatepw()
					APP.logevent("Added a new registration "..new.." with random password "..pass)
				else
					-- Use the old password
					local p = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id='"..password_id.."' AND device_id='"..others[1].device_id.."' AND group_name='"..others[1].group_name.."' LIMIT 1")
					pass = p[1].value
					APP.logevent("Added a new registration "..new.." with reused password "..pass)
				end
			end
			passwords[new] = pass
			params.value[name].value.password.value = pass
			functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..password_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
			functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..password_id.."', '"..pass.."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
			local u = kam.model.get_user(new)
			if u.value.username.errtxt then
				-- Add this registration to Kam
				local u = kam.model.get_new_user()
				u.value.username.value = new
				u.value.password.value = pass
				u.value.password_confirm.value = pass
				kam.model.create_new_user(u)
			else
				u.value.password.value = pass
				u.value.password_confirm.value = pass
				kam.model.update_user(u)
			end
			-- Let's also look at the forwarding settings
			local change = false
			local supported = false
			local fwd = {}
			for i,f in ipairs(forwarding) do
				if params.value[name].value[f] then
					supported = true
					fwd[f] = params.value[name].value[f].value
					if ((not oldparams.value[name] or not oldparams.value[name].value[f]) and (params.value[name].value[f].value ~= params.value[name].value[f].default)) or
						(oldparams.value[name] and oldparams.value[name].value[f] and (params.value[name].value[f].value ~= oldparams.value[name].value[f].value)) then
						change = true
					end
				end
				if change then
					local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
					for i,o in ipairs(others) do
						-- We'll notify whether it changed or not
						devices[o.value] = new
					end
				end
			end
			if supported then
				if not change and forwardsettings[new] then
					fwd = forwardsettings[new]
				elseif not change then
					-- This is a new extension, and the forwarding has not been set. We should check to see if there are any other devices with this extension
					local others = functions.getselectresponse("SELECT * FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."' AND device_id!='"..params.value.device_id.value.."'")
					if #others > 0 then
						-- Use the existing settings
						for i,f in ipairs(forwarding) do
							local v = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND device_id='"..others[1].device_id.."' AND group_name='"..others[1].group_name.."' LIMIT 1")
							if #v > 0 then
								fwd[f] = v[1].value
							else
								fwd[f] = params.value[name].value[f].default
							end
						end
					end
				end
				if not change then
					for i,f in ipairs(forwarding) do
						params.value[name].value[f].value = fwd[f]
					end
				end
				forwardsettings[new] = fwd
				for i,f in ipairs(forwarding) do
					functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
					if fwd[f] ~= params.value[name].value[f].default then
						functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..params.value[name].value[f].param_id.."', '"..tostring(fwd[f]).."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
					end
				end
			end
		end
	elseif params.value[name] and oldparams.value[name] and params.value[name].value.extension.value ~= "" then
		if params.value[name].value.password.value ~= oldparams.value[name].value.password.value then
			-- Password changed - make any other registrations to this extension also change
			local pass = params.value[name].value.password.value
			if pass and pass ~= "" then
				APP.logevent("Password changed for "..new.." from "..oldparams.value[name].value.password.value.." to "..pass)
			else
				pass = generatepw()
				APP.logevent("Password cleared for "..new..", so set new random password "..pass)
			end
			passwords[new] = pass
			functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..password_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
			functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..password_id.."', '"..pass.."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
			local u = kam.model.get_user(new)
			u.value.password.value = params.value[name].value.password.value
			u.value.password_confirm.value = params.value[name].value.password.value
			kam.model.update_user(u)
			-- Have to notify those other devices too
			local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
			for i,o in ipairs(others) do
				devices[o.value] = new
			end
		end
		local change = false
		local fwd = {}
		for i,f in ipairs(forwarding) do
			if params.value[name].value[f] and (not oldparams.value[name].value[f] or params.value[name].value[f].value ~= oldparams.value[name].value[f].value) then
				change = true
				break
			end
		end
		if change then
			-- Forwarding settings changed - make any other registrations to this extension also change
			for i,f in ipairs(forwarding) do
				functions.runsqlcommand("DELETE FROM provisioning_values WHERE param_id='"..params.value[name].value[f].param_id.."' AND (device_id, group_name) IN (SELECT device_id, group_name FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
				if params.value[name].value[f].value ~= params.value[name].value[f].default then
					functions.runsqlcommand("INSERT INTO provisioning_values (SELECT device_id, group_name, '"..params.value[name].value[f].param_id.."', '"..tostring(params.value[name].value[f].value).."' FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
				end
			end
			-- Have to notify those other devices too
			local others = functions.getselectresponse("SELECT value FROM provisioning_values WHERE param_id=(SELECT param_id FROM provisioning_params WHERE name='mac') AND device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND value='"..new.."')")
			for i,o in ipairs(others) do
				devices[o.value] = new
			end
		end
	end
end
functions.runsqlcommand("COMMIT")
-- If reg1 or freepstn changed, then need to change free-pstn in kam
-- Since there can be multiple devices with the same reg1, we can't rely on params and oldparams
-- Check both reg's
--APP.logevent(session.serialize("params", params))
local reg = {}
if oldparams.value.reg1 and oldparams.value.reg1.value.extension and oldparams.value.reg1.value.extension.value ~= "" then
	reg[oldparams.value.reg1.value.extension.value] = false
end
if params.value.reg1 and params.value.reg1.value.extension and params.value.reg1.value.extension.value ~= "" then
	local pstn = false
	if params.value.routing and params.value.routing.value.freepstn then
		pstn = params.value.routing.value.freepstn.value
	end
	reg[params.value.reg1.value.extension.value] = pstn
end
APP.logevent("Looking at free-pstn")
for r,f in pairs(reg) do
	-- if we're not sure, check provisioning database
	if not f then
		-- Most devices will have free-pstn due to class-of-service, but can be overridden, so this can get tricky
		-- Check all devices where reg1 extension = r, and see what the freepstn value is for each
		local others = functions.getselectresponse("SELECT CASE WHEN v.value IS NOT NULL THEN v.value WHEN g2p.value IS NOT NULL THEN g2p.value ELSE p.value END AS value "..
			"FROM (devices_to_classes d2t JOIN provisioning_classes t USING(class_id) JOIN classes_to_param_groups t2g USING (class_id) JOIN provisioning_groups g USING(group_id) "..
			"JOIN param_groups_to_params g2p USING(group_id) JOIN provisioning_params p USING(param_id)) LEFT JOIN provisioning_values v ON (d2t.device_id=v.device_id AND p.param_id=v.param_id AND g.name=v.group_name ) "..
			"WHERE p.name='freepstn' AND d2t.device_id IN (SELECT device_id FROM provisioning_values WHERE param_id='"..extension_id.."' AND group_name='reg1' AND value='"..r.."')")
		for i,o in ipairs(others) do
			-- Now check the freepstn value for each one
			if o.value == "true" then
				f = true
				break
			end
		end
	end
	-- Now, check the Kamailio group table
	local alreadythere = false
	local entries = kam.model.list_table_entries("grp")
	for i,e in ipairs(entries.value.entries.value) do
		if e.username == r then
			alreadythere = true
			if not f then
				APP.logevent("Removing free-pstn for "..r)
				-- Remove free-pstn from the old extension
				kam.model.delete_table_entry("grp", e.id)
			end
			break
		end
	end
	if f and not alreadythere then
		APP.logevent("Adding free-pstn for "..r)
		-- Add free-pstn to the new extension
		local e = kam.model.get_table_entry("grp")
		e.value.username.value = r
		e.value.domain.value = ""
		e.value.grp.value = "free-pstn"
		e.value.last_modified.value = os.date("%c")
		kam.model.create_table_entry(e)
	end
end
kam:destroy()
-- If the mac address changed for Polycom with valid MAC (not blank or all 0's), we need to move the associated config files
if oldparams.value.device and oldparams.value.device.value.mac and oldparams.value.device.value.mac.value ~= "" and params.value.device and params.value.device.value.mac and params.value.device.value.mac.value ~= oldparams.value.device.value.mac.value then
	if string.match(oldparams.value.device.label, "Polycom") and string.match(oldparams.value.device.value.mac.value, "[1-9A-F]") then
		local deletefiles = true
		if string.match(params.value.device.value.mac.value, "[1-9A-F]") then
			--APP.logevent("Moving files for "..oldparams.value.device.value.mac.value)
			deletefiles = false
		else
			--APP.logevent("Deleting files for "..oldparams.value.device.value.mac.value)
		end
		local path = root.."Polycom/"
		if posix.stat(path, "type") == "directory" then
	 for d in posix.files(path) do
 	 if string.match(d, string.lower(oldparams.value.device.value.mac.value)) and posix.stat(path..d, "type") == "regular" then
 	 local newfile = string.gsub(d, string.lower(oldparams.value.device.value.mac.value), string.lower(params.value.device.value.mac.value))
					if deletefiles then
	 	 --APP.logevent("deleting "..path..d)
 	 	os.remove(path..d)
					else
	 	 --APP.logevent("moving "..path..d.." to "..path..newfile)
 	 	os.rename(path..d, path..newfile)
					end
				end
 end
 end
 end
end
-- Then, notify the phone to pull it's config
-- Try to get a valid extension currently on the device
local oldexten = ""
for name,val in pairs(oldparams.value) do
	if string.match(name, "^reg") and val.value.extension and val.value.extension.value ~= "" then
		oldexten = val.value.extension.value
		break
	end
end
if params.value.device and params.value.device.value.mac and params.value.device.value.mac.value ~= "" then
	devices[params.value.device.value.mac.value] = oldexten
end
if oldparams.value.device and oldparams.value.device.value.mac and oldparams.value.device.value.mac.value ~= "" then
	devices[oldparams.value.device.value.mac.value] = oldexten
end
for name,value in pairs(devices) do
	notify_device(name,value)
end
* Create /etc/provisioning/provisioning_db_script
#!/usr/bin/lua
local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin "
local creation_script = {
	-- Parameters
	"INSERT INTO provisioning_params VALUES(default, 'freepstn', 'boolean', 'Free PSTN Access', , 'false', '200', )",
	-- Parameter Groups
	"INSERT INTO provisioning_groups VALUES(default, 'routing', 'Free PSTN Access', '31')",
	"INSERT INTO provisioning_groups VALUES(default, 'routing', 'No Free PSTN Access', '32')",
	-- param_groups_to_params (group_id, param_id, value, editable)
	"INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='Free PSTN Access'), (SELECT param_id FROM provisioning_params WHERE name='freepstn'), 'true', false)",
	"INSERT INTO param_groups_to_params VALUES((SELECT group_id FROM provisioning_groups WHERE label='No Free PSTN Access'), (SELECT param_id FROM provisioning_params WHERE name='freepstn'), 'false', false)",
	
	-- Classes
	-- provisioning_class_groups (class_group_id, name, label, seq)
	"INSERT INTO provisioning_class_groups VALUES(default, 'routing', 'Routing', '3')",
	-- provisioning_classes (class_id, class_group_id, label, seq)
	"INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='routing'), 'Free PSTN Access', '1')",
	"INSERT INTO provisioning_classes VALUES(default, (SELECT class_group_id FROM provisioning_class_groups WHERE name='routing'), 'No Free PSTN Access', '2')",
	-- classes_to_param_groups (class_id, group_id)
	"INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='Free PSTN Access'), (SELECT group_id FROM provisioning_groups WHERE label='Free PSTN Access'))",
	"INSERT INTO classes_to_param_groups VALUES((SELECT class_id FROM provisioning_classes WHERE label='No Free PSTN Access'), (SELECT group_id FROM provisioning_groups WHERE label='No Free PSTN Access'))",
	-- provisioning_options
	"INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='polycomringtone'), 'Warble', '15', '15')",
	"INSERT INTO provisioning_options VALUES((SELECT param_id FROM provisioning_params WHERE name='polycomringtone'), 'Analog Ring', '16', '16')",
}
local f = io.popen("/usr/share/acf/www/cgi-bin/cli /provisioning/provisioning/getdevicevalues")
print(f:read("*a"))
f:close()
for i,c in ipairs(creation_script) do
	print(path..'psql -U postgres -c "'..c..'" provisioning 2>&1')
	local f = io.popen(path..'psql -U postgres -c "'..c..'" provisioning 2>&1')
	print(f:read("*a"))
	f:close()
end
* chmod 755 /etc/provisioning/provisioning_db_script
* /etc/provisioning/provisioning_db_script
* echo '* * * * * run-parts /etc/periodic/1min' >> /etc/crontabs/root
* mkdir /etc/periodic/1min/
* /etc/periodic/1min/notify_device
#!/usr/bin/lua
-- Load libraries
require("luasql.postgres")
-- Set variables
local DatabaseName = "provisioning"
local DatabaseUser = "postgres"
local DatabasePassword
local path = "PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin "
local env
local con
-- ################################################################################
-- LOCAL FUNCTIONS
local function assert (v, m)
	if not v then
		m = m or "Assertion failed!"
		error(m, 0)
	end
	return v, m
end
-- Escape special characters in sql statements
local escape = function(sql)
	sql = sql or ""
	sql = string.gsub(sql, "'", "")
	return string.gsub(sql, "\\", "\\\\")
end
local databaseconnect = function()
	if not con then
		-- create environment object
		env = assert (luasql.postgres())
		-- connect to data source
		local err
		con, err = assert(env:connect(DatabaseName, DatabaseUser, DatabasePassword))
		return true
	end
	return false
end
local databasedisconnect = function()
	if env then
		env:close()
		env = nil
	end
	if con then
		con:close()
		con = nil
	end
end
local getselectresponse = function(sql)
	local retval = {}
	local cur = assert (con:execute(sql))
	local row = cur:fetch ({}, "a")
	while row do
		local tmp = {}
		for name,val in pairs(row) do
			tmp[name] = val
		end
		retval[#retval + 1] = tmp
		row = cur:fetch (row, "a")
	end
	cur:close()
	return retval
end
databaseconnect()
-- First, let's notify for the seasoned requests
local reqs = getselectresponse("SELECT * FROM notify WHERE seasoned='true'")
for i,r in ipairs(reqs) do
	os.execute("/etc/provisioning/notify_device "..r.ipaddr.." "..r.extension)
end
-- Then mark any others as seasoned
assert(con:execute("DELETE FROM notify WHERE seasoned='true'"))
assert(con:execute("UPDATE notify SET seasoned='true' WHERE seasoned='false'"))
databasedisconnect()
* apk add lighttpd
* rm /etc/lighttpd/lighttpd.conf
* ln -s /etc/provisioning/lighttpd.sample.conf /etc/lighttpd/lighttpd.conf
* /etc/lighttpd/mod_cgi.conf:
###############################################################################
# mod_cgi.conf
# include'd by lighttpd.conf.
# $Header: /var/cvsroot/gentoo-x86/www-servers/lighttpd/files/conf/mod_cgi.conf,v 1.1 2005/08/27 12:36:13 ka0ttic Exp $
###############################################################################
#
# see cgi.txt for more information on using mod_cgi
#
server.modules += ("mod_cgi")
# NOTE: this requires mod_alias
alias.url = (
 "/cgi-bin/"	 =>	 var.basedir + "/cgi-bin/"
)
#
# Note that you'll also want to enable the
# cgi-bin alias via mod_alias (above).
#
$HTTP["url"] =~ "^/cgi-bin/" {
 # disable directory listings
 dir-listing.activate = "disable"
 # only allow cgi's in this directory
 cgi.assign = (
		".pl"	=>	"/usr/bin/perl",
		".cgi"	=>	"",
		""	=>	""
	)
}
# vim: set ft=conf foldmethod=marker et :
* rc-service lighttpd start
* rc-update add lighttpd
* Customize provisioning:
Parameter	Default Value
registrar	 IP address or host name of SIP Router(s)
digitmap	 Digit map for your phone system (See Polycom Digit Map Reference)
digitmaptimeout	 Timeout in seconds corresponding to digitmap
sntpserver	 10.2.0.1 (DMVPN spoke node)
timezone	 Timezone information for this location - see below
musiconhold	 SIP uri of the music-on-hold service - moh@media.office.example.net
adminpassword	 Administration password for advanced settings on phone (on-screen and web interface)
* apk fetch --stdout acf-provisioning-polycom | tar -C / -zx
* Create a test Polycom device and boot it in the voice network to verify that it works
== Install Kamailio ==
* apk add kamailio kamailio-presence kamailio-pcre kamailio-postgres
* rc-service kamailio start
* rc-update add kamailio
* FUTURE: POST CONFIG THAT USES SUBSCRIBER TABLE FOR AUTH
= Install the SIP Media container =
== Create and Configure the container ==
Create the startup Script
Edit the container's config file found at /var/lib/lxc/sipmedia/config, to reflect the network for the SIP Media container
Start the container
Configure the container to automatically start
== Enter the SIP Media container ==
Login as root
Remove obsolete /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Startup networking 
Configure and enable proxy settings
Configure remote administration
Start ssh
Configure a passwd for the container
Setup acf for web administration
== Setup Firewall ==
With your favorite editor, create the policies for the firewall
Enable and activate firewall policies, and configure iptables to start at boot
== Install and Configure Freeswitch ==
Install package
Configure /etc/freeswitch/freeswitch.xml
Start Freeswitch and configure to start at boot
= Install the WiFi Web Proxy Container =
== Create and Configure the container ==
Create the startup Script
Edit the container's config file found at /var/lib/lxc/wifi/config, to reflect the network for the wifi container
Start the container
Configure the container to automatically start
== Enter the wifi container ==
Login as root
Remove obsolete /etc/network/interfaces
Create and configure the new /etc/network/interfaces as shown below:
Startup networking 
Configure remote administration
Start ssh
Configure a passwd for the container
Setup acf for web administration
== Setup Firewall ==
==Install and Configure the Recursive DNS Service ==
Install unbound package
With your favorite editor configure /etc/unbound/unbound.conf
== Install and Configure the Proxy service ==
Install the necessary packages
With your preferred editor configure /etc/squid/squid.conf
Configure lighttpd
Link Squark web pages to the Web server home directory
Make 'squid' and 'lighttpd' users member of the group squark
Start lighttpd and configure the Web service to start at boot
Start Squid and configure it to start at boot
= See also =
* Freeswitch Voicemail On Alpine Linux
Category:ACF