Update the conversion script from sip.conf to pjsip.conf
authorMark Michelson <mmichelson@digium.com>
Thu, 31 Oct 2013 22:09:47 +0000 (22:09 +0000)
committerMark Michelson <mmichelson@digium.com>
Thu, 31 Oct 2013 22:09:47 +0000 (22:09 +0000)
(closes issue ASTERISK-22374)
Reported by Matt Jordan

Review: https://reviewboard.asterisk.org/r/2846
........

Merged revisions 402327 from http://svn.asterisk.org/svn/asterisk/branches/12

git-svn-id: https://origsvn.digium.com/svn/asterisk/trunk@402328 65c4cc65-6c06-0410-ace0-fbb531ad65f3

contrib/scripts/sip_to_pjsip/astconfigparser.py [moved from contrib/scripts/sip_to_res_sip/astconfigparser.py with 59% similarity]
contrib/scripts/sip_to_pjsip/astdicts.py [moved from contrib/scripts/sip_to_res_sip/astdicts.py with 100% similarity]
contrib/scripts/sip_to_pjsip/sip_to_pjsip.py [new file with mode: 0755]
contrib/scripts/sip_to_res_sip/sip_to_res_sip.py [deleted file]

@@ -3,11 +3,12 @@ import re
 from astdicts import OrderedDict
 from astdicts import MultiOrderedDict
 
+
 def merge_values(left, right, key):
     """Merges values from right into left."""
     if isinstance(left, list):
         vals0 = left
-    else: # assume dictionary
+    else:  # assume dictionary
         vals0 = left[key] if key in left else []
     vals1 = right[key] if key in right else []
 
@@ -15,14 +16,16 @@ def merge_values(left, right, key):
 
 ###############################################################################
 
+
 class Section(MultiOrderedDict):
-    """A Section is a MultiOrderedDict itself that maintains a list of
-       key/value options.  However, in the case of an Asterisk config
-       file a section may have other defaults sections that is can pull
-       data from (i.e. templates).  So when an option is looked up by key
-       it first checks the base section and if not found looks in the
-       added default sections. If not found at that point then a 'KeyError'
-       exception is raised.
+    """
+    A Section is a MultiOrderedDict itself that maintains a list of
+    key/value options.  However, in the case of an Asterisk config
+    file a section may have other defaults sections that is can pull
+    data from (i.e. templates).  So when an option is looked up by key
+    it first checks the base section and if not found looks in the
+    added default sections. If not found at that point then a 'KeyError'
+    exception is raised.
     """
     count = 0
 
@@ -35,9 +38,24 @@ class Section(MultiOrderedDict):
         self._templates = [] if templates is None else templates
 
     def __cmp__(self, other):
+        """
+        Use self.id as means of determining equality
+        """
         return cmp(self.id, other.id)
 
-    def get(self, key, from_self=True, from_templates=True, from_defaults=True):
+    def get(self, key, from_self=True, from_templates=True,
+            from_defaults=True):
+        """
+        Get the values corresponding to a given key. The parameters to this
+        function form a hierarchy that determines priority of the search.
+        from_self takes priority over from_templates, and from_templates takes
+        priority over from_defaults.
+
+        Parameters:
+        from_self - If True, search within the given section.
+        from_templates - If True, search in this section's templates.
+        from_defaults - If True, search within this section's defaults.
+        """
         if from_self and key in self:
             return MultiOrderedDict.__getitem__(self, key)
 
@@ -62,13 +80,19 @@ class Section(MultiOrderedDict):
         raise KeyError(key)
 
     def __getitem__(self, key):
-        """Get the value for the given key. If it is not found in the 'self'
-           then check inside templates and defaults before declaring raising
-           a KeyError exception.
+        """
+        Get the value for the given key. If it is not found in the 'self'
+        then check inside templates and defaults before declaring raising
+        a KeyError exception.
         """
         return self.get(key)
 
     def keys(self, self_only=False):
+        """
+        Get the keys from this section. If self_only is True, then
+        keys from this section's defaults and templates are not
+        included in the returned value
+        """
         res = MultiOrderedDict.keys(self)
         if self_only:
             return res
@@ -85,13 +109,21 @@ class Section(MultiOrderedDict):
         return res
 
     def add_defaults(self, defaults):
+        """
+        Add a list of defaults to the section. Defaults are
+        sections such as 'general'
+        """
         defaults.sort()
         for i in defaults:
             self._defaults.insert(0, i)
 
     def add_templates(self, templates):
-        templates.sort(reverse=True);
-        self._templates.extend(templates)
+        """
+        Add a list of templates to the section.
+        """
+        templates.sort()
+        for i in templates:
+            self._templates.insert(0, i)
 
     def get_merged(self, key):
         """Return a list of values for a given key merged from default(s)"""
@@ -120,9 +152,11 @@ COMMENT_END = '--;'
 
 DEFAULTSECT = 'general'
 
+
 def remove_comment(line, is_comment):
     """Remove any commented elements from the line."""
-    if not line: return line, is_comment
+    if not line:
+        return line, is_comment
 
     if is_comment:
         part = line.partition(COMMENT_END)
@@ -152,23 +186,21 @@ def remove_comment(line, is_comment):
     # check for eol comment
     return line.partition(COMMENT)[0].strip(), False
 
+
 def try_include(line):
-    """Checks to see if the given line is an include.  If so return the
-       included filename, otherwise None.
     """
-    if not line.startswith('#'):
-        return None
+    Checks to see if the given line is an include.  If so return the
+    included filename, otherwise None.
+    """
+
+    match = re.match('^#include\s*[<"]?(.*)[>"]?$', line)
+    return match.group(1) if match else None
 
-    # it is an include - get file name
-    try:
-        return line[line.index('"') + 1:line.rindex('"')]
-    except ValueError:
-        print "Invalid include - could not parse filename."
-        return None
 
 def try_section(line):
-    """Checks to see if the given line is a section. If so return the section
-       name, otherwise return 'None'.
+    """
+    Checks to see if the given line is a section. If so return the section
+    name, otherwise return 'None'.
     """
     # leading spaces were stripped when checking for comments
     if not line.startswith('['):
@@ -188,6 +220,7 @@ def try_section(line):
     except:
         return section[1:], False, templates
 
+
 def try_option(line):
     """Parses the line as an option, returning the key/value pair."""
     data = re.split('=>?', line)
@@ -196,30 +229,12 @@ def try_option(line):
 
 ###############################################################################
 
-def find_value(sections, key):
-    """Given a list of sections, try to find value(s) for the given key."""
-    # always start looking in the last one added
-    sections.sort(reverse=True);
-    for s in sections:
-        try:
-            # try to find in section and section's templates
-            return s.get(key, from_defaults=False)
-        except KeyError:
-            pass
-
-    # wasn't found in sections or a section's templates so check in defaults
-    for s in sections:
-        try:
-            # try to find in section's defaultsects
-            return s.get(key, from_self=False, from_templates=False)
-        except KeyError:
-            pass
-
-    raise KeyError(key)
 
 def find_dict(mdicts, key, val):
-    """Given a list of mult-dicts, return the multi-dict that contains
-       the given key/value pair."""
+    """
+    Given a list of mult-dicts, return the multi-dict that contains
+    the given key/value pair.
+    """
 
     def found(d):
         return key in d and val in d[key]
@@ -230,44 +245,25 @@ def find_dict(mdicts, key, val):
         raise LookupError("Dictionary not located for key = %s, value = %s"
                           % (key, val))
 
-def get_sections(parser, key, attr='_sections', searched=None):
-    if searched is None:
-        searched = []
-    if parser is None or parser in searched:
-        return []
 
-    try:
-        sections = getattr(parser, attr)
-        res = sections[key] if key in sections else []
-        searched.append(parser)
-        return res + get_sections(parser._includes, key, attr, searched) \
-            + get_sections(parser._parent, key, attr, searched)
-    except:
-        # assume ordereddict of parsers
-        res = []
-        for p in parser.itervalues():
-            res.extend(get_sections(p, key, attr, searched))
-        return res
-
-def get_defaults(parser, key):
-    return get_sections(parser, key, '_defaults')
-
-def write_dicts(file, mdicts):
+def write_dicts(config_file, mdicts):
+    """Write the contents of the mdicts to the specified config file"""
     for section, sect_list in mdicts.iteritems():
         # every section contains a list of dictionaries
         for sect in sect_list:
-            file.write("[%s]\n" % section)
+            config_file.write("[%s]\n" % section)
             for key, val_list in sect.iteritems():
                 # every value is also a list
                 for v in val_list:
                     key_val = key
                     if v is not None:
                         key_val += " = " + str(v)
-                        file.write("%s\n" % (key_val))
-            file.write("\n")
+                        config_file.write("%s\n" % (key_val))
+            config_file.write("\n")
 
 ###############################################################################
 
+
 class MultiOrderedConfigParser:
     def __init__(self, parent=None):
         self._parent = parent
@@ -275,16 +271,39 @@ class MultiOrderedConfigParser:
         self._sections = MultiOrderedDict()
         self._includes = OrderedDict()
 
+    def find_value(self, sections, key):
+        """Given a list of sections, try to find value(s) for the given key."""
+        # always start looking in the last one added
+        sections.sort(reverse=True)
+        for s in sections:
+            try:
+                # try to find in section and section's templates
+                return s.get(key, from_defaults=False)
+            except KeyError:
+                pass
+
+        # wasn't found in sections or a section's templates so check in
+        # defaults
+        for s in sections:
+            try:
+                # try to find in section's defaultsects
+                return s.get(key, from_self=False, from_templates=False)
+            except KeyError:
+                pass
+
+        raise KeyError(key)
+
     def defaults(self):
         return self._defaults
 
     def default(self, key):
         """Retrieves a list of dictionaries for a default section."""
-        return get_defaults(self, key)
+        return self.get_defaults(key)
 
     def add_default(self, key, template_keys=None):
-        """Adds a default section to defaults, returning the
-           default Section object.
+        """
+        Adds a default section to defaults, returning the
+        default Section object.
         """
         if template_keys is None:
             template_keys = []
@@ -295,17 +314,47 @@ class MultiOrderedConfigParser:
 
     def section(self, key):
         """Retrieves a list of dictionaries for a section."""
-        return get_sections(self, key)
+        return self.get_sections(key)
+
+    def get_sections(self, key, attr='_sections', searched=None):
+        """
+        Retrieve a list of sections that have values for the given key.
+        The attr parameter can be used to control what part of the parser
+        to retrieve values from.
+        """
+        if searched is None:
+            searched = []
+        if self in searched:
+            return []
+
+        sections = getattr(self, attr)
+        res = sections[key] if key in sections else []
+        searched.append(self)
+        if self._includes:
+            res += self._includes.get_sections(key, attr, searched)
+        if self._parent:
+            res += self._parent.get_sections(key, attr, searched)
+        return res
+
+    def get_defaults(self, key):
+        """
+        Retrieve a list of defaults that have values for the given key.
+        """
+        return self.get_sections(key, '_defaults')
 
     def add_section(self, key, template_keys=None, mdicts=None):
+        """
+        Create a new section in the configuration. The name of the
+        new section is the 'key' parameter.
+        """
         if template_keys is None:
             template_keys = []
         if mdicts is None:
             mdicts = self._sections
         res = Section()
         for t in template_keys:
-            res.add_templates(get_defaults(self, t))
-        res.add_defaults(get_defaults(self, DEFAULTSECT))
+            res.add_templates(self.get_defaults(t))
+        res.add_defaults(self.get_defaults(DEFAULTSECT))
         mdicts.insert(0, key, res)
         return res
 
@@ -313,29 +362,50 @@ class MultiOrderedConfigParser:
         return self._includes
 
     def add_include(self, filename, parser=None):
+        """
+        Add a new #include file to the configuration.
+        """
         if filename in self._includes:
             return self._includes[filename]
 
         self._includes[filename] = res = \
-             MultiOrderedConfigParser(self) if parser is None else parser
-        return res;
+            MultiOrderedConfigParser(self) if parser is None else parser
+        return res
 
     def get(self, section, key):
         """Retrieves the list of values from a section for a key."""
         try:
             # search for the value in the list of sections
-            return find_value(self.section(section), key)
+            return self.find_value(self.section(section), key)
         except KeyError:
             pass
 
         try:
             # section may be a default section so, search
             # for the value in the list of defaults
-            return find_value(self.default(section), key)
+            return self.find_value(self.default(section), key)
         except KeyError:
             raise LookupError("key %r not found for section %r"
                               % (key, section))
 
+    def multi_get(self, section, key_list):
+        """
+        Retrieves the list of values from a section for a list of keys.
+        This method is intended to be used for equivalent keys. Thus, as soon
+        as any match is found for any key in the key_list, the match is
+        returned. This does not concatenate the lookups of all of the keys
+        together.
+        """
+        for i in key_list:
+            try:
+                return self.get(section, i)
+            except LookupError:
+                pass
+
+        # Making it here means all lookups failed.
+        raise LookupError("keys %r not found for section %r" %
+                          (key_list, section))
+
     def set(self, section, key, val):
         """Sets an option in the given section."""
         # TODO - set in multiple sections? (for now set in first)
@@ -346,15 +416,17 @@ class MultiOrderedConfigParser:
             self.defaults(section)[0][key] = val
 
     def read(self, filename):
+        """Parse configuration information from a file"""
         try:
-            with open(filename, 'rt') as file:
-                self._read(file, filename)
+            with open(filename, 'rt') as config_file:
+                self._read(config_file)
         except IOError:
             print "Could not open file ", filename, " for reading"
 
-    def _read(self, file, filename):
-        is_comment = False # used for multi-lined comments
-        for line in file:
+    def _read(self, config_file):
+        """Parse configuration information from the config_file"""
+        is_comment = False  # used for multi-lined comments
+        for line in config_file:
             line, is_comment = remove_comment(line, is_comment)
             if not line:
                 # line was empty or was a comment
@@ -377,18 +449,19 @@ class MultiOrderedConfigParser:
             key, val = try_option(line)
             sect[key] = val
 
-    def write(self, f):
+    def write(self, config_file):
+        """Write configuration information out to a file"""
         try:
             for key, val in self._includes.iteritems():
                 val.write(key)
-                f.write('#include "%s"\n' % key)
+                config_file.write('#include "%s"\n' % key)
 
-            f.write('\n')
-            write_dicts(f, self._defaults)
-            write_dicts(f, self._sections)
+            config_file.write('\n')
+            write_dicts(config_file, self._defaults)
+            write_dicts(config_file, self._sections)
         except:
             try:
-                with open(f, 'wt') as fp:
+                with open(config_file, 'wt') as fp:
                     self.write(fp)
             except IOError:
-                print "Could not open file ", f, " for writing"
+                print "Could not open file ", config_file, " for writing"
diff --git a/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py b/contrib/scripts/sip_to_pjsip/sip_to_pjsip.py
new file mode 100755 (executable)
index 0000000..96a9a02
--- /dev/null
@@ -0,0 +1,1151 @@
+#!/usr/bin/python
+
+import optparse
+import astdicts
+import astconfigparser
+import socket
+import re
+
+PREFIX = 'pjsip_'
+
+###############################################################################
+### some utility functions
+###############################################################################
+
+
+def section_by_type(section, pjsip, type):
+    """Finds a section based upon the given type, adding it if not found."""
+    def __find_dict(mdicts, key, val):
+        """Given a list of mult-dicts, return the multi-dict that contains
+           the given key/value pair."""
+
+        def found(d):
+            return key in d and val in d[key]
+
+        try:
+            return [d for d in mdicts if found(d)][0]
+        except IndexError:
+            raise LookupError("Dictionary not located for key = %s, value = %s"
+                              % (key, val))
+
+    try:
+        return __find_dict(pjsip.section(section), 'type', type)
+    except LookupError:
+        # section for type doesn't exist, so add
+        sect = pjsip.add_section(section)
+        sect['type'] = type
+        return sect
+
+
+def set_value(key=None, val=None, section=None, pjsip=None,
+              nmapped=None, type='endpoint'):
+    """Sets the key to the value within the section in pjsip.conf"""
+    def _set_value(k, v, s, r, n):
+        set_value(key if key else k, v, s, r, n, type)
+
+    # if no value or section return the set_value
+    # function with the enclosed key and type
+    if not val and not section:
+        return _set_value
+
+    # otherwise try to set the value
+    section_by_type(section, pjsip, type)[key] = \
+        val[0] if isinstance(val, list) else val
+
+
+def merge_value(key=None, val=None, section=None, pjsip=None,
+                nmapped=None, type='endpoint', section_to=None):
+    """Merge values from the given section with those from the default."""
+    def _merge_value(k, v, s, r, n):
+        merge_value(key if key else k, v, s, r, n, type, section_to)
+
+    # if no value or section return the merge_value
+    # function with the enclosed key and type
+    if not val and not section:
+        return _merge_value
+
+    # should return a single value section list
+    try:
+        sect = sip.section(section)[0]
+    except LookupError:
+        sect = sip.default(section)[0]
+    # for each merged value add it to pjsip.conf
+    for i in sect.get_merged(key):
+        set_value(key, i, section_to if section_to else section,
+                  pjsip, nmapped, type)
+
+
+def non_mapped(nmapped):
+    """Write non-mapped sip.conf values to the non-mapped object"""
+    def _non_mapped(section, key, val):
+        """Writes a non-mapped value from sip.conf to the non-mapped object."""
+        if section not in nmapped:
+            nmapped[section] = astconfigparser.Section()
+            if isinstance(val, list):
+                for v in val:
+                    # since coming from sip.conf we can assume
+                    # single section lists
+                    nmapped[section][0][key] = v
+            else:
+                nmapped[section][0][key] = val
+    return _non_mapped
+
+###############################################################################
+### mapping functions -
+###      define f(key, val, section) where key/val are the key/value pair to
+###      write to given section in pjsip.conf
+###############################################################################
+
+
+def set_dtmfmode(key, val, section, pjsip, nmapped):
+    """
+    Sets the dtmfmode value.  If value matches allowable option in pjsip
+    then map it, otherwise set it to none.
+    """
+    # available pjsip.conf values: rfc4733, inband, info, none
+    if val == 'inband' or val == 'info':
+        set_value(key, val, section, pjsip, nmapped)
+    elif val == 'rfc2833':
+        set_value(key, 'rfc4733', section, pjsip, nmapped)
+    else:
+        nmapped(section, key, val + " ; did not fully map - set to none")
+        set_value(key, 'none', section, pjsip, nmapped)
+
+
+def from_nat(key, val, section, pjsip, nmapped):
+    """Sets values from nat into the appropriate pjsip.conf options."""
+    # nat from sip.conf can be comma separated list of values:
+    # yes/no, [auto_]force_rport, [auto_]comedia
+    if 'yes' in val:
+        set_value('rtp_symmetric', 'yes', section, pjsip, nmapped)
+        set_value('rewrite_contact', 'yes', section, pjsip, nmapped)
+    if 'comedia' in val:
+        set_value('rtp_symmetric', 'yes', section, pjsip, nmapped)
+    if 'force_rport' in val:
+        set_value('force_rport', 'yes', section, pjsip, nmapped)
+        set_value('rewrite_contact', 'yes', section, pjsip, nmapped)
+
+
+def set_timers(key, val, section, pjsip, nmapped):
+    """
+    Sets the timers in pjsip.conf from the session-timers option
+    found in sip.conf.
+    """
+    # pjsip.conf values can be yes/no, required, always
+    if val == 'originate':
+        set_value('timers', 'always', section, pjsip, nmapped)
+    elif val == 'accept':
+        set_value('timers', 'required', section, pjsip, nmapped)
+    elif val == 'never':
+        set_value('timers', 'no', section, pjsip, nmapped)
+    else:
+        set_value('timers', 'yes', section, pjsip, nmapped)
+
+
+def set_direct_media(key, val, section, pjsip, nmapped):
+    """
+    Maps values from the sip.conf comma separated direct_media option
+    into pjsip.conf direct_media options.
+    """
+    if 'yes' in val:
+        set_value('direct_media', 'yes', section, pjsip, nmapped)
+    if 'update' in val:
+        set_value('direct_media_method', 'update', section, pjsip, nmapped)
+    if 'outgoing' in val:
+        set_value('directed_media_glare_mitigation', 'outgoing', section,
+                  pjsip, nmapped)
+    if 'nonat' in val:
+        set_value('disable_directed_media_on_nat', 'yes', section, pjsip,
+                  nmapped)
+    if 'no' in val:
+        set_value('direct_media', 'no', section, pjsip, nmapped)
+
+
+def from_sendrpid(key, val, section, pjsip, nmapped):
+    """Sets the send_rpid/pai values in pjsip.conf."""
+    if val == 'yes' or val == 'rpid':
+        set_value('send_rpid', 'yes', section, pjsip, nmapped)
+    elif val == 'pai':
+        set_value('send_pai', 'yes', section, pjsip, nmapped)
+
+
+def set_media_encryption(key, val, section, pjsip, nmapped):
+    """Sets the media_encryption value in pjsip.conf"""
+    try:
+        dtls = sip.get(section, 'dtlsenable')[0]
+        if dtls == 'yes':
+            # If DTLS is enabled, then that overrides SDES encryption.
+            return
+    except LookupError:
+        pass
+
+    if val == 'yes':
+        set_value('media_encryption', 'sdes', section, pjsip, nmapped)
+
+
+def from_recordfeature(key, val, section, pjsip, nmapped):
+    """
+    If record on/off feature is set to automixmon then set
+    one_touch_recording, otherwise it can't be mapped.
+    """
+    set_value('one_touch_recording', 'yes', section, pjsip, nmapped)
+    set_value(key, val, section, pjsip, nmapped)
+
+
+def from_progressinband(key, val, section, pjsip, nmapped):
+    """Sets the inband_progress value in pjsip.conf"""
+    # progressinband can = yes/no/never
+    if val == 'never':
+        val = 'no'
+    set_value('inband_progress', val, section, pjsip, nmapped)
+
+
+def build_host(config, host, section, port_key):
+    """
+    Returns a string composed of a host:port. This assumes that the host
+    may have a port as part of the initial value. The port_key is only used
+    if the host does not already have a port set on it.
+    Throws a LookupError if the key does not exist
+    """
+    port = None
+
+    try:
+        socket.inet_pton(socket.AF_INET6, host)
+        if not host.startswith('['):
+            # SIP URI will need brackets.
+            host = '[' + host + ']'
+        else:
+            # If brackets are present, there may be a port as well
+            port = re.match('\[.*\]:(\d+)', host)
+    except socket.error:
+        # No biggie. It's just not an IPv6 address
+        port = re.match('.*:(\d+)', host)
+
+    result = host
+
+    if not port:
+        try:
+            port = config.get(section, port_key)[0]
+            result += ':' + port
+        except LookupError:
+            pass
+
+    return result
+
+
+def from_host(key, val, section, pjsip, nmapped):
+    """
+    Sets contact info in an AOR section in pjsip.conf using 'host'
+    and 'port' data from sip.conf
+    """
+    # all aors have the same name as the endpoint so makes
+    # it easy to set endpoint's 'aors' value
+    set_value('aors', section, section, pjsip, nmapped)
+    if val == 'dynamic':
+        # Easy case. Just set the max_contacts on the aor and we're done
+        set_value('max_contacts', 1, section, pjsip, nmapped, 'aor')
+        return
+
+    result = 'sip:'
+
+    # More difficult case. The host will be either a hostname or
+    # IP address and may or may not have a port specified. pjsip.conf
+    # expects the contact to be a SIP URI.
+
+    user = None
+
+    try:
+        user = sip.multi_get(section, ['defaultuser', 'username'])[0]
+        result += user + '@'
+    except LookupError:
+        # It's fine if there's no user name
+        pass
+
+    result += build_host(sip, val, section, 'port')
+
+    set_value('contact', result, section, pjsip, nmapped, 'aor')
+
+
+def from_mailbox(key, val, section, pjsip, nmapped):
+    """
+    Determines whether a mailbox configured in sip.conf should map to
+    an endpoint or aor in pjsip.conf. If subscribemwi is true, then the
+    mailboxes are set on an aor. Otherwise the mailboxes are set on the
+    endpoint.
+    """
+
+    try:
+        subscribemwi = sip.get(section, 'subscribemwi')[0]
+    except LookupError:
+        # No subscribemwi option means default it to 'no'
+        subscribemwi = 'no'
+
+    set_value('mailboxes', val, section, pjsip, nmapped, 'aor'
+              if subscribemwi == 'yes' else 'endpoint')
+
+
+def setup_auth(key, val, section, pjsip, nmapped):
+    """
+    Sets up authentication information for a specific endpoint based on the
+    'secret' setting on a peer in sip.conf
+    """
+    set_value('username', section, section, pjsip, nmapped, 'auth')
+    # In chan_sip, if a secret and an md5secret are both specified on a peer,
+    # then in practice, only the md5secret is used. If both are encountered
+    # then we build an auth section that has both an md5_cred and password.
+    # However, the auth_type will indicate to authenticators to use the
+    # md5_cred, so like with sip.conf, the password will be there but have
+    # no purpose.
+    if key == 'secret':
+        set_value('password', val, section, pjsip, nmapped, 'auth')
+    else:
+        set_value('md5_cred', val, section, pjsip, nmapped, 'auth')
+        set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth')
+
+    realms = [section]
+    try:
+        auths = sip.get('authentication', 'auth')
+        for i in auths:
+            user, at, realm = i.partition('@')
+            realms.append(realm)
+    except LookupError:
+        pass
+
+    realm_str = ','.join(realms)
+
+    set_value('auth', section, section, pjsip, nmapped)
+    set_value('outbound_auth', realm_str, section, pjsip, nmapped)
+
+
+def setup_ident(key, val, section, pjsip, nmapped):
+    """
+    Examines the 'type' field for a sip.conf peer and creates an identify
+    section if the type is either 'peer' or 'friend'. The identify section uses
+    either the host or defaultip field of the sip.conf peer.
+    """
+    if val != 'peer' and val != 'friend':
+        return
+
+    try:
+        ip = sip.get(section, 'host')[0]
+    except LookupError:
+        return
+
+    if ip == 'dynamic':
+        try:
+            ip = sip.get(section, 'defaultip')[0]
+        except LookupError:
+            return
+
+    set_value('endpoint', section, section, pjsip, nmapped, 'identify')
+    set_value('match', ip, section, pjsip, nmapped, 'identify')
+
+
+def from_encryption_taglen(key, val, section, pjsip, nmapped):
+    """Sets the srtp_tag32 option based on sip.conf encryption_taglen"""
+    if val == '32':
+        set_value('srtp_tag_32', 'yes', section, pjsip, nmapped)
+
+
+def from_dtlsenable(key, val, section, pjsip, nmapped):
+    """Optionally sets media_encryption=dtls based on sip.conf dtlsenable"""
+    if val == 'yes':
+        set_value('media_encryption', 'dtls', section, pjsip, nmapped)
+
+###############################################################################
+
+# options in pjsip.conf on an endpoint that have no sip.conf equivalent:
+# type, rtp_ipv6, 100rel, trust_id_outbound, aggregate_mwi,
+# connected_line_method
+
+# known sip.conf peer keys that can be mapped to a pjsip.conf section/key
+peer_map = [
+    # sip.conf option      mapping function     pjsip.conf option(s)
+    ###########################################################################
+    ['context',            set_value],
+    ['dtmfmode',           set_dtmfmode],
+    ['disallow',           merge_value],
+    ['allow',              merge_value],
+    ['nat',                from_nat],            # rtp_symmetric, force_rport,
+                                                 # rewrite_contact
+    ['icesupport',         set_value('ice_support')],
+    ['autoframing',        set_value('use_ptime')],
+    ['outboundproxy',      set_value('outbound_proxy')],
+    ['mohsuggest',         set_value],
+    ['session-timers',     set_timers],          # timers
+    ['session-minse',      set_value('timers_min_se')],
+    ['session-expires',    set_value('timers_sess_expires')],
+    ['externip',           set_value('external_media_address')],
+    ['externhost',         set_value('external_media_address')],
+    # identify_by ?
+    ['directmedia',        set_direct_media],    # direct_media
+                                                 # direct_media_method
+                                                 # directed_media_glare_mitigation
+                                                 # disable_directed_media_on_nat
+    ['callerid',           set_value],           # callerid
+    ['callingpres',        set_value('callerid_privacy')],
+    ['cid_tag',            set_value('callerid_tag')],
+    ['trustpid',           set_value('trust_id_inbound')],
+    ['sendrpid',           from_sendrpid],       # send_pai, send_rpid
+    ['send_diversion',     set_value],
+    ['encrpytion',         set_media_encryption],
+    ['avpf',               set_value('use_avpf')],
+    ['recordonfeature',    from_recordfeature],  # automixon
+    ['recordofffeature',   from_recordfeature],  # automixon
+    ['progressinband',     from_progressinband], # in_band_progress
+    ['callgroup',          set_value],
+    ['pickupgroup',        set_value],
+    ['namedcallgroup',     set_value],
+    ['namedpickupgroup',   set_value],
+    ['allowtransfer',      set_value],
+    ['fromuser',           set_value],
+    ['fromdomain',         set_value],
+    ['mwifrom',            set_value('mwifromuser')],
+    ['tos_audio',          set_value],
+    ['tos_video',          set_value],
+    ['cos_audio',          set_value],
+    ['cos_video',          set_value],
+    ['sdpowner',           set_value],
+    ['sdpsession',         set_value],
+    ['tonezone',           set_value],
+    ['language',           set_value],
+    ['allowsubscribe',     set_value],
+    ['subminexpiry',       set_value],
+    ['rtp_engine',         set_value('rtpengine')],
+    ['mailbox',            from_mailbox],
+    ['busylevel',          set_value('devicestate_busy_at')],
+    ['secret',             setup_auth],
+    ['md5secret',          setup_auth],
+    ['type',               setup_ident],
+    ['dtlsenable',         from_dtlsenable],
+    ['dtlsverify',         set_value],
+    ['dtlsrekey',          set_value],
+    ['dtlscertfile',       set_value],
+    ['dtlsprivatekey',     set_value],
+    ['dtlscipher',         set_value],
+    ['dtlscafile',         set_value],
+    ['dtlscapath',         set_value],
+    ['dtlssetup',          set_value],
+    ['encryption_taglen',  from_encryption_taglen],
+
+############################ maps to an aor ###################################
+
+    ['host',               from_host],           # contact, max_contacts
+    ['qualifyfreq',        set_value('qualify_frequency', type='aor')],
+
+############################# maps to auth#####################################
+#        type = auth
+#        username
+#        password
+#        md5_cred
+#        realm
+#        nonce_lifetime
+#        auth_type
+######################### maps to acl/security ################################
+
+    ['permit',             merge_value(type='acl', section_to='acl')],
+    ['deny',               merge_value(type='acl', section_to='acl')],
+    ['acl',                merge_value(type='acl', section_to='acl')],
+    ['contactpermit',      merge_value(type='acl', section_to='acl')],
+    ['contactdeny',        merge_value(type='acl', section_to='acl')],
+    ['contactacl',         merge_value(type='acl', section_to='acl')],
+
+########################### maps to transport #################################
+#        type = transport
+#        protocol
+#        bind
+#        async_operations
+#        ca_list_file
+#        cert_file
+#        privkey_file
+#        password
+#        external_signaling_address - externip & externhost
+#        external_signaling_port
+#        external_media_address
+#        domain
+#        verify_server
+#        verify_client
+#        require_client_cert
+#        method
+#        cipher
+#        localnet
+######################### maps to domain_alias ################################
+#        type = domain_alias
+#        domain
+######################### maps to registration ################################
+#        type = registration
+#        server_uri
+#        client_uri
+#        contact_user
+#        transport
+#        outbound_proxy
+#        expiration
+#        retry_interval
+#        max_retries
+#        auth_rejection_permanent
+#        outbound_auth
+########################### maps to identify ##################################
+#        type = identify
+#        endpoint
+#        match
+]
+
+
+def add_localnet(section, pjsip, nmapped):
+    """
+    Adds localnet values from sip.conf's general section to a transport in
+    pjsip.conf. Ideally, we would have just created a template with the
+    localnet sections, but because this is a script, it's not hard to add
+    the same thing on to every transport.
+    """
+    try:
+        merge_value('localnet', sip.get('general', 'localnet')[0], 'general',
+                    pjsip, nmapped, 'transport', section)
+    except LookupError:
+        # No localnet options configured. No biggie!
+        pass
+
+
+def set_transport_common(section, pjsip, nmapped):
+    """
+    sip.conf has several global settings that in pjsip.conf apply to individual
+    transports. This function adds these global settings to each individual
+    transport.
+
+    The settings included are:
+    localnet
+    tos_sip
+    cos_sip
+    """
+
+    try:
+        merge_value('localnet', sip.get('general', 'localnet')[0], 'general',
+                    pjsip, nmapped, 'transport', section)
+    except LookupError:
+        # No localnet options configured. Move on.
+        pass
+
+    try:
+        set_value('tos', sip.get('general', 'sip_tos')[0], 'general', pjsip,
+                  nmapped, 'transport', section)
+    except LookupError:
+        pass
+
+    try:
+        set_value('cos', sip.get('general', 'sip_cos')[0], 'general', pjsip,
+                  nmapped, 'transport', section)
+    except LookupError:
+        pass
+
+
+def split_hostport(addr):
+    """
+    Given an address in the form 'addr:port' separate the addr and port
+    components.
+    Returns a two-tuple of strings, (addr, port). If no port is present in the
+    string, then the port section of the tuple is None.
+    """
+    try:
+        socket.inet_pton(socket.AF_INET6, addr)
+        if not addr.startswith('['):
+            return (addr, None)
+        else:
+            # If brackets are present, there may be a port as well
+            match = re.match('\[(.*\)]:(\d+)', addr)
+            if match:
+                return (match.group(1), match.group(2))
+            else:
+                return (addr, None)
+    except socket.error:
+        pass
+
+    # IPv4 address or hostname
+    host, sep, port = addr.rpartition(':')
+
+    if not sep and not port:
+        return (host, None)
+    else:
+        return (host, port)
+
+
+def create_udp(sip, pjsip, nmapped):
+    """
+    Creates a 'transport-udp' section in the pjsip.conf file based
+    on the following settings from sip.conf:
+
+    bindaddr (or udpbindaddr)
+    bindport
+    externaddr (or externip)
+    externhost
+    """
+
+    bind = sip.multi_get('general', ['udpbindaddr', 'bindaddr'])[0]
+    bind = build_host(sip, bind, 'general', 'bindport')
+
+    try:
+        extern_addr = sip.multi_get('general', ['externaddr', 'externip',
+                                    'externhost'])[0]
+        host, port = split_hostport(extern_addr)
+        set_value('external_signaling_address', host, 'transport-udp', pjsip,
+                  nmapped, 'transport')
+        if port:
+            set_value('external_signaling_port', port, 'transport-udp', pjsip,
+                      nmapped, 'transport')
+    except LookupError:
+        pass
+
+    set_value('protocol', 'udp', 'transport-udp', pjsip, nmapped, 'transport')
+    set_value('bind', bind, 'transport-udp', pjsip, nmapped, 'transport')
+    set_transport_common('transport-udp', pjsip, nmapped)
+
+
+def create_tcp(sip, pjsip, nmapped):
+    """
+    Creates a 'transport-tcp' section in the pjsip.conf file based
+    on the following settings from sip.conf:
+
+    tcpenable
+    tcpbindaddr
+    externtcpport
+    """
+
+    try:
+        enabled = sip.get('general', 'tcpenable')[0]
+    except:
+        # No value means disabled by default. No need for a tranport
+        return
+
+    if enabled == 'no':
+        return
+
+    try:
+        bind = sip.get('general', 'tcpbindaddr')[0]
+        bind = build_host(sip, bind, 'general', 'bindport')
+    except LookupError:
+        # No tcpbindaddr means to default to the udpbindaddr
+        bind = pjsip.get('transport-udp', 'bind')[0]
+
+    try:
+        extern_addr = sip.multi_get('general', ['externaddr', 'externip',
+                                    'externhost'])[0]
+        host, port = split_hostport(extern_addr)
+        try:
+            tcpport = sip.get('general', 'externtcpport')[0]
+        except:
+            tcpport = port
+        set_value('external_signaling_address', host, 'transport-tcp', pjsip,
+                  nmapped, 'transport')
+        if tcpport:
+            set_value('external_signaling_port', tcpport, 'transport-tcp',
+                      pjsip, nmapped, 'transport')
+    except LookupError:
+        pass
+
+    set_value('protocol', 'tcp', 'transport-tcp', pjsip, nmapped, 'transport')
+    set_value('bind', bind, 'transport-tcp', pjsip, nmapped, 'transport')
+    set_transport_common('transport-tcp', pjsip, nmapped)
+
+
+def set_tls_bindaddr(val, pjsip, nmapped):
+    """
+    Creates the TCP bind address. This has two possible methods of
+    working:
+    Use the 'tlsbindaddr' option from sip.conf directly if it has both
+    an address and port. If no port is present, use 5061
+    If there is no 'tlsbindaddr' option present in sip.conf, use the
+    previously-established UDP bind address and port 5061
+    """
+    try:
+        bind = sip.get('general', 'tlsbindaddr')[0]
+        explicit = True
+    except LookupError:
+        # No tlsbindaddr means to default to the bindaddr but with standard TLS
+        # port
+        bind = pjsip.get('transport-udp', 'bind')[0]
+        explicit = False
+
+    matchv4 = re.match('\d+\.\d+\.\d+\.\d+:\d+', bind)
+    matchv6 = re.match('\[.*\]:d+', bind)
+    if matchv4 or matchv6:
+        if explicit:
+            # They provided a port. We'll just use it.
+            set_value('bind', bind, 'transport-tls', pjsip, nmapped,
+                      'transport')
+            return
+        else:
+            # Need to strip the port from the UDP address
+            index = bind.rfind(':')
+            bind = bind[:index]
+
+    # Reaching this point means either there was no port provided or we
+    # stripped the port off. We need to add on the default 5061 port
+
+    bind += ':5061'
+
+    set_value('bind', bind, 'transport-tls', pjsip, nmapped, 'transport')
+
+
+def set_tls_private_key(val, pjsip, nmapped):
+    """Sets privkey_file based on sip.conf tlsprivatekey or sslprivatekey"""
+    set_value('privkey_file', val, 'transport-tls', pjsip, nmapped,
+              'transport')
+
+
+def set_tls_cipher(val, pjsip, nmapped):
+    """Sets cipher based on sip.conf tlscipher or sslcipher"""
+    set_value('cipher', val, 'transport-tls', pjsip, nmapped, 'transport')
+
+
+def set_tls_cafile(val, pjsip, nmapped):
+    """Sets ca_list_file based on sip.conf tlscafile"""
+    set_value('ca_list_file', val, 'transport-tls', pjsip, nmapped,
+              'transport')
+
+
+def set_tls_verifyclient(val, pjsip, nmapped):
+    """Sets verify_client based on sip.conf tlsverifyclient"""
+    set_value('verify_client', val, 'transport-tls', pjsip, nmapped,
+              'transport')
+
+
+def set_tls_verifyserver(val, pjsip, nmapped):
+    """Sets verify_server based on sip.conf tlsdontverifyserver"""
+
+    if val == 'no':
+        set_value('verify_server', 'yes', 'transport-tls', pjsip, nmapped,
+                  'transport')
+    else:
+        set_value('verify_server', 'no', 'transport-tls', pjsip, nmapped,
+                  'transport')
+
+
+def set_tls_method(val, pjsip, nmapped):
+    """Sets method based on sip.conf tlsclientmethod or sslclientmethod"""
+    set_value('method', val, 'transport-tls', pjsip, nmapped, 'transport')
+
+
+def create_tls(sip, pjsip, nmapped):
+    """
+    Creates a 'transport-tls' section in pjsip.conf based on the following
+    settings from sip.conf:
+
+    tlsenable (or sslenable)
+    tlsbindaddr (or sslbindaddr)
+    tlsprivatekey (or sslprivatekey)
+    tlscipher (or sslcipher)
+    tlscafile
+    tlscapath (or tlscadir)
+    tlscertfile (or sslcert or tlscert)
+    tlsverifyclient
+    tlsdontverifyserver
+    tlsclientmethod (or sslclientmethod)
+    """
+
+    tls_map = [
+        (['tlsbindaddr', 'sslbindaddr'], set_tls_bindaddr),
+        (['tlsprivatekey', 'sslprivatekey'], set_tls_private_key),
+        (['tlscipher', 'sslcipher'], set_tls_cipher),
+        (['tlscafile'], set_tls_cafile),
+        (['tlsverifyclient'], set_tls_verifyclient),
+        (['tlsdontverifyserver'], set_tls_verifyserver),
+        (['tlsclientmethod', 'sslclientmethod'], set_tls_method)
+    ]
+
+    try:
+        enabled = sip.multi_get('general', ['tlsenable', 'sslenable'])[0]
+    except LookupError:
+        # Not enabled. Don't create a transport
+        return
+
+    if enabled == 'no':
+        return
+
+    set_value('protocol', 'tls', 'transport-tls', pjsip, nmapped, 'transport')
+
+    for i in tls_map:
+        try:
+            i[1](sip.multi_get('general', i[0])[0], pjsip, nmapped)
+        except LookupError:
+            pass
+
+    set_transport_common('transport-tls', pjsip, nmapped)
+    try:
+        extern_addr = sip.multi_get('general', ['externaddr', 'externip',
+                                    'externhost'])[0]
+        host, port = split_hostport(extern_addr)
+        try:
+            tlsport = sip.get('general', 'externtlsport')[0]
+        except:
+            tlsport = port
+        set_value('external_signaling_address', host, 'transport-tls', pjsip,
+                  nmapped, 'transport')
+        if tlsport:
+            set_value('external_signaling_port', tlsport, 'transport-tls',
+                      pjsip, nmapped, 'transport')
+    except LookupError:
+        pass
+
+
+def map_transports(sip, pjsip, nmapped):
+    """
+    Finds options in sip.conf general section pertaining to
+    transport configuration and creates appropriate transport
+    configuration sections in pjsip.conf.
+
+    sip.conf only allows a single UDP transport, TCP transport,
+    and TLS transport. As such, the mapping into PJSIP can be made
+    consistent by defining three sections:
+
+    transport-udp
+    transport-tcp
+    transport-tls
+
+    To accommodate the default behaviors in sip.conf, we'll need to
+    create the UDP transport first, followed by the TCP and TLS transports.
+    """
+
+    # First create a UDP transport. Even if no bind parameters were provided
+    # in sip.conf, chan_sip would always bind to UDP 0.0.0.0:5060
+    create_udp(sip, pjsip, nmapped)
+
+    # TCP settings may be dependent on UDP settings, so do it second.
+    create_tcp(sip, pjsip, nmapped)
+    create_tls(sip, pjsip, nmapped)
+
+
+def map_auth(sip, pjsip, nmapped):
+    """
+    Creates auth sections based on entries in the authentication section of
+    sip.conf. pjsip.conf section names consist of "auth_" followed by the name
+    of the realm.
+    """
+    try:
+        auths = sip.get('authentication', 'auth')
+    except LookupError:
+        return
+
+    for i in auths:
+        creds, at, realm = i.partition('@')
+        if not at and not realm:
+            # Invalid. Move on
+            continue
+        user, colon, secret = creds.partition(':')
+        if not secret:
+            user, sharp, md5 = creds.partition('#')
+            if not md5:
+                #Invalid. move on
+                continue
+        section = "auth_" + realm
+
+        set_value('realm', realm, section, pjsip, nmapped, 'auth')
+        set_value('username', user, section, pjsip, nmapped, 'auth')
+        if secret:
+            set_value('password', secret, section, pjsip, nmapped, 'auth')
+        else:
+            set_value('md5_cred', md5, section, pjsip, nmapped, 'auth')
+            set_value('auth_type', 'md5', section, pjsip, nmapped, 'auth')
+
+
+class Registration:
+    """
+    Class for parsing and storing information in a register line in sip.conf.
+    """
+    def __init__(self, line, retry_interval, max_attempts, outbound_proxy):
+        self.retry_interval = retry_interval
+        self.max_attempts = max_attempts
+        self.outbound_proxy = outbound_proxy
+        self.parse(line)
+
+    def parse(self, line):
+        """
+        Initial parsing routine for register lines in sip.conf.
+
+        This splits the line into the part before the host, and the part
+        after the '@' symbol. These two parts are then passed to their
+        own parsing routines
+        """
+
+        # register =>
+        # [peer?][transport://]user[@domain][:secret[:authuser]]@host[:port][/extension][~expiry]
+
+        prehost, at, host_part = line.rpartition('@')
+        if not prehost:
+            raise
+
+        self.parse_host_part(host_part)
+        self.parse_user_part(prehost)
+
+    def parse_host_part(self, host_part):
+        """
+        Parsing routine for the part after the final '@' in a register line.
+        The strategy is to use partition calls to peel away the data starting
+        from the right and working to the left.
+        """
+        pre_expiry, sep, expiry = host_part.partition('~')
+        pre_extension, sep, self.extension = pre_expiry.partition('/')
+        self.host, sep, self.port = pre_extension.partition(':')
+
+        self.expiry = expiry if expiry else '120'
+
+    def parse_user_part(self, user_part):
+        """
+        Parsing routine for the part before the final '@' in a register line.
+        The only mandatory part of this line is the user portion. The strategy
+        here is to start by using partition calls to remove everything to
+        the right of the user, then finish by using rpartition calls to remove
+        everything to the left of the user.
+        """
+        colons = user_part.count(':')
+        if (colons == 3):
+            # :domainport:secret:authuser
+            pre_auth, sep, port_auth = user_part.partition(':')
+            self.domainport, sep, auth = port_auth.partition(':')
+            self.secret, sep, self.authuser = auth.partition(':')
+        elif (colons == 2):
+            # :secret:authuser
+            pre_auth, sep, auth = user_part.partition(':')
+            self.secret, sep, self.authuser = auth.partition(':')
+        elif (colons == 1):
+            # :secret
+            pre_auth, sep, self.secret = user_part.partition(':')
+        elif (colons == 0):
+            # No port, secret, or authuser
+            pre_auth = user_part
+        else:
+            # Invalid setting
+            raise
+
+        pre_domain, sep, self.domain = pre_auth.partition('@')
+        self.peer, sep, post_peer = pre_domain.rpartition('?')
+        transport, sep, self.user = post_peer.rpartition('://')
+
+        self.protocol = transport if transport else 'udp'
+
+    def write(self, pjsip, nmapped):
+        """
+        Write parsed registration data into a section in pjsip.conf
+
+        Most of the data in self will get written to a registration section.
+        However, there will also need to be an auth section created if a
+        secret or authuser is present.
+
+        General mapping of values:
+        A combination of self.host and self.port is server_uri
+        A combination of self.user, self.domain, and self.domainport is
+          client_uri
+        self.expiry is expiration
+        self.extension is contact_user
+        self.protocol will map to one of the mapped transports
+        self.secret and self.authuser will result in a new auth section, and
+          outbound_auth will point to that section.
+        XXX self.peer really doesn't map to anything :(
+        """
+
+        section = 'reg_' + self.host
+
+        set_value('retry_interval', self.retry_interval, section, pjsip,
+                  nmapped, 'registration')
+        set_value('max_retries', self.max_attempts, section, pjsip, nmapped,
+                  'registration')
+        if self.extension:
+            set_value('contact_user', self.extension, section, pjsip, nmapped,
+                      'registration')
+
+        set_value('expiration', self.expiry, section, pjsip, nmapped,
+                  'registration')
+
+        if self.protocol == 'udp':
+            set_value('transport', 'transport-udp', section, pjsip, nmapped,
+                      'registration')
+        elif self.protocol == 'tcp':
+            set_value('transport', 'transport-tcp', section, pjsip, nmapped,
+                      'registration')
+        elif self.protocol == 'tls':
+            set_value('transport', 'transport-tls', section, pjsip, nmapped,
+                      'registration')
+
+        auth_section = 'auth_reg_' + self.host
+
+        if self.secret:
+            set_value('password', self.secret, auth_section, pjsip, nmapped,
+                      'auth')
+            set_value('username', self.authuser or self.user, auth_section,
+                      pjsip, nmapped, 'auth')
+            set_value('outbound_auth', auth_section, section, pjsip, nmapped,
+                      'registration')
+
+        client_uri = "sip:%s@" % self.user
+        if self.domain:
+            client_uri += self.domain
+        else:
+            client_uri += self.host
+
+        if self.domainport:
+            client_uri += ":" + self.domainport
+        elif self.port:
+            client_uri += ":" + self.port
+
+        set_value('client_uri', client_uri, section, pjsip, nmapped,
+                  'registration')
+
+        server_uri = "sip:%s" % self.host
+        if self.port:
+            server_uri += ":" + self.port
+
+        set_value('server_uri', server_uri, section, pjsip, nmapped,
+                  'registration')
+
+        if self.outbound_proxy:
+            set_value('outboundproxy', self.outbound_proxy, section, pjsip,
+                      nmapped, 'registartion')
+
+
+def map_registrations(sip, pjsip, nmapped):
+    """
+    Gathers all necessary outbound registration data in sip.conf and creates
+    corresponding registration sections in pjsip.conf
+    """
+    try:
+        regs = sip.get('general', 'register')
+    except LookupError:
+        return
+
+    try:
+        retry_interval = sip.get('general', 'registertimeout')[0]
+    except LookupError:
+        retry_interval = '20'
+
+    try:
+        max_attempts = sip.get('general', 'registerattempts')[0]
+    except LookupError:
+        max_attempts = '10'
+
+    try:
+        outbound_proxy = sip.get('general', 'outboundproxy')[0]
+    except LookupError:
+        outbound_proxy = ''
+
+    for i in regs:
+        reg = Registration(i, retry_interval, max_attempts, outbound_proxy)
+        reg.write(pjsip, nmapped)
+
+
+def map_peer(sip, section, pjsip, nmapped):
+    """
+    Map the options from a peer section in sip.conf into the appropriate
+    sections in pjsip.conf
+    """
+    for i in peer_map:
+        try:
+            # coming from sip.conf the values should mostly be a list with a
+            # single value.  In the few cases that they are not a specialized
+            # function (see merge_value) is used to retrieve the values.
+            i[1](i[0], sip.get(section, i[0])[0], section, pjsip, nmapped)
+        except LookupError:
+            pass  # key not found in sip.conf
+
+
+def find_non_mapped(sections, nmapped):
+    """
+    Determine sip.conf options that were not properly mapped to pjsip.conf
+    options.
+    """
+    for section, sect in sections.iteritems():
+        try:
+            # since we are pulling from sip.conf this should always
+            # be a single value list
+            sect = sect[0]
+            # loop through the section and store any values that were not
+            # mapped
+            for key in sect.keys(True):
+                for i in peer_map:
+                    if i[0] == key:
+                        break
+                else:
+                    nmapped(section, key, sect[key])
+        except LookupError:
+            pass
+
+
+def convert(sip, filename, non_mappings, include):
+    """
+    Entry point for configuration file conversion. This
+    function will create a pjsip.conf object and begin to
+    map specific sections from sip.conf into it.
+    Returns the new pjsip.conf object once completed
+    """
+    pjsip = astconfigparser.MultiOrderedConfigParser()
+    non_mappings[filename] = astdicts.MultiOrderedDict()
+    nmapped = non_mapped(non_mappings[filename])
+    if not include:
+        # Don't duplicate transport and registration configs
+        map_transports(sip, pjsip, nmapped)
+        map_registrations(sip, pjsip, nmapped)
+    map_auth(sip, pjsip, nmapped)
+    for section in sip.sections():
+        if section == 'authentication':
+            pass
+        else:
+            map_peer(sip, section, pjsip, nmapped)
+
+    find_non_mapped(sip.defaults(), nmapped)
+    find_non_mapped(sip.sections(), nmapped)
+
+    for key, val in sip.includes().iteritems():
+        pjsip.add_include(PREFIX + key, convert(val, PREFIX + key,
+                          non_mappings, True)[0])
+    return pjsip, non_mappings
+
+
+def write_pjsip(filename, pjsip, non_mappings):
+    """
+    Write pjsip.conf file to disk
+    """
+    try:
+        with open(filename, 'wt') as fp:
+            fp.write(';--\n')
+            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
+            fp.write('Non mapped elements start\n')
+            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n')
+            astconfigparser.write_dicts(fp, non_mappings[filename])
+            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
+            fp.write('Non mapped elements end\n')
+            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
+            fp.write('--;\n\n')
+            # write out include file(s)
+            pjsip.write(fp)
+
+    except IOError:
+        print "Could not open file ", filename, " for writing"
+
+###############################################################################
+
+
+def cli_options():
+    """
+    Parse command line options and apply them. If invalid input is given,
+    print usage information
+    """
+    global PREFIX
+    usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
+        "input-file defaults to 'sip.conf'\n" \
+        "output-file defaults to 'pjsip.conf'"
+    parser = optparse.OptionParser(usage=usage)
+    parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX,
+                      help='output prefix for include files')
+
+    options, args = parser.parse_args()
+    PREFIX = options.prefix
+
+    sip_filename = args[0] if len(args) else 'sip.conf'
+    pjsip_filename = args[1] if len(args) == 2 else 'pjsip.conf'
+
+    return sip_filename, pjsip_filename
+
+if __name__ == "__main__":
+    sip_filename, pjsip_filename = cli_options()
+    # configuration parser for sip.conf
+    sip = astconfigparser.MultiOrderedConfigParser()
+    sip.read(sip_filename)
+    pjsip, non_mappings = convert(sip, pjsip_filename, dict(), False)
+    write_pjsip(pjsip_filename, pjsip, non_mappings)
diff --git a/contrib/scripts/sip_to_res_sip/sip_to_res_sip.py b/contrib/scripts/sip_to_res_sip/sip_to_res_sip.py
deleted file mode 100755 (executable)
index ca253bd..0000000
+++ /dev/null
@@ -1,392 +0,0 @@
-#!/usr/bin/python
-
-###############################################################################
-# TODO:
-# (1) There is more work to do here, at least for the sip.conf items that
-#     aren't currently parsed. An issue will be created for that.
-# (2) All of the scripts should probably be passed through pylint and have
-#     as many PEP8 issues fixed as possible
-# (3) A public review is probably warranted at that point of the entire script
-###############################################################################
-
-import optparse
-import astdicts
-import astconfigparser
-
-PREFIX = 'res_sip_'
-
-###############################################################################
-### some utility functions
-###############################################################################
-def section_by_type(section, res_sip, type):
-    """Finds a section based upon the given type, adding it if not found."""
-    try:
-        return astconfigparser.find_dict(
-            res_sip.section(section), 'type', type)
-    except LookupError:
-        # section for type doesn't exist, so add
-        sect = res_sip.add_section(section)
-        sect['type'] = type
-        return sect
-
-def set_value(key=None, val=None, section=None, res_sip=None,
-              nmapped=None, type='endpoint'):
-    """Sets the key to the value within the section in res_sip.conf"""
-    def _set_value(k, v, s, r, n):
-        set_value(key if key else k, v, s, r, n, type)
-
-    # if no value or section return the set_value
-    # function with the enclosed key and type
-    if not val and not section:
-        return _set_value
-
-    # otherwise try to set the value
-    section_by_type(section, res_sip, type)[key] = \
-        val[0] if isinstance(val, list) else val
-
-def merge_value(key=None, val=None, section=None, res_sip=None,
-                nmapped=None, type='endpoint', section_to=None):
-    """Merge values from the given section with those from the default."""
-    def _merge_value(k, v, s, r, n):
-        merge_value(key if key else k, v, s, r, n, type, section_to)
-
-    # if no value or section return the merge_value
-    # function with the enclosed key and type
-    if not val and not section:
-        return _merge_value
-
-    # should return a single value section list
-    sect = sip.section(section)[0]
-    # for each merged value add it to res_sip.conf
-    for i in sect.get_merged(key):
-        set_value(key, i, section_to if section_to else section,
-                  res_sip, nmapped, type)
-
-def is_in(s, sub):
-    """Returns true if 'sub' is in 's'"""
-    return s.find(sub) != -1
-
-def non_mapped(nmapped):
-    def _non_mapped(section, key, val):
-        """Writes a non-mapped value from sip.conf to the non-mapped object."""
-        if section not in nmapped:
-            nmapped[section] = astconfigparser.Section()
-            if isinstance(val, list):
-                for v in val:
-                    # since coming from sip.conf we can assume
-                    # single section lists
-                    nmapped[section][0][key] = v
-            else:
-                nmapped[section][0][key] = val
-    return _non_mapped
-
-###############################################################################
-### mapping functions -
-###      define f(key, val, section) where key/val are the key/value pair to
-###      write to given section in res_sip.conf
-###############################################################################
-
-def set_dtmfmode(key, val, section, res_sip, nmapped):
-    """Sets the dtmfmode value.  If value matches allowable option in res_sip
-       then map it, otherwise set it to none.
-    """
-    # available res_sip.conf values: frc4733, inband, info, none
-    if val != 'inband' or val != 'info':
-        nmapped(section, key, val + " ; did not fully map - set to none")
-        val = 'none'
-    set_value(key, val, section, res_sip, nmapped)
-
-def from_nat(key, val, section, res_sip, nmapped):
-    """Sets values from nat into the appropriate res_sip.conf options."""
-    # nat from sip.conf can be comma separated list of values:
-    # yes/no, [auto_]force_rport, [auto_]comedia
-    if is_in(val, 'yes'):
-        set_value('rtp_symmetric', 'yes', section, res_sip, nmapped)
-        set_value('rewrite_contact', 'yes', section, res_sip, nmapped)
-    if is_in(val, 'comedia'):
-        set_value('rtp_symmetric', 'yes', section, res_sip, nmapped)
-    if is_in(val, 'force_rport'):
-        set_value('force_rport', 'yes', section, res_sip, nmapped)
-        set_value('rewrite_contact', 'yes', section, res_sip, nmapped)
-
-def set_timers(key, val, section, res_sip, nmapped):
-    """Sets the timers in res_sip.conf from the session-timers option
-       found in sip.conf.
-    """
-    # res_sip.conf values can be yes/no, required, always
-    if val == 'originate':
-        set_value('timers', 'always', section, res_sip, nmapped)
-    elif val == 'accept':
-        set_value('timers', 'required', section, res_sip, nmapped)
-    elif val == 'never':
-        set_value('timers', 'no', section, res_sip, nmapped)
-    else:
-        set_value('timers', 'yes', section, res_sip, nmapped)
-
-def set_direct_media(key, val, section, res_sip, nmapped):
-    """Maps values from the sip.conf comma separated direct_media option
-       into res_sip.conf direct_media options.
-    """
-    if is_in(val, 'yes'):
-        set_value('direct_media', 'yes', section, res_sip, nmapped)
-    if is_in(val, 'update'):
-        set_value('direct_media_method', 'update', section, res_sip, nmapped)
-    if is_in(val, 'outgoing'):
-        set_value('directed_media_glare_mitigation', 'outgoing', section, res_sip, nmapped)
-    if is_in(val, 'nonat'):
-        set_value('disable_directed_media_on_nat','yes', section, res_sip, nmapped)
-    if (val == 'no'):
-        set_value('direct_media', 'no', section, res_sip, nmapped)
-
-def from_sendrpid(key, val, section, res_sip, nmapped):
-    """Sets the send_rpid/pai values in res_sip.conf."""
-    if val == 'yes' or val == 'rpid':
-        set_value('send_rpid', 'yes', section, res_sip, nmapped)
-    elif val == 'pai':
-        set_value('send_pai', 'yes', section, res_sip, nmapped)
-
-def set_media_encryption(key, val, section, res_sip, nmapped):
-    """Sets the media_encryption value in res_sip.conf"""
-    if val == 'yes':
-        set_value('media_encryption', 'sdes', section, res_sip, nmapped)
-
-def from_recordfeature(key, val, section, res_sip, nmapped):
-    """If record on/off feature is set to automixmon then set
-       one_touch_recording, otherwise it can't be mapped.
-    """
-    if val == 'automixmon':
-        set_value('one_touch_recording', 'yes', section, res_sip, nmapped)
-    else:
-        nmapped(section, key, val + " ; could not be fully mapped")
-
-def from_progressinband(key, val, section, res_sip, nmapped):
-    """Sets the inband_progress value in res_sip.conf"""
-    # progressinband can = yes/no/never
-    if val == 'never':
-        val = 'no'
-    set_value('inband_progress', val, section, res_sip, nmapped)
-
-def from_host(key, val, section, res_sip, nmapped):
-    """Sets contact info in an AOR section in in res_sip.conf using 'host'
-       data from sip.conf
-    """
-    # all aors have the same name as the endpoint so makes
-    # it easy to endpoint's 'aors' value
-    set_value('aors', section, section, res_sip, nmapped)
-    if val != 'dynamic':
-        set_value('contact', val, section, res_sip, nmapped, 'aor')
-    else:
-        set_value('max_contacts', 1, section, res_sip, nmapped, 'aor')
-
-def from_subscribemwi(key, val, section, res_sip, nmapped):
-    """Checks the subscribemwi value in sip.conf.  If yes places the
-       mailbox value in mailboxes within the endpoint, otherwise puts
-       it in the aor.
-    """
-    mailboxes = sip.get('mailbox', section, res_sip)
-    type = 'endpoint' if val == 'yes' else 'aor'
-    set_value('mailboxes', mailboxes, section, res_sip, nmapped, type)
-
-###############################################################################
-
-# options in res_sip.conf on an endpoint that have no sip.conf equivalent:
-# type, rtp_ipv6, 100rel, trust_id_outbound, aggregate_mwi,
-# connected_line_method
-
-# known sip.conf peer keys that can be mapped to a res_sip.conf section/key
-peer_map = [
-    # sip.conf option      mapping function     res_sip.conf option(s)
-    ###########################################################################
-    ['context',            set_value],
-    ['dtmfmode',           set_dtmfmode],
-    ['disallow',           merge_value],
-    ['allow',              merge_value],
-    ['nat',                from_nat],            # rtp_symmetric, force_rport,
-                                                 # rewrite_contact
-    ['icesupport',         set_value('ice_support')],
-    ['autoframing',        set_value('use_ptime')],
-    ['outboundproxy',      set_value('outbound_proxy')],
-    ['mohsuggest',         set_value],
-    ['session-timers',     set_timers],          # timers
-    ['session-minse',      set_value('timers_min_se')],
-    ['session-expires',    set_value('timers_sess_expires')],
-    ['externip',           set_value('external_media_address')],
-    ['externhost',         set_value('external_media_address')],
-    # identify_by ?
-    ['directmedia',        set_direct_media],    # direct_media
-                                                 # direct_media_method
-                                                 # directed_media_glare_mitigation
-                                                 # disable_directed_media_on_nat
-    ['callerid',           set_value],           # callerid
-    ['callingpres',        set_value('callerid_privacy')],
-    ['cid_tag',            set_value('callerid_tag')],
-    ['trustpid',           set_value('trust_id_inbound')],
-    ['sendrpid',           from_sendrpid],       # send_pai, send_rpid
-    ['send_diversion',     set_value],
-    ['encrpytion',         set_media_encryption],
-    ['use_avpf',           set_value],
-    ['recordonfeature',    from_recordfeature],  # automixon
-    ['recordofffeature',   from_recordfeature],  # automixon
-    ['progressinband',     from_progressinband], # in_band_progress
-    ['callgroup',          set_value],
-    ['pickupgroup',        set_value],
-    ['namedcallgroup',     set_value],
-    ['namedpickupgroup',   set_value],
-    ['busylevel',          set_value('devicestate_busy_at')],
-
-############################ maps to an aor ###################################
-
-    ['host',               from_host],           # contact, max_contacts
-    ['subscribemwi',       from_subscribemwi],   # mailboxes
-    ['qualifyfreq',        set_value('qualify_frequency', type='aor')],
-
-############################# maps to auth#####################################
-#        type = auth
-#        username
-#        password
-#        md5_cred
-#        realm
-#        nonce_lifetime
-#        auth_type
-######################### maps to acl/security ################################
-
-    ['permit',             merge_value(type='security', section_to='acl')],
-    ['deny',               merge_value(type='security', section_to='acl')],
-    ['acl',                merge_value(type='security', section_to='acl')],
-    ['contactpermit',      merge_value(type='security', section_to='acl')],
-    ['contactdeny',        merge_value(type='security', section_to='acl')],
-    ['contactacl',         merge_value(type='security', section_to='acl')],
-
-########################### maps to transport #################################
-#        type = transport
-#        protocol
-#        bind
-#        async_operations
-#        ca_list_file
-#        cert_file
-#        privkey_file
-#        password
-#        external_signaling_address - externip & externhost
-#        external_signaling_port
-#        external_media_address
-#        domain
-#        verify_server
-#        verify_client
-#        require_client_cert
-#        method
-#        cipher
-#        localnet
-######################### maps to domain_alias ################################
-#        type = domain_alias
-#        domain
-######################### maps to registration ################################
-#        type = registration
-#        server_uri
-#        client_uri
-#        contact_user
-#        transport
-#        outbound_proxy
-#        expiration
-#        retry_interval
-#        max_retries
-#        auth_rejection_permanent
-#        outbound_auth
-########################### maps to identify ##################################
-#        type = identify
-#        endpoint
-#        match
-]
-
-def map_peer(sip, section, res_sip, nmapped):
-    for i in peer_map:
-        try:
-            # coming from sip.conf the values should mostly be a list with a
-            # single value.  In the few cases that they are not a specialized
-            # function (see merge_value) is used to retrieve the values.
-            i[1](i[0], sip.get(section, i[0])[0], section, res_sip, nmapped)
-        except LookupError:
-            pass # key not found in sip.conf
-
-def find_non_mapped(sections, nmapped):
-    for section, sect in sections.iteritems():
-        try:
-            # since we are pulling from sip.conf this should always
-            # be a single value list
-            sect = sect[0]
-            # loop through the section and store any values that were not mapped
-            for key in sect.keys(True):
-                for i in peer_map:
-                    if i[0] == key:
-                        break;
-                else:
-                    nmapped(section, key, sect[key])
-        except LookupError:
-            pass
-
-def convert(sip, filename, non_mappings):
-    res_sip = astconfigparser.MultiOrderedConfigParser()
-    non_mappings[filename] = astdicts.MultiOrderedDict()
-    nmapped = non_mapped(non_mappings[filename])
-    for section in sip.sections():
-        if section == 'authentication':
-            pass
-        else:
-            map_peer(sip, section, res_sip, nmapped)
-
-    find_non_mapped(sip.defaults(), nmapped)
-    find_non_mapped(sip.sections(), nmapped)
-
-    for key, val in sip.includes().iteritems():
-        res_sip.add_include(PREFIX + key, convert(val, PREFIX + key, non_mappings)[0])
-    return res_sip, non_mappings
-
-def write_res_sip(filename, res_sip, non_mappings):
-    try:
-        with open(filename, 'wt') as fp:
-            fp.write(';--\n')
-            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
-            fp.write('Non mapped elements start\n')
-            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n')
-            astconfigparser.write_dicts(fp, non_mappings[filename])
-            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
-            fp.write('Non mapped elements end\n')
-            fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
-            fp.write('--;\n\n')
-            # write out include file(s)
-            for key, val in res_sip.includes().iteritems():
-                write_res_sip(key, val, non_mappings)
-                fp.write('#include "%s"\n' % key)
-            fp.write('\n')
-            # write out mapped data elements
-            astconfigparser.write_dicts(fp, res_sip.defaults())
-            astconfigparser.write_dicts(fp, res_sip.sections())
-
-    except IOError:
-        print "Could not open file ", filename, " for writing"
-
-###############################################################################
-
-def cli_options():
-    global PREFIX
-    usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
-        "input-file defaults to 'sip.conf'\n" \
-        "output-file defaults to 'res_sip.conf'"
-    parser = optparse.OptionParser(usage=usage)
-    parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX,
-                      help='output prefix for include files')
-
-    options, args = parser.parse_args()
-    PREFIX = options.prefix
-
-    sip_filename = args[0] if len(args) else 'sip.conf'
-    res_sip_filename = args[1] if len(args) == 2 else 'res_sip.conf'
-
-    return sip_filename, res_sip_filename
-
-if __name__ == "__main__":
-    sip_filename, res_sip_filename = cli_options()
-    # configuration parser for sip.conf
-    sip = astconfigparser.MultiOrderedConfigParser()
-    sip.read(sip_filename)
-    res_sip, non_mappings = convert(sip, res_sip_filename, dict())
-    write_res_sip(res_sip_filename, res_sip, non_mappings)