Developing Provisioning Plugins¶
Here is an example of how to develop a provisioning plugin for Digium phones. You can find all the code on Github.
Phone Analysis¶
Here’s a non-exhaustive list of what a phone may or may not support:
- Language
- Timezone
- UTF-8
- Reboot of the phone (SIP notify ?)
- Simple call
- Blind transfer
- Attended transfer
- Firmware upgrade
- Multiple lines
- DTMF (RTP ? SIP ?)
- MWI (voicemail indication)
- Voicemail button
- Call on hold
- Function keys
- Call interception (with BLF)
- NTP
DHCP Configuration¶
In xivo-provd-plugins/provisioning/dhcpd-update/dhcp/dhcpd_update
:
group {
option tftp-server-name = concat(config-option VOIP.http-server-uri, "/Digium");
class "DigiumD40" {
match if substring(option vendor-class-identifier, 0, 10) = "digium_D40";
log(concat("[", binary-to-ascii(16, 8, ":", hardware), "] ", "BOOT Digium D40"));
}
class "DigiumD50" {
match if substring(option vendor-class-identifier, 0, 10) = "digium_D50";
log(concat("[", binary-to-ascii(16, 8, ":", hardware), "] ", "BOOT Digium D50"));
}
class "DigiumD70" {
match if substring(option vendor-class-identifier, 0, 10) = "digium_D70";
log(concat("[", binary-to-ascii(16, 8, ":", hardware), "] ", "BOOT Digium D70"));
}
}
In xivo-provd-plugins/provisioning/dhcpd-update/dhcp/dhcpd_subnet.conf.middle
:
# Digium
allow members of "DigiumD40";
allow members of "DigiumD50";
allow members of "DigiumD70";
You can check the logs in /var/log/syslog
:
dhcpd: [1:0:f:d3:5:48:48] [VENDOR-CLASS-IDENTIFIER: digium_D40_1_1_0_0_48178]
dhcpd: [1:0:f:d3:5:48:48] POOL VoIP
dhcpd: [1:0:f:d3:5:48:48] BOOT Digium D40
dhcpd: DHCPDISCOVER from 00:0f:d3:05:48:48 via eth0
dhcpd: DHCPOFFER on 10.42.1.100 to 00:0f:d3:05:48:48 via eth0
dhcpd: [1:0:f:d3:5:48:48] [VENDOR-CLASS-IDENTIFIER: digium_D40_1_1_0_0_48178]
dhcpd: [1:0:f:d3:5:48:48] POOL VoIP
dhcpd: [1:0:f:d3:5:48:48] BOOT Digium D40
dhcpd: DHCPREQUEST for 10.42.1.100 (10.42.1.1) from 00:0f:d3:05:48:48 via eth0
dhcpd: DHCPACK on 10.42.1.100 to 00:0f:d3:05:48:48 via eth0
Update the DHCP configuration¶
To upload the new DHCP configuration on provd.wazo.community
, in xivo-provd-plugins/dhcpd-update
:
make upload
To download the DHCP configuration on the Wazo server, run:
dhcpcd-update -d
Plugin creation¶
In xivo-provd-plugins/plugins
, create the directory tree:
xivo-digium/
build.py
1.1.0.0/
plugin-info
entry.py
pkgs/
pkgs.db
common/
common.py
var/
tftpboot/
Digium/
In build.py
:
# -*- coding: UTF-8 -*-
from subprocess import check_call
@target('1.1.0.0', 'xivo-digium-1.1.0.0')
def build_1_1_0_0(path):
check_call(['rsync', '-rlp', '--exclude', '.*',
'common/', path])
check_call(['rsync', '-rlp', '--exclude', '.*',
'1.1.0.0/', path])
In 1.1.0.0/plugin-info
:
{
"version": "0.3",
"description": "Plugin for Digium D40, D50 and D70 in version 1.1.0.0.",
"description_fr": "Greffon pour Digium D40, D50 et D70 en version 1.1.0.0.",
"capabilities": {
"Digium, D40, 1.1.0.0": {
"sip.lines": 2
},
"Digium, D50, 1.1.0.0": {
"sip.lines": 4
},
"Digium, D70, 1.1.0.0": {
"sip.lines": 6
}
}
}
In 1.1.0.0/entry.py
:
# -*- coding: UTF-8 -*-
common = {}
execfile_('common.py', common)
VERSION = u'1.1.0.0.48178'
class DigiumPlugin(common['BaseDigiumPlugin']):
IS_PLUGIN = True
pg_associator = common['DigiumPgAssociator'](VERSION)
In 1.1.0.0/pkgs/pkgs.db
, put the informations needed to download the firmwares:
[pkg_firmware]
description: Firmware for all Digium phones
description_fr: Micrologiciel pour tous les téléphones Digium
version: 1.1.0.0
files: firmware
install: digium-fw
[install_digium-fw]
a-b: untar $FILE1
b-c: cp */*.eff firmware/
[file_firmware]
url: http://downloads.digium.com/pub/telephony/res_digium_phone/firmware/firmware_1_1_0_0_package.tar.gz
size: 100111361
sha1sum: 1d44148b996eaf270fd35995f3c5d69ff0438c5b
In common/common.py
, put the code needed to extract informations about the phone:
class DigiumDHCPDeviceInfoExtractor(object):
_VDI_REGEX = re.compile(r'^digium_(D\d\d)_([\d_]+)$')
def extract(self, request, request_type):
return defer.succeed(self._do_extract(request))
def _do_extract(self, request):
options = request['options']
if 60 in options:
return self._extract_from_vdi(options[60])
def _extract_from_vdi(self, vdi):
# Vendor Class Identifier:
# digium_D40_1_0_5_46476
# digium_D40_1_1_0_0_48178
# digium_D70_1_0_5_46476
# digium_D70_1_1_0_0_48178
match = self._VDI_REGEX.match(vdi)
if match:
model = match.group(1).decode('ascii')
fw_version = match.group(2).replace('_', '.').decode('ascii')
dev_info = {u'vendor': u'Digium',
u'model': model,
u'version': fw_version}
return dev_info
class DigiumHTTPDeviceInfoExtractor(object):
_PATH_REGEX = re.compile(r'^/Digium/(?:([a-fA-F\d]{12})\.cfg)?')
def extract(self, request, request_type):
return defer.succeed(self._do_extract(request))
def _do_extract(self, request):
match = self._PATH_REGEX.match(request.path)
if match:
dev_info = {u'vendor': u'Digium'}
raw_mac = match.group(1)
if raw_mac and raw_mac != '000000000000':
mac = norm_mac(raw_mac.decode('ascii'))
dev_info[u'mac'] = mac
return dev_info
You should see in the logs (/var/log/xivo-provd.log
):
provd[1090]: Processing HTTP request: /Digium/000fd3054848.cfg
provd[1090]: <11> Extracted device info: {u'ip': u'10.42.1.100', u'mac': u'00:0f:d3:05:48:48', u'vendor': u'Digium'}
provd[1090]: <11> Retrieved device id: 254374beec8d40209ff70393326b0b13
provd[1090]: <11> Routing request to plugin xivo-digium-1.1.0.0
Still in common/common.py
, put the code needed to associate the phone with the plugin:
class DigiumPgAssociator(BasePgAssociator):
_MODELS = [u'D40', u'D50', u'D70']
def __init__(self, version):
BasePgAssociator.__init__(self)
self._version = version
def _do_associate(self, vendor, model, version):
if vendor == u'Digium':
if model in self._MODELS:
if version == self._version:
return FULL_SUPPORT
return COMPLETE_SUPPORT
return PROBABLE_SUPPORT
return IMPROBABLE_SUPPORT
Then, the last piece: the generation of the phone configuration:
class BaseDigiumPlugin(StandardPlugin):
_ENCODING = 'UTF-8'
_CONTACT_TEMPLATE = 'contact.tpl'
def __init__(self, app, plugin_dir, gen_cfg, spec_cfg):
StandardPlugin.__init__(self, app, plugin_dir, gen_cfg, spec_cfg)
self._tpl_helper = TemplatePluginHelper(plugin_dir)
self._digium_dir = os.path.join(self._tftpboot_dir, 'Digium')
downloaders = FetchfwPluginHelper.new_downloaders(gen_cfg.get('proxies'))
fetchfw_helper = FetchfwPluginHelper(plugin_dir, downloaders)
self.services = fetchfw_helper.services()
self.http_service = HTTPNoListingFileService(self._tftpboot_dir)
dhcp_dev_info_extractor = DigiumDHCPDeviceInfoExtractor()
http_dev_info_extractor = DigiumHTTPDeviceInfoExtractor()
def configure(self, device, raw_config):
self._check_device(device)
filename = self._dev_specific_filename(device)
contact_filename = self._dev_contact_filename(device)
tpl = self._tpl_helper.get_dev_template(filename, device)
contact_tpl = self._tpl_helper.get_template(self._CONTACT_TEMPLATE)
raw_config['XX_mac'] = self._format_mac(device)
raw_config['XX_main_proxy_ip'] = self._get_main_proxy_ip(raw_config)
raw_config['XX_funckeys'] = self._transform_funckeys(raw_config)
raw_config['XX_lang'] = raw_config.get(u'locale')
path = os.path.join(self._digium_dir, filename)
contact_path = os.path.join(self._digium_dir, contact_filename)
self._tpl_helper.dump(tpl, raw_config, path, self._ENCODING)
self._tpl_helper.dump(contact_tpl, raw_config, contact_path, self._ENCODING)
def deconfigure(self, device):
filenames = [
self._dev_specific_filename(device),
self._dev_contact_filename(device)
]
for filename in filenames:
path = os.path.join(self._digium_dir, filename)
try:
os.remove(path)
except OSError as e:
logger.info('error while removing file %s: %s', path, e)
if hasattr(synchronize, 'standard_sip_synchronize'):
def synchronize(self, device, raw_config):
return synchronize.standard_sip_synchronize(device)
else:
# backward compatibility with older xivo-provd server
def synchronize(self, device, raw_config):
try:
ip = device[u'ip'].encode('ascii')
except KeyError:
return defer.fail(Exception('IP address needed for device synchronization'))
else:
sync_service = synchronize.get_sync_service()
if sync_service is None or sync_service.TYPE != 'AsteriskAMI':
return defer.fail(Exception('Incompatible sync service: %s' % sync_service))
else:
return threads.deferToThread(sync_service.sip_notify, ip, 'check-sync')
def get_remote_state_trigger_filename(self, device):
if u'mac' not in device:
return None
return self._dev_specific_filename(device)
def is_sensitive_filename(self, filename):
return bool(self._SENSITIVE_FILENAME_REGEX.match(filename))
def _check_device(self, device):
if u'mac' not in device:
raise Exception('MAC address needed to configure device')
def _get_main_proxy_ip(self, raw_config):
if raw_config[u'sip_lines']:
line_no = min(int(x) for x in raw_config[u'sip_lines'].keys())
line_no = str(line_no)
return raw_config[u'sip_lines'][line_no][u'proxy_ip']
else:
return raw_config[u'ip']
def _format_mac(self, device):
return format_mac(device[u'mac'], separator='', uppercase=False)
_SENSITIVE_FILENAME_REGEX = re.compile(r'^[0-9a-f]{12}\.cfg$')
def _dev_specific_filename(self, device):
filename = '%s.cfg' % self._format_mac(device)
return filename
def _dev_contact_filename(self, device):
contact_filename = '%s-contacts.xml' % self._format_mac(device)
return contact_filename
def _transform_funckeys(self, raw_config):
return dict(
(int(k), v) for k, v in raw_config['funckeys'].iteritems()
)
Then you can create the configuration templates with Jinja syntax. Here are some examples:
Upload the plugin on provd.wazo.community
¶
First, change the source of your plugins in Alternative plugins repository)
(cf.For a development version:
cd xivo-skaro/provisioning/plugins
make upload
For a stable version:
cd xivo-skaro/provisioning/plugins
make download-stable
cd _build
cp dev/xivo-digium-1.1.0.0-0.3.tar.bz2 stable/
cd ..
make upload-stable
More details about this in Managing Plugins.