Table of Contents

Loading ...

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.

If instead you want to add a model to an existing provisioning plugin, see the corresponding guide instead.

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 wazo-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 wazo-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 wazo-provd-plugins/dhcpd-update:

make upload

To download the DHCP configuration on the Wazo server, run:

dhcpcd-update -d

Plugin creation

In wazo-provd-plugins/plugins, create the directory tree:

wazo_digium/
    build.py
    v1_4_0_0/
        plugin-info
        entry.py
        pkgs/
            pkgs.db
        templates
            D40.tpl
            D50.tpl
            D70.tpl
            firmware.tpl
    common/
        common.py
        var/
            tftpboot/
                Digium/

In build.py:

# -*- coding: UTF-8 -*-

from subprocess import check_call

@target('1.4.0.0', 'wazo-digium-1.4.0.0')
def build_1_4_0_0(path):
    check_call(['rsync', '-rlp', '--exclude', '.*', 'common/', path])
    check_call(['rsync', '-rlp', '--exclude', '_*',  'v1_4_0_0/', path])

In v1_4_0_0/plugin-info:

{
  "version": "1.1.0",
  "description": "Plugin for Digium D40, D50 and D70 in version 1.4.0.0.",
  "description_fr": "Greffon pour Digium D40, D50 et D70 en version 1.4.0.0.",
  "capabilities": {
    "Digium, D40, 1.4.0.0": {
      "lines": 2,
      "high_availability": true,
      "function_keys": 0,
      "expansion_modules": 0,
      "switchboard": false,
      "type": "deskphone",
      "protocol": "sip"
    },
    "Digium, D50, 1.4.0.0": {
      "lines": 4,
      "high_availability": true,
      "function_keys": 0,
      "expansion_modules": 0,
      "switchboard": false,
      "type": "deskphone",
      "protocol": "sip"
    },
    "Digium, D70, 1.4.0.0": {
      "lines": 6,
      "high_availability": true,
      "function_keys": 0,
      "expansion_modules": 0,
      "switchboard": false,
      "type": "deskphone",
      "protocol": "sip"
    }
  }
}

In v1_4_0_0/entry_py:

common = {}
execfile_('common.py', common)
VERSION = '1.4.0.0.57389'
class DigiumPlugin(common['BaseDigiumPlugin']):
    IS_PLUGIN = True
    pg_associator = common['DigiumPgAssociator'](VERSION)

In 1.1.0.0/pkgs/pkgs.db, put the information needed to download the firmware:

[pkg_firmware]
description: Firmware for all Digium phones
description_fr: Micrologiciel pour tous les téléphones Digium
version: 1.4.0.0
files: firmware
install: digium-fw


[install_digium-fw]
a-b: untar $FILE1
b-c: cp */*.eff Digium/firmware/


[file_firmware]
url: https://downloads.digium.com/pub/telephony/res_digium_phone/firmware/firmware_1_4_0_0_package.tar.gz
size: 101303480
sha1sum: 626273aaf6dd33e1927f3acb4a90f86a6e4e9f25

In common/common.py, put the code needed to extract information about the phone:

import re
from provd.util import norm_mac, format_mac
from twisted.internet import defer
from provd.servers.http_site import Request
from provd.devices.ident import RequestType, DHCPRequest

class DigiumDHCPDeviceInfoExtractor:

    _VDI_REGEX = re.compile(r'^digium_(D\d\d)_([\d_]+)$')

    def extract(self, request: DHCPRequest, request_type: RequestType):
        return defer.succeed(self._do_extract(request))

    def _do_extract(self, request: DHCPRequest):
        options = request['options']
        if 60 in options:
            return self._extract_from_vdi(options[60])

    def _extract_from_vdi(self, vdi: str):
        # 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)
            fw_version = match.group(2).replace('_', '.')
            return {'vendor': 'Digium', 'model': model, 'version': fw_version}


class DigiumHTTPDeviceInfoExtractor:

    _PATH_REGEX = re.compile(r'^/Digium/(?:([a-fA-F\d]{12})\.cfg)?')

    def extract(self, request: Request, request_type: RequestType):
        return defer.succeed(self._do_extract(request))

    def _do_extract(self, request: Request):
        match = self._PATH_REGEX.match(request.path.decode('ascii'))
        if match:
            dev_info = {'vendor': 'Digium'}
            raw_mac = match.group(1)
            if raw_mac and raw_mac != '000000000000':
                mac = norm_mac(raw_mac)
                dev_info['mac'] = mac
            return dev_info

You should see in the logs (/var/log/wazo-provd.log):

provd[1090]: Processing HTTP request: /Digium/000fd3054848.cfg
provd[1090]: <11> Extracted device info: {'ip': '10.42.1.100', 'mac': '00:0f:d3:05:48:48', 'vendor': 'Digium'}
provd[1090]: <11> Retrieved device id: 254374beec8d40209ff70393326b0b13
provd[1090]: <11> Routing request to plugin wazo-digium-1.4.0.0

Still in common/common.py, put the code needed to associate the phone with the plugin:

from provd.devices.pgasso import BasePgAssociator, DeviceSupport

class DigiumPgAssociator(BasePgAssociator):

    _MODELS = ['D40', 'D45', 'D50', 'D60', 'D62', 'D65', 'D70']

    def __init__(self, version):
        super().__init__()
        self._version = version

    def _do_associate(
        self, vendor: str, model: Optional[str], version: Optional[str]
    ) -> DeviceSupport:
        if vendor == 'Digium':
            if model in self._MODELS:
                if version == self._version:
                    return DeviceSupport.EXACT
                return DeviceSupport.COMPLETE
            return DeviceSupport.PROBABLE
        return DeviceSupport.IMPROBABLE

Then, the last piece: the generation of the phone configuration:

import os
import re
import logging

from provd import synchronize
from provd.util import norm_mac, format_mac
from provd.plugins import StandardPlugin, FetchfwPluginHelper, TemplatePluginHelper
from provd.servers.http import HTTPNoListingFileService


logger = logging.getLogger('plugin.wazo_digium')


class BaseDigiumPlugin(StandardPlugin):

    _ENCODING = 'UTF-8'
    _CONTACT_TEMPLATE = 'contact.tpl'
    _SENSITIVE_FILENAME_REGEX = re.compile(r'^[0-9a-f]{12}\.cfg$')

    def __init__(self, app, plugin_dir, gen_cfg, spec_cfg):
        super().__init__(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('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)

    def synchronize(self, device, raw_config):
        return synchronize.standard_sip_synchronize(device)

    def get_remote_state_trigger_filename(self, device):
        if '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 'mac' not in device:
            raise Exception('MAC address needed to configure device')

    def _get_main_proxy_ip(self, raw_config):
        if raw_config['sip_lines']:
            line_no = min(int(x) for x in list(raw_config['sip_lines']))
            line_no = str(line_no)
            return raw_config['sip_lines'][line_no]['proxy_ip']
        return raw_config['ip']

    def _format_mac(self, device):
        return format_mac(device['mac'], separator='', uppercase=False)

    def _dev_specific_filename(self, device: Dict[str, str]) -> str:
        return f'{self._format_mac(device)}.cfg'

    def _dev_contact_filename(self, device):
        return f'{self._format_mac(device)}-contacts.xml'

    def _transform_funckeys(self, raw_config):
        return {int(k): v for k, v in raw_config['funckeys'].items()}

Then you can create the configuration templates using Jinja templates. Here are some examples:

Upload the plugin on provd.wazo.community

First, change the source of your plugins (cf. Alternative plugins repository)

For a development version:

cd wazo-provd-plugins/plugins
make upload

For a stable version:

cd wazo-provd-plugins/plugins
make download-stable
cd _build
cp dev/wazo-digium-1.4.0.0.tar.bz2 stable/
cd ..
make upload-stable