PSJIP - sip.conf to res_sip.conf script
[asterisk/asterisk.git] / contrib / scripts / sip_to_res_sip / sip_to_res_sip.py
1 #!/usr/bin/python
2
3 ###############################################################################
4 # TODO:
5 # (1) There is more work to do here, at least for the sip.conf items that
6 #     aren't currently parsed. An issue will be created for that.
7 # (2) All of the scripts should probably be passed through pylint and have
8 #     as many PEP8 issues fixed as possible
9 # (3) A public review is probably warranted at that point of the entire script
10 ###############################################################################
11
12 import optparse
13 import astdicts
14 import astconfigparser
15
16 PREFIX = 'res_sip_'
17
18 ###############################################################################
19 ### some utility functions
20 ###############################################################################
21 def section_by_type(section, res_sip, type):
22     """Finds a section based upon the given type, adding it if not found."""
23     try:
24         return astconfigparser.find_dict(
25             res_sip.section(section), 'type', type)
26     except LookupError:
27         # section for type doesn't exist, so add
28         sect = res_sip.add_section(section)
29         sect['type'] = type
30         return sect
31
32 def set_value(key=None, val=None, section=None, res_sip=None,
33               nmapped=None, type='endpoint'):
34     """Sets the key to the value within the section in res_sip.conf"""
35     def _set_value(k, v, s, r, n):
36         set_value(key if key else k, v, s, r, n, type)
37
38     # if no value or section return the set_value
39     # function with the enclosed key and type
40     if not val and not section:
41         return _set_value
42
43     # otherwise try to set the value
44     section_by_type(section, res_sip, type)[key] = \
45         val[0] if isinstance(val, list) else val
46
47 def merge_value(key=None, val=None, section=None, res_sip=None,
48                 nmapped=None, type='endpoint', section_to=None):
49     """Merge values from the given section with those from the default."""
50     def _merge_value(k, v, s, r, n):
51         merge_value(key if key else k, v, s, r, n, type, section_to)
52
53     # if no value or section return the merge_value
54     # function with the enclosed key and type
55     if not val and not section:
56         return _merge_value
57
58     # should return a single value section list
59     sect = sip.section(section)[0]
60     # for each merged value add it to res_sip.conf
61     for i in sect.get_merged(key):
62         set_value(key, i, section_to if section_to else section,
63                   res_sip, nmapped, type)
64
65 def is_in(s, sub):
66     """Returns true if 'sub' is in 's'"""
67     return s.find(sub) != -1
68
69 def non_mapped(nmapped):
70     def _non_mapped(section, key, val):
71         """Writes a non-mapped value from sip.conf to the non-mapped object."""
72         if section not in nmapped:
73             nmapped[section] = astconfigparser.Section()
74             if isinstance(val, list):
75                 for v in val:
76                     # since coming from sip.conf we can assume
77                     # single section lists
78                     nmapped[section][0][key] = v
79             else:
80                 nmapped[section][0][key] = val
81     return _non_mapped
82
83 ###############################################################################
84 ### mapping functions -
85 ###      define f(key, val, section) where key/val are the key/value pair to
86 ###      write to given section in res_sip.conf
87 ###############################################################################
88
89 def set_dtmfmode(key, val, section, res_sip, nmapped):
90     """Sets the dtmfmode value.  If value matches allowable option in res_sip
91        then map it, otherwise set it to none.
92     """
93     # available res_sip.conf values: frc4733, inband, info, none
94     if val != 'inband' or val != 'info':
95         nmapped(section, key, val + " ; did not fully map - set to none")
96         val = 'none'
97     set_value(key, val, section, res_sip, nmapped)
98
99 def from_nat(key, val, section, res_sip, nmapped):
100     """Sets values from nat into the appropriate res_sip.conf options."""
101     # nat from sip.conf can be comma separated list of values:
102     # yes/no, [auto_]force_rport, [auto_]comedia
103     if is_in(val, 'yes'):
104         set_value('rtp_symmetric', 'yes', section, res_sip, nmapped)
105         set_value('rewrite_contact', 'yes', section, res_sip, nmapped)
106     if is_in(val, 'comedia'):
107         set_value('rtp_symmetric', 'yes', section, res_sip, nmapped)
108     if is_in(val, 'force_rport'):
109         set_value('force_rport', 'yes', section, res_sip, nmapped)
110         set_value('rewrite_contact', 'yes', section, res_sip, nmapped)
111
112 def set_timers(key, val, section, res_sip, nmapped):
113     """Sets the timers in res_sip.conf from the session-timers option
114        found in sip.conf.
115     """
116     # res_sip.conf values can be yes/no, required, always
117     if val == 'originate':
118         set_value('timers', 'always', section, res_sip, nmapped)
119     elif val == 'accept':
120         set_value('timers', 'required', section, res_sip, nmapped)
121     elif val == 'never':
122         set_value('timers', 'no', section, res_sip, nmapped)
123     else:
124         set_value('timers', 'yes', section, res_sip, nmapped)
125
126 def set_direct_media(key, val, section, res_sip, nmapped):
127     """Maps values from the sip.conf comma separated direct_media option
128        into res_sip.conf direct_media options.
129     """
130     if is_in(val, 'yes'):
131         set_value('direct_media', 'yes', section, res_sip, nmapped)
132     if is_in(val, 'update'):
133         set_value('direct_media_method', 'update', section, res_sip, nmapped)
134     if is_in(val, 'outgoing'):
135         set_value('directed_media_glare_mitigation', 'outgoing', section, res_sip, nmapped)
136     if is_in(val, 'nonat'):
137         set_value('disable_directed_media_on_nat','yes', section, res_sip, nmapped)
138     if (val == 'no'):
139         set_value('direct_media', 'no', section, res_sip, nmapped)
140
141 def from_sendrpid(key, val, section, res_sip, nmapped):
142     """Sets the send_rpid/pai values in res_sip.conf."""
143     if val == 'yes' or val == 'rpid':
144         set_value('send_rpid', 'yes', section, res_sip, nmapped)
145     elif val == 'pai':
146         set_value('send_pai', 'yes', section, res_sip, nmapped)
147
148 def set_media_encryption(key, val, section, res_sip, nmapped):
149     """Sets the media_encryption value in res_sip.conf"""
150     if val == 'yes':
151         set_value('media_encryption', 'sdes', section, res_sip, nmapped)
152
153 def from_recordfeature(key, val, section, res_sip, nmapped):
154     """If record on/off feature is set to automixmon then set
155        one_touch_recording, otherwise it can't be mapped.
156     """
157     if val == 'automixmon':
158         set_value('one_touch_recording', 'yes', section, res_sip, nmapped)
159     else:
160         nmapped(section, key, val + " ; could not be fully mapped")
161
162 def from_progressinband(key, val, section, res_sip, nmapped):
163     """Sets the inband_progress value in res_sip.conf"""
164     # progressinband can = yes/no/never
165     if val == 'never':
166         val = 'no'
167     set_value('inband_progress', val, section, res_sip, nmapped)
168
169 def from_host(key, val, section, res_sip, nmapped):
170     """Sets contact info in an AOR section in in res_sip.conf using 'host'
171        data from sip.conf
172     """
173     # all aors have the same name as the endpoint so makes
174     # it easy to endpoint's 'aors' value
175     set_value('aors', section, section, res_sip, nmapped)
176     if val != 'dynamic':
177         set_value('contact', val, section, res_sip, nmapped, 'aor')
178     else:
179         set_value('max_contacts', 1, section, res_sip, nmapped, 'aor')
180
181 def from_subscribemwi(key, val, section, res_sip, nmapped):
182     """Checks the subscribemwi value in sip.conf.  If yes places the
183        mailbox value in mailboxes within the endpoint, otherwise puts
184        it in the aor.
185     """
186     mailboxes = sip.get('mailbox', section, res_sip)
187     type = 'endpoint' if val == 'yes' else 'aor'
188     set_value('mailboxes', mailboxes, section, res_sip, nmapped, type)
189
190 ###############################################################################
191
192 # options in res_sip.conf on an endpoint that have no sip.conf equivalent:
193 # type, rtp_ipv6, 100rel, trust_id_outbound, aggregate_mwi,
194 # connected_line_method
195
196 # known sip.conf peer keys that can be mapped to a res_sip.conf section/key
197 peer_map = [
198     # sip.conf option      mapping function     res_sip.conf option(s)
199     ###########################################################################
200     ['context',            set_value],
201     ['dtmfmode',           set_dtmfmode],
202     ['disallow',           merge_value],
203     ['allow',              merge_value],
204     ['nat',                from_nat],            # rtp_symmetric, force_rport,
205                                                  # rewrite_contact
206     ['icesupport',         set_value('ice_support')],
207     ['autoframing',        set_value('use_ptime')],
208     ['outboundproxy',      set_value('outbound_proxy')],
209     ['mohsuggest',         set_value],
210     ['session-timers',     set_timers],          # timers
211     ['session-minse',      set_value('timers_min_se')],
212     ['session-expires',    set_value('timers_sess_expires')],
213     ['externip',           set_value('external_media_address')],
214     ['externhost',         set_value('external_media_address')],
215     # identify_by ?
216     ['directmedia',        set_direct_media],    # direct_media
217                                                  # direct_media_method
218                                                  # directed_media_glare_mitigation
219                                                  # disable_directed_media_on_nat
220     ['callerid',           set_value],           # callerid
221     ['callingpres',        set_value('callerid_privacy')],
222     ['cid_tag',            set_value('callerid_tag')],
223     ['trustpid',           set_value('trust_id_inbound')],
224     ['sendrpid',           from_sendrpid],       # send_pai, send_rpid
225     ['send_diversion',     set_value],
226     ['encrpytion',         set_media_encryption],
227     ['use_avpf',           set_value],
228     ['recordonfeature',    from_recordfeature],  # automixon
229     ['recordofffeature',   from_recordfeature],  # automixon
230     ['progressinband',     from_progressinband], # in_band_progress
231     ['callgroup',          set_value],
232     ['pickupgroup',        set_value],
233     ['namedcallgroup',     set_value],
234     ['namedpickupgroup',   set_value],
235     ['busylevel',          set_value('devicestate_busy_at')],
236
237 ############################ maps to an aor ###################################
238
239     ['host',               from_host],           # contact, max_contacts
240     ['subscribemwi',       from_subscribemwi],   # mailboxes
241     ['qualifyfreq',        set_value('qualify_frequency', type='aor')],
242
243 ############################# maps to auth#####################################
244 #        type = auth
245 #        username
246 #        password
247 #        md5_cred
248 #        realm
249 #        nonce_lifetime
250 #        auth_type
251 ######################### maps to acl/security ################################
252
253     ['permit',             merge_value(type='security', section_to='acl')],
254     ['deny',               merge_value(type='security', section_to='acl')],
255     ['acl',                merge_value(type='security', section_to='acl')],
256     ['contactpermit',      merge_value(type='security', section_to='acl')],
257     ['contactdeny',        merge_value(type='security', section_to='acl')],
258     ['contactacl',         merge_value(type='security', section_to='acl')],
259
260 ########################### maps to transport #################################
261 #        type = transport
262 #        protocol
263 #        bind
264 #        async_operations
265 #        ca_list_file
266 #        cert_file
267 #        privkey_file
268 #        password
269 #        external_signaling_address - externip & externhost
270 #        external_signaling_port
271 #        external_media_address
272 #        domain
273 #        verify_server
274 #        verify_client
275 #        require_client_cert
276 #        method
277 #        cipher
278 #        localnet
279 ######################### maps to domain_alias ################################
280 #        type = domain_alias
281 #        domain
282 ######################### maps to registration ################################
283 #        type = registration
284 #        server_uri
285 #        client_uri
286 #        contact_user
287 #        transport
288 #        outbound_proxy
289 #        expiration
290 #        retry_interval
291 #        max_retries
292 #        auth_rejection_permanent
293 #        outbound_auth
294 ########################### maps to identify ##################################
295 #        type = identify
296 #        endpoint
297 #        match
298 ]
299
300 def map_peer(sip, section, res_sip, nmapped):
301     for i in peer_map:
302         try:
303             # coming from sip.conf the values should mostly be a list with a
304             # single value.  In the few cases that they are not a specialized
305             # function (see merge_value) is used to retrieve the values.
306             i[1](i[0], sip.get(section, i[0])[0], section, res_sip, nmapped)
307         except LookupError:
308             pass # key not found in sip.conf
309
310 def find_non_mapped(sections, nmapped):
311     for section, sect in sections.iteritems():
312         try:
313             # since we are pulling from sip.conf this should always
314             # be a single value list
315             sect = sect[0]
316             # loop through the section and store any values that were not mapped
317             for key in sect.keys(True):
318                 for i in peer_map:
319                     if i[0] == key:
320                         break;
321                 else:
322                     nmapped(section, key, sect[key])
323         except LookupError:
324             pass
325
326 def convert(sip, filename, non_mappings):
327     res_sip = astconfigparser.MultiOrderedConfigParser()
328     non_mappings[filename] = astdicts.MultiOrderedDict()
329     nmapped = non_mapped(non_mappings[filename])
330     for section in sip.sections():
331         if section == 'authentication':
332             pass
333         else:
334             map_peer(sip, section, res_sip, nmapped)
335
336     find_non_mapped(sip.defaults(), nmapped)
337     find_non_mapped(sip.sections(), nmapped)
338
339     for key, val in sip.includes().iteritems():
340         res_sip.add_include(PREFIX + key, convert(val, PREFIX + key, non_mappings)[0])
341     return res_sip, non_mappings
342
343 def write_res_sip(filename, res_sip, non_mappings):
344     try:
345         with open(filename, 'wt') as fp:
346             fp.write(';--\n')
347             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
348             fp.write('Non mapped elements start\n')
349             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n\n')
350             astconfigparser.write_dicts(fp, non_mappings[filename])
351             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
352             fp.write('Non mapped elements end\n')
353             fp.write(';;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;\n')
354             fp.write('--;\n\n')
355             # write out include file(s)
356             for key, val in res_sip.includes().iteritems():
357                 write_res_sip(key, val, non_mappings)
358                 fp.write('#include "%s"\n' % key)
359             fp.write('\n')
360             # write out mapped data elements
361             astconfigparser.write_dicts(fp, res_sip.defaults())
362             astconfigparser.write_dicts(fp, res_sip.sections())
363
364     except IOError:
365         print "Could not open file ", filename, " for writing"
366
367 ###############################################################################
368
369 def cli_options():
370     global PREFIX
371     usage = "usage: %prog [options] [input-file [output-file]]\n\n" \
372         "input-file defaults to 'sip.conf'\n" \
373         "output-file defaults to 'res_sip.conf'"
374     parser = optparse.OptionParser(usage=usage)
375     parser.add_option('-p', '--prefix', dest='prefix', default=PREFIX,
376                       help='output prefix for include files')
377
378     options, args = parser.parse_args()
379     PREFIX = options.prefix
380
381     sip_filename = args[0] if len(args) else 'sip.conf'
382     res_sip_filename = args[1] if len(args) == 2 else 'res_sip.conf'
383
384     return sip_filename, res_sip_filename
385
386 if __name__ == "__main__":
387     sip_filename, res_sip_filename = cli_options()
388     # configuration parser for sip.conf
389     sip = astconfigparser.MultiOrderedConfigParser()
390     sip.read(sip_filename)
391     res_sip, non_mappings = convert(sip, res_sip_filename, dict())
392     write_res_sip(res_sip_filename, res_sip, non_mappings)