Automatically Provisioning Polycom Phones

This is an old post!

This post is over 2 years old. Solutions referenced in this article may no longer be valid. Please consider this when utilizing any information referenced here.

The goal of this project were twofold:

  1. To completely eliminate the need for me to touch the phone to provision it. I want to be able to create a profile for it in the database, then simply plug the phone in and let it do the rest. And…

  2. To eliminate per-phone physical configuration files stored on the server. The configuration files should be generated on the fly when the phone requests them.

So the flow of what happens is this:

  1. I create a profile for the phone in the database, then plug the phone in.

  2. Phone boots initially, receives server from DHCP option 66. Script on the server hands out the correct provisioning path for that model of phone. Reboots with new provisioning information.

  3. Phone boots with new provisioning information, begins downloading update SIP application and BootROM. Reboots.

  4. Phone boots again, connects to Asterisk.

At this rate, provisioning a phone for a new employee is simply me entering the new extension and MAC address into an admin screen, and giving them the phone. It’s pretty neat.

**Note: **there are some areas where this is intentionally vague, as I’ve tried to avoid revealing too much about our private corporate administrative structure. If something here doesn’t make sense or you’re curious, post a comment. I’ll answer as best I can.

Creating the initial configs

I used the standard download of firmware and configs from Polycom to seed a base directory. This directory, on my server, is /www/asterisk/prov/polycom_ipXXX, where XXX in the phone model. Right now we deploy the IP-330, IP-331 and IP-4000. While right now the IP-330 and IP-331 can use the same firmware and configs, since the IP-330 has been discontinued they will probably diverge sometime in the not too near future.

With the base configs in place, this is where mod_rewrite comes into play. I added the following rewrite rules to the Apache configs:

RewriteEngine on
RewriteRule ^/000000000000\.cfg /index.php
RewriteRule /prov/[^/]+/([^/]+)-phone\.cfg /provision.php?mac=$1 [L]
RewriteRule /prov/polycom_[^/]+/[^/]+-directory\.xml /prov/polycom_directory.php`

RewriteCond %{THE_REQUEST} ^PUT*
RewriteRule /prov/[^/]+/([^/]+)\.log /prov/polycom_log.php?file=$1`

To understand what these do, you will need to take apart the anatomy of a Polycom boot request. It requests the following files in this order: whichever bootrom.ld image it’s using, [mac-address].cfg if it exists or 000000000000.cfg otherwise, the sip.ld image, [mac-address]-phone.cfg, [mac-address]-web.cfg, and [mac-address]-directory.xml. So, we’re going to rewrite some of these requests to our scripts instead.

Generating configs on the fly

We’re going to skip the first rewrite rule (we’ll talk about that one in a little bit since it has to do with plug-in auto provisioning). The one we’re concerned with is the next one, which rewrites [mac-address]-phone.cfg requests to our provisioning script. So each request to that file is actually rewritten to provision.php?mac=[mac-address].

Now, in the database, we’re keeping track of what kind of phone it is (an IP-330, IP-331 or IP-4000), so when a request hits the script, we look up in the database what kind of phone we’re dealing with based on the MAC address, and use the variables from the database to fill in a template file containing exactly what that phone needs to configure itself.

For example, the base template file for the IP-330 looks something like this:

<sip>
<userinfo>
<server
<?php foreach($phone as $key => $p) { ?>
voIpProt.server.<?php echo $key+1 ?>.address="<?php echo $p["host"] ?>"
voIpProt.server.<?php echo $key+1 ?>.expires="3600"
voIpProt.server.<?php echo $key+1 ?>.transport="UDPOnly"
<?php } ?>
/>

<reg
<?php foreach($phone as $key => $p) { ?>
reg.<?php echo $key+1 ?>.displayName="<?php echo $p["first_name"] ?> <?php echo $p["last_name"] ?>"
reg.<?php echo $key+1 ?>.address="<?php echo $p["name"] ?>"
reg.<?php echo $key+1 ?>.type="private"
reg.<?php echo $key+1 ?>.auth.password="<?php echo $p["secret"] ?>"
reg.<?php echo $key+1 ?>.auth.userId="<?php echo $p["name"] ?>"
reg.<?php echo $key+1 ?>.label="<?php echo $p["first_name"] ?> <?php echo $p["last_name"] ?>"
reg.<?php echo $key+1 ?>.server.1.register="1"
reg.<?php echo $key+1 ?>.server.1.address="<?php echo $p["host"] ?>"
reg.<?php echo $key+1 ?>.server.1.port="5060"
reg.<?php echo $key+1 ?>.server.1.expires="3600"
reg.<?php echo $key+1 ?>.server.1.transport="UDPOnly"
<?php } ?>
/>
</userinfo>
<tcpIpApp>
<sntp
tcpIpApp.sntp.address="pool.ntp.org"
tcpIpApp.sntp.gmtOffset="<?php echo $tz ?>" />
</tcpIpApp>
</sip>

The script outputs this when the phone requests it. Voila. Magic configuration from the database.

There’s a little bit more to it than this. A lot of the settings custom to the company and shared among the various phones are in a master dealnews.cfg file, and included with each phone (it was added to the 000000000000.cfg file).

Now, on to the next rule.

Generating the company directory

Polycom phones support directories. There’s a way to get this to work with LDAP, but I haven’t tackled that yet. So, for now, we generate those dynamically as well when the phone requests any of its *-directory.xml files.

This one’s pretty easy since 1) we don’t allow the endpoints to customize their directories (yet), and 2) because every phone has the same directory. So all of those requests go to a script that outputs the XML structure for the directory:

<directory>
<item_list>
<?php
if(!empty($extensions)) {
    foreach($extensions as $key => $ext) { ?>
        <item>
            <fn><?php echo $ext["first_name"]?></fn>
            <ln><?php echo $ext["last_name"]?></ln>
            <ct><?php echo $ext["mailbox"]?></ct>
        </item>
    <?php } ?>
<? } ?>
</item_list>
</directory>

We do this for both the 000000000000-directory.xml and the [mac-address]-directory.xml file because one is requested at initial boot (the 000000000000-directory.xml file is intended to be a “seed” directory), whereas subsequent requests are for the MAC address specific file.

Getting the log files

Polycoms log, and occasionally the logs are useful for debug purposes. The phones, by default, will try to upload these logs (using PUT requests if you’re provisioning via HTTP like we are). But having the phone fill up a directory full of logs is ungainly. Wouldn’t it be better to parse that into the database, where it can be easily queried? And because the log files have standardized names ([mac-address]-boot/app/flash.log), we know what phone they came from.Well, that’s what the last two rewrite lines do. We rewrite those PUT requests to a PHP script and parse the data off stdin, adding it to the database.

A little warning about this. Even at low settings Polycom phones are chatty with their logs. You may want to have some kind of cleaning script to remove log entries over X days old.

Passing the initial config via DHCP

At this point, we have a working magic configuration. Phones, once configured, fetch dynamically-generated configuration files that are guaranteed to be as up-to-date as possible. Their directories are generated out of the same database, and log files are added back to the same database. It all works well!

… except that it still requires me to touch the phone. I’m still required to punch into the keypad the provisioning directory to get it going. That sucks. But there’s a way around that too!

By default, Polycom phones out of the box look for a provisioning server on DHCP option 66. If they don’t find this, they will proceed to boot the default profile thats ships with the phone. It’s worth noting that, if you don’t pass it in the form of a fully-qualified URL, it will default to TFTP. But you can pass any format you can add to the phone.

if substring(hardware, 1, 3) = 00:04:f2 {
    option tftp-server-name "http://server.com";
}

In this case, what we’ve done is look for a MAC address in Polycom’s space (00:04:f2) and pass it option 66 with our boot server. But, we’re passing the same thing no matter what kind of phone it is! How can we tell them apart, especially since, at this point, we don’t know the MAC address.

The first rewrite rule handles part of this for us. When the phone receives the server from option 66 and requests 000000000000.cfg from the root directory, we instead forward it on to our index.php file, which handles the initial configuration. Our script looks at the HTTP_USER_AGENT, which tells us what kind of phone we’re dealing with (they’ll contain strings such as “SPIP_330”, “SPIP_331” or “SSIP_4000”). Using that, we selectively give it an initial configuration that tells it the RIGHT place to look.

<?php
ob_start();

if(stristr($_SERVER['HTTP_USER_AGENT'], "SPIP_330")) {
include "devices/polycom_ip330_initial.php";
}

if(stristr($_SERVER['HTTP_USER_AGENT'], "SPIP_331")) {
include "devices/polycom_ip331_initial.php";
}

if(stristr($_SERVER['HTTP_USER_AGENT'], "SSIP_4000")) {
include "devices/polycom_ip4000_initial.php";
}

$contents = ob_get_contents();
ob_end_clean();

echo $contents;
?>

These files all contain a variation of my previous auto-provisioning configuration config, which tells it the proper directory to look in for phone-specific configuration.

Now, all you do is plug the phone in, and everything else just happens. A phone admin’s dream.

Keeping things up to date

By default, the phones won’t check to see if there’s new config or updated firmware until you tell them to. But his also means that some things, especially directory changes, won’t get picked up with any regularity. A quick change to the configs makes it possible to schedule the phones to look for changes at a certain time:

<provisioning prov.polling.enabled="1" prov.polling.mode="abs" prov.polling.period="86400" prov.polling.time="01:00" />

This causes the phones to look for new configs at 1AM each morning and do whatever they have to with them.

Conclusions

The reason all this is possible is because Polycom’s files are 1) easily manipulatable XML, as opposed to the binary configurations used by other manufacturers, and 2) distributed, so that you only need to actually send what you need set, and the phone can get the rest from the defaults. In practice this all works very well, and cut the time it used to take me to configure a phone from 5-10 minutes to about 30 seconds. Basically, as long as it takes me to get the phone off the shelf and punch the MAC address into the admin GUI I wrote.

I don’t even need to take it out of the box!

Comments (0)

Interested in why you can't leave comments on my blog? Read the article about why comments are uniquely terrible and need to die. If you are still interested in commenting on this article, feel free to reach out to me directly and/or share it on social media.

Contact Me
Share It
Asterisk
At dealnews, as I’ve written before, we run Asterisk as our telephone system. I find it to be a pretty good solution to our telecom needs: we have multiple offices and several home-based users. And, for the most part, for hard telephones, we use Polycoms. We run mostly IP-330s, with a couple of IP-4000s and a few new IP-331s. We also have softphones, a couple of PAP2s and a couple of old Grandstreams from our original Asterisk deployment in 2007 that I’m desperately trying to get out of circulation. But it’s mostly Polycoms. Recently, I changed how we were doing provisioning. I’ll write a more in-depth post about this later, but the short of it is that since Polycom phones use XML for their configuration information, we now generate them dynamically instead of creating a configuration file. It’s what I should have done back in 2007 when we bought our first round of Polycoms. But this presented me with a problem: how do I re-provision the older phones - some of which I don’t have easy physical access to (at least that doesn’t involve an airplane ride) - to use the new configuration system? In doing some research, I discovered that Polycom allows you to set, via certain commands, the provisioning server from within a config. With this information, I crafted a custom re-provisioning config that looks like this: <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <deviceSettings> <device device.set="1" device.dhcp.bootSrvUseOpt.set="1" device.dhcp.bootSrvUseOpt="2" device.net.cdpEnabled.set="1" device.net.cdpEnabled="0" device.prov.serverType.set="1" device.prov.serverType="2" device.prov.serverName.set="1" device.prov.serverName="server"/> </deviceSettings> And included it at the top of the 000000000000.cfg file (one of the default files downloaded by each Polycom phone): <?xml version="1.0" encoding="UTF-8" standalone="yes"?> <APPLICATION APP_FILE_PATH="sip.ld" CONFIG_FILES="update.cfg, phone1.cfg, sip.cfg" MISC_FILES="" LOG_FILE_DIRECTORY="" OVERRIDES_DIRECTORY="" CONTACTS_DIRECTORY=""/> Then, using Asterisk, I issue the check-config command: asterisk*CLI> sip notify polycom-check-cfg peer The phone should reboot, pick up its new config, then reboot again with with proper new provisioning information from the new provisioning provider. Next post, I’ll show you how to use PHP and mod_rewrite to eliminate the need for per-phone config files.
Read More
Apache
I’ve been using Google Chrome as my primary browser for the last few months. Sorry, Firefox, but with all the stuff I need to work installed, you’re so slow as to be unusable. Up to and including having to force-quit at the end of the day. Chrome starts and stops quickly But that’s not the purpose of this entry. The purpose is how to live with self-signed SSL certificates and Google Chrome. Let’s say you have a server with a self-signed HTTP SSL certificate. Every time you hit a page, you get a nasty error message. You ignore it once and it’s fine for that browsing session. But when you restart, it’s back. Unlike Firefox, there’s no easy way to say “yes, I know what I’m doing, ignore this.” This is an oversight I wish Chromium would correct, but until they do, we have to hack our way around it. Caveat: these instructions are written for Mac OS X. PC instructions will be slightly different at PCs don’t have a keychain, and Google Chrome (unlike Firefox) uses the system keychain. So here’s how to get Google Chrome to play nicely with your self-signed SSL certificate: On your web server, copy the crt file (in my case, server.crt) over to your Macintosh. I scp'd it to my Desktop for ease of work. ** These directions has been updated. Thanks to Josh below for pointing out a slightly easier way.** In the address bar, click the little lock with the X. This will bring up a small information screen. Click the button that says “Certificate Information.” Click and drag the image to your desktop. It looks like a little certificate. Double-click it. This will bring up the Keychain Access utility. Enter your password to unlock it. Be sure you add the certificate to the System keychain, not the login keychain. Click “Always Trust,” even though this doesn’t seem to do anything. After it has been added, double-click it. You may have to authenticate again. Expand the “Trust” section. “When using this certificate,” set to “Always Trust” That’s it! Close Keychain Access and restart Chrome, and your self-signed certificate should be recognized now by the browser. This is one thing I hope Google/Chromium fixes soon as it should not be this difficult. Self-signed SSL certificates are used **a lot **in the business world, and there should be an easier way for someone who knows what they are doing to be able to ignore this error than copying certificates around and manually adding them to the system keychain.
Read More
Asterisk
Hello there! I figure that if I’m going to start using this blog to post the wanderings and wonderings of a mid-level engineer at a dot-com company (I work at dealnews to be specific, and I guess I should include the standard disclosure that my employer does not endose or support anything that I say/do here), perhaps I should give some substance to my first post. So, I figure I would write a post on something I have plenty of experience with: PHP. But what to write about? Surely, there must be ten million PHP tutorials on the ‘net and I don’t need to add to the noise already out there as to what are/aren’t the best practices using PHP, so I thought about using PHP in some lesser known areas. And here is one lesser known, but very cool area: you can use PHP to route phone calls! At a previous employer, I worked with Asterisk as a software development consultant. My primary role was to build web interfaces to Asterisk (and other telecom hardware) backends, though while working as a consultant I learned quite a bit about extending Asterisk to do crazy cool things. “It’s Just Software!” Asterisk is an open-source software PBX that was created by Mark Spencer (an Auburn grad and now CEO at digium). It is quickly becoming a challenger in the PBX market (fact: we use it at dealnews), and an entire industry has sprung up around Asterisk and open-source IP telephony. For the purposes of this tutorial, I’m going to assume that you already have Asterisk installed and configured to your liking, and are now wishing to extend it beyond what it is capable of doing with the builtin dialplan applications. If this is not a good assumption in your case, may I highly suggest the Asterisk Tutorial at voip-info.org, or even better, the O’Reilly Asterisk book, which is a little dated but still quite relevant to most beginner-level stuff. Meet AGI, CGI’s hard-working cousin: AGI, or the Asterisk Gateway Interface, is the key to extending Asterisk beyond what it is capable of doing on its own. AGI gives Asterisk the ability to run and interact with scripts and programs outside of Asterisk. AGIs can be written in any language that can be executed on a Linux system (and there have been AGIs written in PHP, Python, Perl, C, Bash and just about every other language out there). Since PHP is my language of choice, that is what I’m going to concentrate on in this tutorial. Asterisk AGIs are actually incredibly simple creatures. When run from within the Asterisk dialplan, they simply send commands to Asterisk using standard output and read the results on standard input. Its what happens between those that is really, really cool. Enough Talk! Code or GTFO! So, let’s get started! First, you need to set up your script environment. I recommend doing this in an include-able file so that you can reuse it in future AGIs. There are a few commands you need to know about: <?php // This turns on implicit flushing, meaning PHP will flush the buffer after // every output call. This is necessary to make sure that AGI scripts get their // instructions to Asterisk as soon as possible, rather than buffering until // script termination. ob_implicit_flush(true); // This sets the maximum execution time for the AGI script. I usually like to // keep this set low (6 seconds), because the script should complete pretty // quickly and the last thing we want it to do is hang a call because the script // is churning. set_time_limit(6); //This sets a custom error handler function. We'll get back to this later. set_error_handler("error"); //This creates a standard in that can be used by our script. $in = fopen("php://stdin","r"); //This creates an access to standard error, for debugging. $stdlog = fopen("php://stderr", "w"); ?> Okay, that’s not too bad! Now, we’re going to do a little more advanced stuff. Every time an AGI script executes, Asterisk passes a number (about 20) values to the script. These AGI headers take the form of “key: value”, one per line separated with a line feed (\n), concluding with a blank line. Before we can do this, we need to write a few functions to read from AGI input, write to Asterisk, Execute commands, and write to the Asterisk CLI. These are the functions I use: <?php function read() { global $in, $debug, $stdlog; $input = str_replace("\n", "", fgets($in, 4096)); if ($debug){ fputs($stdlog, "read: $input\n"); } return $input; } ?> So what are we doing here? Well, the first line, we strip out the line feed in each chunk we get from stdin. Then, we check to see if $debug is set and, if so, echo what we read to standard error. Finally, we return the line we just read. Pretty simple, right? Well, this little funtion will save you lots of time. Next, we need a way to write data: <?php function write($line) { global $debug, $stdlog; if ($debug) { fputs($stdlog, "write: $line\n"); } echo $line."\n"; } ?> This function is even more simple: it just writes out to standard error if $debug is on, and outputs whatever was sent to it with an additional new line. This next function, however, is more complex. <?php function execute($command) { global $in, $out, $debug, $stdlog; write($command); $data = fgets($in, 4096); if (preg_match("/^([0-9]{1,3}) (.*)/", $data, $matches)) { if (preg_match('/^result=([0-9a-zA-Z]*)( ?\((.*)\))?$/', $matches[2], $match)) { $arr['code'] = $matches[1]; $arr['result'] = $match[1]; if (isset($match[3]) && $match[3]) { $arr['data'] = $match[3]; } if($debug) { fputs($stdlog, "CODE: " . $arr['code'] . " \n"); fputs($stdlog, "result: " . $arr['result'] . " \n"); fputs($stdlog, "result: " . $arr['data'] . " \n"); fflush($stdlog); } return $arr; } else return 0; } else return -1; } ?> Woah, complex! Well, not really. execute() is the swiss army knife of AGI programming: it allows you to do interactive stuff inside this AGI script. First, as you can see, it calls the write() function we just wrote, writing an AGI command to Asterisk. Then it looks for a response on standard in. A response from Asterisk takes the form of “result=<result> <data>”. So, we use preg_match to get this out for us and put it into something usable. We do the debug output again, then return the array or 0 or -1 in the event of failures. Just two more functions to go: <?php function verbose($str,$level=0) { $str=addslashes($str); execute("VERBOSE \"$str\" $level"); } function error($errno,$errst,$errfile,$errline) { verbose("AGI ERROR: $errfile, on line $errline: $errst"); } ?> As you can see, these two functions are very simple. One gives verbose output to the Asterisk CLI, and the other is the error function we declared using set_error_handler above. Back to reading in variables. Now that we have the ability to read in, let’s read in the default variables that are passed to the script by Asterisk. We do this using the following code chunk: <?php while ($env=read()) { $s = split(": ",$env); $key = str_replace("agi_","",$s[0]); $value = trim($s[1]); $_AGI[$key] = $value; if($debug) { verbose("Registered AGI variable $key as $value."); } if (($env == "") || ($env == "\n")) { break; } } ?> This creates an $_AGI associative array (in the spirit of $_POST, $_GET, etc) for you to use containing all the items Asterisk passed in. For each read() line, in the first line we split it to get the key and value (this could probably have been done better with a regular expression, but I got a copy of some AGI code from a friend and modified it many moons ago before I began using regular expressions). Then, we strip out the “agi_” that Asterisk adds to the key because it is superfluous, and trim out the spaces and other garbage from the value, adding them to an array. Putting It All Together: Congratuations! You now have all the tools necessary to write an AGI! I suggest (as above) putting those in an include so you can reuse as necessary. So what next? Now, you write an AGI script! Let’s start with a simple example: #!/usr/bin/php <?php include "agi.php"; execute("SAY DATETIME #"); ?> That simple! Of course, all this AGI does is read the date and time to the caller, then exit, but it just shows that AGIs can do really powerful things, really simply. “Calling” you AGI: So now you have this AGI written and you want to use it, but you don’t know how. Well this is pretty easy too! AGIs should be placed in whatever directory you define for “astagidir” in your asterisk.conf file. Unless you changed it, this will be /var/lib/asterisk/agi-bin. Next, be sure that the file is executed by setting the executable bit “chmod +x ". You may also have to fiddle with the permissions: the asterisk user or group need the ability to read and execute the script. Then, you just call it from your dialplan, like so: exten => 1000,1,AGI(<filename>)` Now, after you “extensions reload” of course, you should be able to dial 1000, and watch your AGI spring into action! A more complex example: This is an AGI I wrote at dealnews when someone in the office requested the ability to custom set names to caller IDs and have it work on all phones. Keep in mind that this is only half of the solution (the other half is a web interface). #!/usr/bin/php <?php include "agi.php" ; $db=mysql_connect('redacted', 'redacted', 'redacted'); if(!$db) {     verbose("Could not connect to DB!"); } if(!mysql_select_db('redacted', $db)) {     verbose("Could not use DB!"); } $res=mysql_query(sprintf("select substitution_name from cid_substitution where from_number='%s'",$_AGI['callerid'])); $result=mysql_fetch_row($res); if(sizeof($result)) {     execute(sprintf("SET CALLERID \"%s <%s>\"",$result[0],$_AGI['callerid'])); } mysql_close($db); ?> This demonstrates one of the main advantages to using AGIs, and PHP in particular: the ability to easily interact with databases. In this program, I’m using the caller ID supplied by the carrier to fetch a corresponding name from a database and send it back along with the call. Routing calls is accomplished by calling the EXEC function with DIAL, giving you the ability, with a little work, to route calls based on the database. Pretty neat for a language thought of only as web coding. Indeed, there is a large list of commands that AGIs can use, and variables passed into them, available here. Help! It doesn’t work! Relax! Problems happen from time to time. One of the most common faults is forgetting to set the +x bit on the file to make it executable. Permissions problems are also relatively common. For More Information: voip-info.org - a.k.a. “the wiki,” is the major information repository for Asterisk knowledge specifically, and IP telephony in general.
Read More