Browse files

Moved more opensips code out to ccnq3-opensips-latest.

  • Loading branch information...
1 parent 31f6bf3 commit 41b1ac9633bea32dd651620f15ac621c676ee422 @shimaore committed Feb 17, 2014
Showing with 0 additions and 1,466 deletions.
  1. +0 −3 applications/emergency/README.md
  2. +0 −63 applications/emergency/agents/emergency.coffee
  3. +0 −1 applications/emergency/agents/opensips
  4. +0 −22 applications/emergency/package.json
  5. +0 −7 applications/opensips/README.md
  6. +0 −2 applications/opensips/agents/TODO
  7. +0 −18 applications/opensips/agents/aggregate.coffee
  8. +0 −1 applications/opensips/agents/opensips
  9. +0 −86 applications/opensips/agents/opensips.coffee
  10. +0 −11 applications/opensips/couchapps/compact.coffee
  11. +0 −25 applications/opensips/couchapps/install.coffee
  12. +0 −24 applications/opensips/couchapps/location.coffee
  13. +0 −154 applications/opensips/couchapps/opensips.coffee
  14. +0 −172 applications/opensips/couchapps/quote.coffee
  15. +0 −108 applications/opensips/node/opensips.coffee.md
  16. +0 −28 applications/opensips/package.json
  17. +0 −6 applications/opensips/zappa/README
  18. +0 −260 applications/opensips/zappa/main.coffee
  19. +0 −6 applications/registrant/README.md
  20. +0 −15 applications/registrant/agents/amqp-listener.coffee
  21. +0 −68 applications/registrant/agents/api.coffee
  22. +0 −1 applications/registrant/agents/opensips
  23. +0 −13 applications/registrant/agents/opensips-command.coffee
  24. +0 −21 applications/registrant/agents/params.coffee
  25. +0 −35 applications/registrant/agents/registrant.coffee
  26. +0 −23 applications/registrant/package.json
  27. +0 −2 common/munin/plugin-conf.d/ccnq3-munin-opensips
  28. +0 −25 common/munin/plugins/opensips_dialogs
  29. +0 −24 common/munin/plugins/opensips_registered
  30. +0 −219 test/10000_opensips_compiles.coffee.md
  31. +0 −3 test/Makefile
  32. +0 −20 test/test_opensips.coffee.md
View
3 applications/emergency/README.md
@@ -1,3 +0,0 @@
-CCNQ3 Emergency Call Router (France)
-
-Install on: a server running OpenSIPS; will start a separate OpenSIPS instance for 302 emergency calls redirection.
View
63 applications/emergency/agents/emergency.coffee
@@ -1,63 +0,0 @@
-#!/usr/bin/env coffee
-
-fs = require 'fs'
-util = require 'util'
-pico = require 'pico'
-spawn = require('child_process').spawn
-
-dgram = require 'dgram'
-opensips_command = (port,command) ->
- # Connect to the MI datagram port
- # Send command
- message = new Buffer(command)
- client = dgram.createSocket "udp4"
- client.send message, 0, message.length, port, "127.0.0.1", (err, bytes) ->
- # FIXME we might receive data (and might want to report it)
- # FIXME the proper way to do so is to collect it then implement a timeout (say 1s)
- # to declare the UDP session over with.
- client.close()
-
-process_changes = (port,command,cfg) ->
- switch command
- when 'stop'
- opensips_command port, ":kill:\n"
- when 'start'
- spawn '/usr/sbin/opensips', [ '-f', cfg ], stdio:'ignore'
-
-
-require('ccnq3').config (config) ->
-
- provisioning = pico config.provisioning.local_couchdb_uri
-
- handler = (p) ->
-
- if not p.emergency? then return
-
- base_path = "./opensips"
- model = 'emergency'
-
- params = {}
- for _ in ['default.json',"#{model}.json"]
- do (_) ->
- data = JSON.parse fs.readFileSync "#{base_path}/#{_}"
- params[k] = data[k] for own k of data
-
- params.opensips_base_lib = base_path
- params.notify_via_rabbitmq ?= "#{config.amqp}/logging".replace(/^amqp/,'rabbitmq') if config.amqp?
-
- require("#{base_path}/compiler.coffee") params
-
- # Process any MI commands
- if p.sip_commands?.emergency?
- process_changes params.mi_port, p.sip_commands.emergency, params.runtime_opensips_cfg
-
- # Start with the current configuration
- handler config
-
- options =
- since_name: "emergency #{config.host}"
- filter_name: "host/hostname"
- filter_params:
- hostname: config.host
-
- provisioning.monitor options, handler
View
1 applications/emergency/agents/opensips
View
22 applications/emergency/package.json
@@ -1,22 +0,0 @@
-{
- "name": "ccnq3_emergency",
- "version": "0.0.1",
- "description": "Emergency server for CCNQ3",
- "keywords": "",
- "author": { "name": "Stephane Alnet", "email": "stephane@shimaore.net" },
- "config": {
- "file": "/etc/ccnq3/host.json"
- },
- "scripts": {
- "start": "daemon -n ccnq3_emergency -o daemon.debug -D \"`pwd`/agents\" -r -- ./emergency.coffee; sleep 3; /usr/sbin/opensips -f /tmp/emergency.cfg || true"
- , "stop": "daemon -n ccnq3_emergency -o daemon.debug --stop; echo ':kill:' | nc -u -t 1 127.0.0.1 30012 || true"
- },
- "dependencies": {
- "ccnq3": ">=0.3.3"
- , "pico": ">=0.1.0"
- },
- "engines": { "node": ">=0.4.7", "coffee": ">=1.1.1" },
-
- "private": true,
- "ok": true
-}
View
7 applications/opensips/README.md
@@ -1,7 +0,0 @@
-OpenSIPS Server Management
-
-Only provide services for the system default OpenSIPS instance (/etc/init.d/opensips).
-Other instances (emergency server, registrant, ..) are managed by their own individual
-packages.
-
-Install on: any server running OpenSIPS.
View
2 applications/opensips/agents/TODO
@@ -1,2 +0,0 @@
-Modify the agent so that it starts / stops opensips.
-Modify the installer so that the default opensips (using /etc/opensips) is disabled.
View
18 applications/opensips/agents/aggregate.coffee
@@ -1,18 +0,0 @@
-# aggregate.coffee
-# (c) 2012 Stephane Alnet
-# License: AGPL3+
-
-pico = require 'pico'
-
-replicate = (config) ->
-
- return unless config.opensips_proxy?.usrloc_aggregate_uri?
-
- # Replicate the local "location" database
- source_uri = config.opensips_proxy?.usrloc_uri ? 'http://127.0.0.1:5984/location'
- # into the global one
- target_uri = config.opensips_proxy?.usrloc_aggregate_uri
-
- pico.replicate source_uri, target_uri, config.replicate_interval
-
-module.exports = replicate
View
1 applications/opensips/agents/opensips
View
86 applications/opensips/agents/opensips.coffee
@@ -1,86 +0,0 @@
-#!/usr/bin/env coffee
-###
-(c) 2010 Stephane Alnet
-Released under the AGPL3 license
-###
-
-fs = require 'fs'
-dgram = require 'dgram'
-
-opensips_command = (port,command) ->
- # Connect to the MI datagram port
- # Send command
- message = new Buffer(command)
- client = dgram.createSocket "udp4"
- client.on 'error', (e) ->
- console.error "Socket failed"
- client.send message, 0, message.length, port, "127.0.0.1", (err, bytes) ->
- # FIXME we might receive data (and might want to report it)
- # FIXME the proper way to do so is to collect it then implement a timeout (say 1s)
- # to declare the UDP session over with.
- client.close()
-
-# Quoting from the documentation for OpenSIPS' mi_datagram module:
-# The external commands issued via DATAGRAM interface must follow
-# the following syntax:
-# * request = first_line (argument '\n')*
-# * first_line = ':'command_name':''\n'
-# * argument = (arg_name '::' (arg_value)? ) | (arg_value)
-# * arg_name = not-quoted_string
-# * arg_value = not-quoted_string | '"' string '"'
-# * not-quoted_string = string - {',",\n,\r}
-
-process_changes = (port,command) ->
- switch command
- when 'reload routes'
- opensips_command port, ":dr_reload:\n"
-
-# Main
-
-util = require 'util'
-pico = require 'pico'
-
-require('ccnq3').config (config) ->
-
- # Aggregate back towards the main database if requested.
- require('./aggregate') config
-
- handler = (p) ->
-
- # If the host does not support OpenSIPS then skip this update.
- return unless p.opensips?.model?
-
- # 1. Generate new configuration files
- base_path = "./opensips"
-
- params = {}
- for _ in ['default.json',"#{p.opensips.model}.json"]
- do (_) ->
- data = JSON.parse fs.readFileSync "#{base_path}/#{_}"
- params[k] = data[k] for own k of data
-
- params[k] = p.opensips[k] for own k of p.opensips
-
- params.opensips_base_lib = base_path
- params.sip_domain_name = config.sip_domain_name
- params.notify_via_rabbitmq ?= "#{config.amqp}/logging".replace(/^amqp/,'rabbitmq') if config.amqp?
-
- require("#{base_path}/compiler.coffee") params
-
- # 2. Process any MI commands
- if p.sip_commands?.opensips?
- process_changes params.mi_port, p.sip_commands.opensips
-
- # First start with the current configuration.
- handler config
-
- # Monitor for changes and commands.
- src = pico config.provisioning.local_couchdb_uri
-
- options =
- since_name: "opensips #{config.host}"
- filter_name: "host/hostname"
- filter_params:
- hostname: config.host
-
- src.monitor options, handler
View
11 applications/opensips/couchapps/compact.coffee
@@ -1,11 +0,0 @@
-#!/usr/bin/env coffee
-
-pico = require 'pico'
-
-require('ccnq3').config (config)->
-
- location_uri = config.opensips_proxy?.usrloc_uri ? 'http://127.0.0.1:5984/location'
- location = pico location_uri
- location.compact pico.log
- location.compact_design 'opensips', pico.log
- location.compact_design 'location', pico.log
View
25 applications/opensips/couchapps/install.coffee
@@ -1,25 +0,0 @@
-#!/usr/bin/env coffee
-
-couchapp = require 'couchapp'
-pico = require 'pico'
-
-push_script = (uri, script,cb) ->
- couchapp.createApp require("./#{script}"), uri, (app)-> app.push(cb)
-
-require('ccnq3').config (config)->
-
- # These only get installed on host running OpenSIPS.
-
- # Install the views so that opensips-proxy might work.
- provisioning_uri = config.provisioning.local_couchdb_uri
- provisioning = pico provisioning_uri
- push_script provisioning_uri, 'opensips'
-
- # Create this database (local to the host, normally)
- location_uri = config.opensips_proxy?.usrloc_uri ? 'http://127.0.0.1:5984/location'
- location = pico location_uri
- location.create ->
- push_script location_uri, 'opensips' # for CommonJS
- push_script location_uri, 'location'
- location.request.put '_revs_limit',body:"2", (e,r,b) =>
- if e? then console.dir failure error:e, when:"set revs_limit for #{location_uri}"
View
24 applications/opensips/couchapps/location.coffee
@@ -1,24 +0,0 @@
-###
-(c) 2010 Stephane Alnet
-Released under the Affero GPL3 license or above
-###
-
-p_fun = (f) -> '('+f+')'
-
-ddoc =
- _id: '_design/location'
- language: 'javascript'
- views: {}
- lists: {} # http://guide.couchdb.org/draft/transforming.html
- shows: {} # http://guide.couchdb.org/draft/show.html
- filters: {} # used by _changes
- rewrites: [] # http://blog.couchone.com/post/443028592/whats-new-in-apache-couchdb-0-11-part-one-nice-urls
- lib: {}
-
-module.exports = ddoc
-
-# Too bad we can't use _all_docs with a list.
-ddoc.views.all =
- map: p_fun (doc) ->
- if doc.callid?
- emit null, doc
View
154 applications/opensips/couchapps/opensips.coffee
@@ -1,154 +0,0 @@
-p_fun = (f) -> '('+f+')'
-
-ddoc =
- _id: '_design/opensips'
- language: 'javascript'
- views: {}
- lists: {}
- shows: {}
- filters: {}
- updates: {}
- rewrites: []
- lib: {}
-
-fs = require 'fs'
-coffee = require 'coffee-script'
-
-module.exports = ddoc
-
-ddoc.lib.quote = coffee.compile ''+fs.readFileSync './quote.coffee'
-
-ddoc.shows.format = p_fun (doc,req) ->
- quote = require 'lib/quote'
- body = ''
- if doc?
- t = req.query.t
- c = req.query.c
- types = quote.column_types[t]
- columns = c.split ','
- body = quote.first_line(types,columns) + quote.value_line(types,t,doc,columns)
- return {
- headers:
- 'Content-Type': 'text/plain'
- body:
- body
- }
-
-ddoc.lists.format = p_fun (head,req) ->
- quote = require 'lib/quote'
- start {
- headers:
- 'Content-Type': 'text/plain'
- }
- t = req.query.t
- c = req.query.c
- types = quote.column_types[t]
- columns = c.split ','
- started = false
- while row = getRow()
- do (row) ->
- if not started
- send quote.first_line(types,columns)
- started = true
- send quote.value_line types, t, row.value, columns
- if not started
- send ''
- return # KeepMe!
-
-ddoc.views.gateways_by_domain =
- map: p_fun (doc) ->
-
- if doc.type? and doc.type is 'gateway'
- emit doc.sip_domain_name, doc
-
- if doc.type? and doc.type is 'host' and doc.sip_profiles?
- for name, rec of doc.sip_profiles
- do (rec) ->
- # for now we only generate for egress gateways
- if rec.egress_gwid?
- ip = rec.egress_sip_ip ? rec.ingress_sip_ip
- port = rec.egress_sip_port ? rec.ingress_sip_port+10000
- emit doc.sip_domain_name,
- account: ""
- gwid: rec.egress_gwid
- address: ip+':'+port
- gwtype: 0
- probe_mode: 0
- strip: 0
-
- return
-
-ddoc.views.rules_by_domain =
- map: p_fun (doc) ->
- if doc.type? and doc.type is 'rule'
- emit doc.sip_domain_name, doc
- return
-
-ddoc.views.carriers_by_host =
- map: p_fun (doc) ->
- if doc.type? and doc.type is 'carrier'
- emit doc.host, doc
- return
-
-## Registrant view and list
-
-ddoc.views.registrant_by_host =
- map: p_fun (doc) ->
-
- if doc.type? and doc.type is 'number' and doc.registrant_password? and doc.registrant_host? and doc.registrant_remote_ipv4?
- value =
- registrar: "sip:#{doc.registrant_remote_ipv4}"
- # proxy: null
- aor: "sip:00#{doc.number}@#{doc.registrant_remote_ipv4}"
- # third_party_registrant: null
- username: "00#{doc.number}"
- password: doc.registrant_password
- # binding_URI: "sip:00#{doc.number}@#{p.interfaces.primary.ipv4 ? p.host}:5070"
- # binding_params: null
- expiry: doc.registrant_expiry ? 86400
- # forced_socket: null
-
- hosts = doc.registrant_host
- if typeof hosts is 'string'
- hosts = [hosts]
-
- for host in hosts
- [hostname,port] = host.split /:/
- port ?= 5070
- value.binding_URI = "sip:00#{doc.number}@#{hostname}:#{port}"
- emit [hostname,1], value
-
- if doc.type? and doc.type is 'host' and doc.applications.indexOf('applications/registrant') >= 0
- # Make sure these records show up at the top
- emit [doc.host,0], interfaces:doc.interfaces
-
- return
-
-ddoc.lists.registrant = p_fun (head,req) ->
- quote = require 'lib/quote'
- start {
- headers:
- 'Content-Type': 'text/plain'
- }
- t = req.query.t
- c = req.query.c
- types = quote.column_types[t]
- columns = c.split ','
- hosts = {}
- started = false
- while row = getRow()
- do (row) ->
- host =row.key[0]
- if row.value.interfaces?
- hosts[host] = row.value
- else
- ipv4 = hosts[host]?.interfaces.primary?.ipv4
- if ipv4?
- row.value.binding_URI = row.value.binding_URI.replace host, ipv4
- if not started
- send quote.first_line(types,columns)
- started = true
- send quote.value_line types, t, row.value, columns
- if not started
- send ''
- return # KeepMe!
View
172 applications/opensips/couchapps/quote.coffee
@@ -1,172 +0,0 @@
- # db_dbase.c lists: int, double, string, str, blob, date; str and blob are equivalent for this interface.
- column_types =
- usrloc:
- username: 'string'
- domain: 'string'
- contact: 'string'
- received: 'string'
- path: 'string'
- expires: 'date'
- q: 'double'
- callid: 'string'
- cseq: 'int'
- last_modified: 'date'
- flags: 'int'
- cflags: 'string'
- user_agent: 'string'
- socket: 'string'
- methods: 'int'
- sip_instance: 'string'
- attr: 'string'
- version:
- table_name: 'string'
- table_version: 'int'
- dr_gateways:
- id: 'int'
- gwid: 'string'
- type: 'int'
- address: 'string'
- strip: 'int'
- pri_prefix: 'string'
- attrs: 'string'
- probe_mode: 'int'
- socket: 'string'
- state: 'int'
- dr_rules:
- ruleid: 'int'
- # keys
- groupid: 'string'
- prefix: 'string'
- timerec: 'string'
- priority: 'int'
- # others
- routeid: 'string'
- gwlist: 'string'
- attrs: 'string'
- dr_carriers:
- id: 'int'
- carrierid: 'string'
- gwlist: 'string'
- flags: 'int'
- attrs: 'string'
- state: 'int'
- dr_groups:
- username:'string'
- domain:'string'
- groupid:'int'
- domain:
- domain: 'string'
- subscriber:
- username: 'string'
- domain: 'string'
- password: 'string'
- ha1: 'string'
- ha1b: 'string'
- rpid: 'string'
- avpops:
- uuid: 'string'
- username: 'string'
- domain: 'string'
- attribute: 'string'
- type: 'int'
- value: 'string'
- location:
- username:'string'
- domain:'string'
- contact:'string'
- received:'string'
- path:'string'
- expires:'date'
- q:'double'
- callid:'string'
- cseq:'int'
- last_modified:'date'
- flags:'int'
- cflags:'int'
- user_agent:'string'
- socket:'string'
- methods:'int'
- registrant:
- registrar:'string'
- proxy:'string'
- aor:'string'
- third_party_registrant:'string'
- username:'string'
- password:'string'
- binding_URI:'string'
- binding_params:'string'
- expiry:'int'
- forced_socket:'string'
-
- quoted_value = (t,x) ->
- # No value: no quoting.
- if not x?
- return ''
-
- # Expects numerical types => no quoting.
- if t is 'int' or t is 'double'
- # assert(parseInt(x).toString is x) if t is 'int' and typeof x isnt 'number'
- # assert(parseFloat(x).toString is x) if t is 'double' and typeof x isnt 'number'
- return x
-
- # assert(t is 'string')
- if typeof x is 'number'
- x = x.toString()
- if typeof x isnt 'string'
- x = JSON.stringify x
- # assert typeof x is 'string'
-
- # Assumes quote_delimiter = '"'
- return '"'+x.replace(/"/g, '""')+'"'
-
-
- field_delimiter = "\t"
- row_delimiter = "\n"
-
- line = (a) ->
- a.join(field_delimiter) + row_delimiter
-
- first_line = (types,c)->
- return line( types[col] for col in c )
-
- value_line = (types,n,hash,c)->
- if n is 'avpops'
- # Shorten output a little since these are not used
- # (avpops has a limited input buffer size).
- delete hash._id
- delete hash._rev
- delete hash._revisions
- # Build a proper "avpops" response.
- hash =
- value: hash
- attribute: hash.type
- type: 2
- if n is 'dr_rules'
- hash.ruleid ?= 1
- hash.routeid ?= ""
- hash.timerec ?= ""
- hash.priority ?= 1
- hash.attrs ?= '{}'
- hash.attrs = JSON.stringify(hash.attrs) unless typeof hash.attrs is 'string'
- if n is 'dr_carriers'
- hash.id ?= 1
- hash.flags ?= 0
- hash.attrs ?= '{}'
- hash.attrs = JSON.stringify(hash.attrs) unless typeof hash.attrs is 'string'
- hash.state ?= 0
- if n is 'dr_gateways'
- hash.id ?= 1
- hash.gwtype ?= 0
- hash.type = hash.gwtype
- hash.probe_mode ?= 0
- hash.strip ?= 0
- hash.attrs ?= '{}'
- hash.attrs = JSON.stringify(hash.attrs) unless typeof hash.attrs is 'string'
- hash.state ?= 0
- if n is 'dr_groups'
- hash.groupid = hash.outbound_route # alternatively set the "drg_grpid_col" parameter to "outbound_route"
- return line( quoted_value(types[col], hash[col]) for col in c )
-
- exports.column_types = column_types
- exports.first_line = first_line
- exports.value_line = value_line
View
108 applications/opensips/node/opensips.coffee.md
@@ -1,108 +0,0 @@
-Overview
-========
-
-This is a CCNQ3 application
-
- ccnq3 = require 'ccnq3'
-
-which implements an AMQP agent for OpenSIPS.
-
- ccnq3.amqp (c) ->
-
-Registration Status
-===================
-
-The agent provides registration status for a given user.
-
- c.exchange 'registration', {type:'topic',durable:true,autoDelete:false}, (e) ->
-
-The request comes as an AMQP event.
-
- c.queue "registration-request-#{config.host}", (q) ->
- q.bind e, 'request'
- q.subscribe (request,headers,info) ->
-
-The request contains the name of the queue to which we should reply.
-
- reply_to = info.replyTo
-
-OpenSIPS is queried for the response.
-
- registration_status config, request, (response) ->
-
-The response is sent back over AMQP.
-
- e.publish reply_to, response
-
-Registration Status Handler
----------------------------
-
-This part actual sends the command to OpenSIPS and parses the response.
-
- registration_status = (config,request,cb) ->
-
-FIXME: The default `mi_port` is actually recorded by default in the opensisp.model.
-
- mi_port = config.opensips.mi_port ? 30000
-
-Quoting from the documentation for OpenSIPS' `mi_datagram` module:
-The external commands issued via DATAGRAM interface must follow the following syntax:
-* `request = first_line (argument '\n')*`
-* `first_line = ':'command_name':''\n'`
-* `argument = (arg_name '::' (arg_value)? ) | (arg_value)`
-* `arg_name = not-quoted_string`
-* `arg_value = not-quoted_string | '"' string '"'`
-* `not-quoted_string = string - {',",\n,\r}`
-
- command = ccnq3.opensips.command 'ul_show_contact',
- 'location',
- request.username
-
- ccnq3.opensips.mi null, mi_port, command, (error,response) ->
-
- if error
- return cb {error}
-
- result = ccnq3.opensips.parse response
-
- if result.error?
- return cb result
-
-If successful we get one or more Contact entries which look like
-```
-Contact:: <sip:..@...>;q=;expires=726;flags=..;cflags=..;socket=...;methods=...;user_agent=<...>
-```
-
-We rewrite them as objects
-
- clean_value = (s) ->
- return s if typeof s isnt 'string'
- if s[0] is '<' and s[s.length-1] is '>'
- s.substr(1,s.length-2)
- else
- s
-
- outcome = []
- for r in result.Contact
- [uri,params...] = r.value.split /;/
-
- o =
- uri: clean_value uri
- for p in params
- [key,value] = p.split /[=]/
- o[key] = clean_value value
-
- outcome.push o
-
- cb outcome
-
-Tools
-=====
-
-We need access to the local configuration,
-
- config = null
-
-it is loaded once at startup.
-
- ccnq3.config (c) -> config = c
View
28 applications/opensips/package.json
@@ -1,28 +0,0 @@
-{
- "name": "ccnq3_opensips",
- "version": "0.0.1",
- "description": "OpenSIPS support for CCNQ3",
- "keywords": "",
- "author": { "name": "Stephane Alnet", "email": "stephane@shimaore.net" },
- "config": {
- "file": "/etc/ccnq3/host.json"
- },
- "scripts": {
- "start": "daemon -n ccnq3_opensips -o daemon.debug -D \"`pwd`/agents\" -r -- ./opensips.coffee; daemon -n ccnq3_opensips_http_db -o daemon.debug -D \"`pwd`/zappa\" -r -- ./main.coffee"
- , "stop": "daemon -n ccnq3_opensips -o daemon.debug --stop; daemon -n ccnq3_opensips_http_db -o daemon.debug --stop"
- , "couchapps": "cd couchapps && ./install.coffee"
- , "compact": "cd couchapps && ./compact.coffee"
- },
- "dependencies": {
- "ccnq3": ">=0.5"
- , "zappajs": "0.4"
- , "couchapp": "*"
- , "coffee-script": ">=1.1.2"
- , "request": "~2.21.0"
- , "pico": ">=0.1.13"
- },
- "engines": { "node": ">=0.4.7", "coffee": ">=1.1.1" },
-
- "private": true,
- "ok": true
-}
View
6 applications/opensips/zappa/README
@@ -1,6 +0,0 @@
-This application's purpose is to provide a fast http-db server for OpenSIPS
-so that data can be served directly from CouchDB without the need for MySQL
-or PostgreSQL insertions.
-
-It essentially consists of a Zappa application which uses a replica of the provisioning
-database in order to provide http-db content to OpenSIPS.
View
260 applications/opensips/zappa/main.coffee
@@ -1,260 +0,0 @@
-#!/usr/bin/env coffee
-###
-(c) 2010 Stephane Alnet
-Released under the AGPL3 license
-###
-
-util = require 'util'
-qs = require 'querystring'
-request = require 'request'
-
-make_id = (t,n) -> [t,n].join ':'
-
-require('ccnq3').config (config)->
-
- config.opensips_proxy ?= {}
- config.opensips_proxy.port ?= 34340
- config.opensips_proxy.hostname ?= "127.0.0.1"
- config.opensips_proxy.usrloc_uri ?= "http://127.0.0.1:5984/location"
-
- zappa = require 'zappajs'
- zappa config.opensips_proxy.port, config.opensips_proxy.hostname, {config}, ->
-
- @use 'bodyParser'
-
- column_types =
- location:
- # keys
- username:'string'
- contact:'string'
- callid:'string'
- domain:'string'
- # non-keys
- expires:'date'
- q:'double'
- cseq:'int'
- flags:'int'
- cflags:'string'
- user_agent:'string'
- received:'string'
- path:'string'
- socket:'string'
- methods:'int'
- last_modified:'date'
- sip_instance:'string'
- attr:'string'
-
- unquote_value = (t,x) ->
-
- if not x?
- return x
-
- if t is 'int'
- return parseInt(x)
- if t is 'double'
- return parseFloat(x)
- # Not sure what the issue is, but we're getting garbage at the end of dates.
- if t is 'date'
- d = new Date(x)
- # Format expected by db_str2time() in db/db_ut.c
- # Note: This requires opensips to be started in UTC, assuming
- # toISOString() outputs using UTC (which it does in Node.js 0.4.11).
- # Our script ccnq3-opensips.postinst makes sure this is the case.
- return d.toISOString().replace 'T', ' '
-
- # string, blob, ...
- return x.toString()
-
- unquote_params = (k,v,table)->
- doc = {}
- names = k.split ','
- values = v.split ','
- types = column_types[table]
-
- doc[names[i]] = unquote_value(types[names[i]],values[i]) for i in [0..names.length]
-
- return doc
-
- _request = (that,loc) ->
- r = request loc, (e,r,b) ->
- if e?
- util.log loc + 'failed with error ' + util.inspect e
- r.pipe(that.response)
-
- _pipe = (that,base,t,id) ->
- loc = "#{base}/_design/opensips/_show/format/#{qs.escape id}?t=#{t}&c=#{qs.escape that.query.c}"
- _request that, loc
-
- pipe_req = (that,t,id) ->
- _pipe that, config.provisioning.local_couchdb_uri, t, id
-
- pipe_loc_req = (that,t,id) ->
- _pipe that, config.opensips_proxy.usrloc_uri, t, id
-
- _list = (that,base,t,view) ->
- loc = "#{base}/_design/opensips/_list/format/#{view}?t=#{t}&c=#{qs.escape that.query.c}"
- _request that, loc
-
- pipe_list = (that,t,view) ->
- _list that, config.provisioning.local_couchdb_uri, t, view
-
- pipe_loc_list = (that,t,view) ->
- _list that, config.opensips_proxy.usrloc_uri, t, view
-
- _list_key = (that,base,t,view,key,format) ->
- key = "\"#{key}\""
- loc = "#{base}/_design/opensips/_list/#{format}/#{view}?t=#{t}&c=#{qs.escape that.query.c}&key=#{qs.escape key}"
- _request that, loc
-
- pipe_list_key = (that,t,view,key,format='format') ->
- _list_key that, config.provisioning.local_couchdb_uri, t, view, key, format
-
- _list_keys = (that,base,t,view,key,format) ->
- startkey = """["#{key}"]"""
- endkey = """["#{key}",{}]"""
- loc = "#{base}/_design/opensips/_list/#{format}/#{view}?t=#{t}&c=#{qs.escape that.query.c}&startkey=#{qs.escape startkey}&endkey=#{qs.escape endkey}"
- _request that, loc
-
- pipe_list_keys = (that,t,view,key,format='format') ->
- _list_keys that, config.provisioning.local_couchdb_uri, t, view, key, format
-
-
- # Action!
- @get '/subscriber/': -> # auth_table
- if @query.k is 'username,domain'
- # Parse @v -- what is the actual format?
- [username,domain] = @query.v.split ","
- pipe_req @, 'subscriber', make_id('endpoint',"#{username}@#{domain}")
- return
-
- util.error "subscriber: not handled: #{@query.k}"
- @send ""
-
- @get '/location/': -> # usrloc_table
-
- if @query.k is 'username'
- pipe_loc_req @, 'usrloc', @query.v
- return
-
- if @query.k is 'username,domain'
- [username,domain] = @query.v.split ','
- pipe_loc_req @, 'usrloc', "#{username}@#{domain}"
- return
-
- if not @query.k?
- pipe_loc_list @, 'usrloc', 'location/all' # Can't use _all_docs
- return
-
- util.error "location: not handled: #{@query.k}"
- @send ""
-
- pico = require 'pico'
- loc_db = pico config.opensips_proxy.usrloc_uri
-
- @post '/location': ->
-
- doc = unquote_params(@body.k,@body.v,'location')
- # Note: this allows for easy retrieval, but only one location can be stored.
- # Use "callid" as an extra key parameter otherwise.
- doc._id = "#{doc.username}@#{doc.domain}"
-
- if @body.uk?
- update_doc = unquote_params(@body.uk,@body.uv,'location')
- doc[k] = v for k,v of update_doc
-
- if @body.query_type is 'insert' or @body.query_type is 'update'
-
- loc_db.rev doc._id, (e,r,h) =>
- doc._rev = h.rev if h?.rev?
- loc_db.put doc, (e,r,p) =>
- if e then util.error "location: error updating #{doc._id}"
- @send doc._id
- return
-
- if @body.query_type is 'delete'
-
- loc_db.rev doc._id, (e,r,h) =>
- if not h?.rev? then return @send ""
- doc._rev = h.rev
- loc_db.remove doc, (e) =>
- if e then util.error "location: error removing #{doc._id}"
- @send ""
- return
-
- util.error "location: not handled: #{util.inspect @req}"
- @send ""
-
- @get '/avpops/': ->
-
- if @query.k is 'uuid,attribute'
- [uuid,attribute] = @query.v.split ','
- pipe_req @, 'avpops', make_id attribute, uuid
- return
-
- if @query.k is 'username,domain,attribute'
- [username,domain,attribute] = @query.v.split ','
- pipe_req @, 'avpops', make_id(attribute,"#{username}@#{domain}")
- return
-
- util.error "avpops: not handled: #{@query.k}"
- @send ""
-
- @get '/dr_gateways/': ->
- if not @query.k?
- pipe_list_key @, 'dr_gateways', 'gateways_by_domain', config.sip_domain_name
- return
-
- util.error "dr_gateways: not handled: #{@query.k}"
- @send ""
-
- @get '/dr_rules/': -> # ?c=ruleid,groupid,prefix,timerec,priority,routeid,gwlist,attrs
- if not @query.k?
- pipe_list_key @, 'dr_rules', 'rules_by_domain', config.sip_domain_name
- return
-
- util.error "dr_rules: not handled: #{@query.k}"
- @send ""
-
- @get '/dr_groups/': ->
-
- if @query.k is 'username,domain'
- [username,domain] = @query.v.split ','
- # However we do not currently support "number@domain", so skip that.
- # (Compare to use_domain=0.)
- pipe_req @, 'dr_groups', make_id('number',username)
- return
-
- util.error "dr_groups: not handled: #{@query.k}"
- @send ""
-
- @get '/dr_carriers/': -> # id,gwlist
- if not @query.k?
- pipe_list_key @, 'dr_carriers', 'carriers_by_host', config.host
- return
-
- util.error "dr_carriers: not handled: #{@query.k}"
- @send ""
-
- @get '/registrant/': ->
- if not @query.k?
- pipe_list_keys @, 'registrant', 'registrant_by_host', config.host, 'registrant'
- return
-
- util.error "registrant: not handled: #{@query.k}"
- @send ""
-
- @get '/version/': ->
- if @query.k is 'table_name' and @query.c is 'table_version'
-
- # Versions for OpenSIPS 1.8.1
- versions =
- location: 1009
- subscriber: 7
- dr_gateways: 6
- dr_rules: 3
- registrant: 1
-
- return "int\n#{versions[@query.v]}\n"
-
- util.error "version not handled: #{util.inspect @req}"
- @send ""
View
6 applications/registrant/README.md
@@ -1,6 +0,0 @@
-OpenSIPS server providing batch registration to an upstream server.
-This is a registrant and inbound call forwarder for carrier-side SBCs.
-
-A separate opensips process is spawned which will register ourselves with a third-party provider, while inbound INVITE messages are routed to one (or more) carrier-sbcs.
-
-Install on: a server running OpenSIPS; will start a separate OpenSIPS instance for client registration.
View
15 applications/registrant/agents/amqp-listener.coffee
@@ -1,15 +0,0 @@
-ccnq3 = require 'ccnq3'
-process_commands = require './api'
-ccnq3.config (config) ->
- ccnq3.amqp (c) ->
- c.exchange 'commands', {type:'topic',durable:true,autoDelete:false}, (e) ->
- c.queue "commands-#{config.host}", (q) ->
-
- # Handle requests specific to this host.
- q.bind e, "request-#{config.host}"
- # Handle requests addressed to all hosts.
- q.bind e, "request"
-
- q.subscribe (request) ->
- process_commands request, (response) ->
- e.publish "response-#{request.reference}", response
View
68 applications/registrant/agents/api.coffee
@@ -1,68 +0,0 @@
-{ spawn } = require 'child_process'
-ccnq3 = require 'ccnq3'
-opensips_command = require './opensips-command'
-params = require './params'
-
-service = {}
-
-process_command = (port,command,cfg) ->
- kill_service = ->
- service[port].kill 'SIGKILL'
- service[port] = null
-
- stop_service = ->
- opensips_command port, ":kill:\n"
- if service[port]?
- setTimeout kill_service, 4000
- start_service = ->
- if service[port]?
- ccnq3.log "WARNING in start_service: service already running?"
- shared_megs = 1024
- pkg_megs = 512
- service[port] = spawn '/usr/sbin/opensips', [ '-m', shared_megs, '-M', pkg_megs, '-f', cfg ]
-
- switch command
- when 'stop'
- do stop_service
- when 'start'
- do start_service
- when 'restart'
- do stop_service
- setTimeout start_service, 5000
-
-module.exports = (command,cb) ->
- unless command.command?
- cb error:"`command` is a required parameter", arguments: command
-
- ccnq3.config (config) ->
- if command.port?
- switch command.command
- when 'restart registrant'
- c = 'restart'
- when 'start registrant'
- c = 'start'
- when 'stop registrant'
- c = 'stop'
- else
- cb error:"Invalid command", received:command
-
- p = params {proxy_port:command.port}, config
- process_command command.port+30000, c, p.runtime_opensips_cfg
- cb?()
-
- else
- switch command.command
- when 'restart all registrant'
- c = 'restart'
- when 'start all registrant'
- c = 'start'
- when 'stop all registrant'
- c = 'stop'
- else
- cb error:"Invalid command", received:command
-
- for r in config.registrants
- p = params r, config
- process_command command.port+30000, c, p.runtime_opensips_cfg
-
- cb?()
View
1 applications/registrant/agents/opensips
View
13 applications/registrant/agents/opensips-command.coffee
@@ -1,13 +0,0 @@
-dgram = require 'dgram'
-opensips_command = (port,command) ->
- # Connect to the MI datagram port
- # Send command
- message = new Buffer(command)
- client = dgram.createSocket "udp4"
- client.send message, 0, message.length, port, "127.0.0.1", (err, bytes) ->
- # FIXME we might receive data (and might want to report it)
- # FIXME the proper way to do so is to collect it then implement a timeout (say 1s)
- # to declare the UDP session over with.
- client.close()
-
-module.exports = opensips_command
View
21 applications/registrant/agents/params.coffee
@@ -1,21 +0,0 @@
-fs = require 'fs'
-module.exports = (p,config) ->
- config ?= p
- base_path = "./opensips"
- model = 'registrant'
-
- params = {}
- for _ in ['default.json',"#{model}.json"]
- do (_) ->
- data = require "#{base_path}/#{_}"
- params[k] = data[k] for own k of data
-
- params.opensips_base_lib = base_path
- params.notify_via_rabbitmq ?= "#{config.amqp}/logging".replace(/^amqp/,'rabbitmq') if config.amqp?
-
- params[k] = p[k] for own k of p
-
- params.mi_port = params.proxy_port + 30000 # i.e. 35070 etc.
- params.runtime_opensips_cfg = "#{params.runtime_opensips_cfg}.#{params.proxy_port}"
-
- return params
View
35 applications/registrant/agents/registrant.coffee
@@ -1,35 +0,0 @@
-#!/usr/bin/env coffee
-
-pico = require 'pico'
-ccnq3 = require 'ccnq3'
-params = require './params'
-
-# Agent main process
-ccnq3.config (config) ->
-
- provisioning = pico config.provisioning.local_couchdb_uri
-
- handler = (config) ->
-
- if not config.registrants? then return
-
- for r in config.registrants
- do (r) ->
- p = params r, config
-
- # Build the configuration file.
- require("#{base_path}/compiler.coffee") params, config
-
- # At startup, use the current document.
- handler config
-
- # Then start monitoring forward.
- options =
- since_name: "registrant #{config.host}"
- filter_name: "host/hostname"
- filter_params:
- hostname: config.host
-
- provisioning.monitor options, handler
-
-require './amqp-listener'
View
23 applications/registrant/package.json
@@ -1,23 +0,0 @@
-{
- "name": "ccnq3_registrant",
- "version": "0.0.2",
- "description": "Registrant for CCNQ3",
- "keywords": "",
- "author": { "name": "Stephane Alnet", "email": "stephane@shimaore.net" },
- "config": {
- "file": "/etc/ccnq3/host.json"
- },
- "scripts": {
- "start": "daemon -n ccnq3_registrant -o daemon.debug -D \"`pwd`/agents\" -r -- ./registrant.coffee; sleep 3; /usr/sbin/opensips -f /tmp/registrant.cfg || true"
- , "stop": "daemon -n ccnq3_registrant -o daemon.debug --stop; echo ':kill:' | nc -u -t 1 127.0.0.1 30010 || true"
- },
- "dependencies": {
- "ccnq3": ">=0.3.3"
- , "couchapp": "*"
- , "pico": ">=0.1.13"
- },
- "engines": { "node": ">=0.4.7", "coffee": ">=1.1.1" },
-
- "private": true,
- "ok": true
-}
View
2 common/munin/plugin-conf.d/ccnq3-munin-opensips
@@ -1,2 +0,0 @@
-[opensips*]
-user opensips
View
25 common/munin/plugins/opensips_dialogs
@@ -1,25 +0,0 @@
-#!/bin/bash
-# (c) 2011 Stephane Alnet
-# License: AGPL3+
-
-if [[ $# -eq 1 ]] && [[ $1 == 'autoconf' ]]; then
- echo "yes"
- exit
-fi
-
-if [[ $# -eq 1 ]] && [[ $1 == 'config' ]]; then
- cat <<EOT ;
-graph_title Dialogs
-graph_args -l 0
-graph_vlabel dialogs
-graph_category voice
-opensips_dialogs.label Dialogs
-opensips_dialogs.max 25000
-opensips_dialogs.min 0
-opensips_dialogs.draw LINE2
-EOT
- exit
-fi
-
-echo -n "opensips_dialogs.value "
-/usr/sbin/opensipsctl fifo dlg_list | egrep '^dialog::' | wc -l
View
24 common/munin/plugins/opensips_registered
@@ -1,24 +0,0 @@
-#!/bin/bash
-# (c) 2011 Stephane Alnet
-
-if [[ $# -eq 1 ]] && [[ $1 == 'autoconf' ]]; then
- echo "yes"
- exit
-fi
-
-if [[ $# -eq 1 ]] && [[ $1 == 'config' ]]; then
- cat <<EOT ;
-graph_title Registered Users
-graph_args -l 0
-graph_vlabel AORs
-graph_category voice
-opensips_registered.label Users
-opensips_registered.max 25000
-opensips_registered.min 0
-opensips_registered.draw LINE2
-EOT
- exit
-fi
-
-echo -n "opensips_registered.value "
-/usr/sbin/opensipsctl fifo ul_dump brief | egrep '^\s*AOR::' | wc -l
View
219 test/10000_opensips_compiles.coffee.md
@@ -1,219 +0,0 @@
-Check whether generic opensips configurations will compile OK.
-
-Requirements:
-
-```
-aptitude install opensips opensips-dbhttp-module opensips-json-module opensips-b2bua-module
-```
-
- fs = require 'fs'
- test = require './test_opensips'
-
- mktemp = (name,cb) ->
- (require 'mktemp').createFile "opensips-#{name}-XXXXXX.cfg", (err,cfg_path) ->
- if err
- throw err
- try
- cb? cfg_path, ->
- # fs.unlink cfg_path
-
-Here's the first test: test `complete` model with default options.
-
- compiler = require '../common/opensips/compiler'
-
- check_provided_config = (name) ->
- port = 15500
- mktemp name, (cfg_path,next) ->
- base = (require 'path').join process.cwd(), '../common/opensips'
-
- options =
- opensips_base_lib: base
- runtime_opensips_cfg: cfg_path
- sip_domain_name: 'test'
- listen: ['127.0.0.1']
- port: port++
-
- for k,v of require "../common/opensips/#{name}.json"
- options[k] ?= v
- for k,v of require '../common/opensips/default.json'
- options[k] ?= v
-
- # local-vars
- options.local_ipv4 = '127.0.0.1'
-
- compiler options
-
- unless fs.existsSync cfg_path
- throw "Compiler did not create #{cfg_path}"
-
- test.check_config cfg_path, next
-
- for name in 'complete conference emergency outbound-proxy registrant'.split ' '
- do (name) ->
- check_provided_config name
-
-Here's the second test: make sure the json module works.
-
- mktemp 'json-module', (cfg_path,next) ->
- fs.writeFile cfg_path, '''
- mpath = "/usr/lib/opensips/modules/"
- loadmodule "json.so"
- listen=127.0.0.1
- port=15061
- debug=0
- startup_route {
-
-First test whether we can at least assign the empty object.
-(This is something we do pretty often in our code.)
-
- $avp(foo) := '{}';
- $json(foo) := $avp(foo);
- $avp(foo) := null;
-
- if($json(foo)) {
- log("\nOK\n");
- } else {
- log("\nFailed\n");
- }
-
-Then check for values in object.
-
- $avp(foo) := '{"bar":4}';
- $json(foo) := $avp(foo);
- $avp(foo) := null;
-
- if($json(foo/bar) == 4) {
- log(0,"\nOK\n");
- } else {
- log(0,"\nFailed\n");
- }
-
- }
- route {
- exit;
- }
- ''', ->
- test.run cfg_path, next
-
-Here's the third test: make sure numerical port number works.
-
- mktemp 'port-as-integer', (cfg_path,next) ->
- fs.writeFile cfg_path, '''
- listen=127.0.0.1
- port=15062
- debug=0
- startup_route {
-
- $var(port) = 1234;
- $ru = "sip:foo@127.0.0.1";
- $rp = $var(port);
-
- if($ru == "sip:foo@127.0.0.1:1234") {
- log(0,"\nOK\n");
- } else {
- log(0,"\nFailed\n");
- }
-
- $var(port) = "1234";
- $ru = "sip:foo@127.0.0.1";
- $rp = $var(port);
-
- if($ru == "sip:foo@127.0.0.1:1234") {
- log(0,"\nOK\n");
- } else {
- log(0,"\nFailed\n");
- }
-
-
- }
- route {
- exit;
- }
- ''', ->
- test.run cfg_path, next
-
-
- mktemp 'rabbitmq', (cfg_path,next) ->
- fs.writeFile cfg_path, '''
- mpath="/usr/lib/opensips/modules"
- listen=127.0.0.1
- port=15063
- debug=5
- loadmodule "event_rabbitmq.so"
- startup_route {
- # subscribe_event("E_SCRIPT_REPORT","rabbitmq:guest:guest@127.0.0.1/ccnq3/test");
- subscribe_event("E_SCRIPT_REPORT","rabbitmq:guest:guest@127.0.0.1/test");
- $avp(event-names) := null;
- $avp(event-values) := null;
- $avp(event-names) = "event";
- $avp(event-values) = "startup";
- raise_event("E_SCRIPT_REPORT",$avp(event-names),$avp(event-values));
- }
- route {
- exit;
- }
- timer_route[tester,10] {
- $avp(event-names) := null;
- $avp(event-values) := null;
- $avp(event-names) = "event";
- $avp(event-values) = "timer";
- $avp(event-names) = "foo";
- $avp(event-values) = "bar";
- raise_event("E_SCRIPT_REPORT",$avp(event-names),$avp(event-values));
- }
- ''', ->
- test.run cfg_path, next
-
- mktemp 'crash-drouting-1.11.0', (cfg_path,next) ->
- fs.writeFileSync cfg_path, '''
- mpath="/usr/lib/opensips/modules"
- listen=127.0.0.1
- port=15063
- debug=5
- loadmodule "db_text.so"
- loadmodule "tm.so"
- loadmodule "drouting.so"
- modparam("drouting","db_url","text:///tmp")
- modparam("drouting","ruri_avp","$avp(dr_ruri)")
- modparam("drouting","gw_id_avp","$avp(gw_id)")
- modparam("drouting","gw_attrs_avp","$avp(gw_attrs)")
- modparam("drouting","gw_priprefix_avp","$avp(gw_priprefix)")
- modparam("drouting","rule_id_avp","$avp(rule_id)")
- modparam("drouting","rule_attrs_avp","$avp(rule_attrs)")
- modparam("drouting","rule_prefix_avp","$avp(rule_prefix)")
- modparam("drouting","carrier_id_avp","$avp(carrier_id)")
- modparam("drouting","carrier_attrs_avp","$avp(carrier_attrs)")
- route {
- do_routing("14");
- exit;
- }
- ''', flags:'w'
- fs.writeFileSync '/tmp/version', '''
- table_name(str) table_version(int)
- dr_gateways:6
- dr_groups:1
- dr_carriers:1
- dr_rules:1
-
- ''', flags:'w'
- fs.writeFileSync '/tmp/dr_gateways', '''
- id(int) gwid(str) type(int) address(str) strip(int) pri_prefix(str,null) attrs(str,null) probe_mod(int) description(str)
- 1:gw1:0:192.168.1.10:0:::0:gw1
- 2:gw2:0:192.168.1.11:0:::0:gw2
-
- ''', flags:'w'
- fs.writeFileSync '/tmp/dr_groups', '''
- id(int) username(str) domain(str) groupid(int) description(str)
-
- ''', flags:'w'
- fs.writeFileSync '/tmp/dr_carriers', '''
- id(int) carrierid(str) gwlist(str) flags(int) attrs(str,null) description(str)
-
- ''', flags:'w'
- fs.writeFileSync '/tmp/dr_rules', '''
- ruleid(int) groupid(str) prefix(str) timerec(string) priority(int) routeid(str,null) gwlist(str) attrs(str,null) description(str)
- 1:14:1536::0::gw1,gw2:attrs_here:very expensive
- 2:14:1627::0::gw1,gw2:attrs_here:very expensive
-
- ''', flags:'w'
- test.run cfg_path, next
View
3 test/Makefile
@@ -1,3 +0,0 @@
-clean:
- killall -q -9 opensips || true
- rm -f opensips-*.cfg
View
20 test/test_opensips.coffee.md
@@ -1,20 +0,0 @@
- should = require 'should'
-
- exec = (command,args,next) ->
- run = (require 'child_process').spawn command, args
- run.stdout.pipe process.stdout
- run.stderr.pipe process.stderr
- run.on 'error', (e) ->
- next?()
- throw e
- run.on 'exit', (code,signal) ->
- next?()
- (code).should.equal 0, "#{command} #{args.join ' '} -> died with code = #{code}, signal = #{signal}"
-
- @check_config = (cfg_path,next) ->
- console.log "Checking configuration file #{cfg_path}"
- exec '/usr/sbin/opensips', ['-C', '-D', '-E', '-f', cfg_path], next
-
- @run = (cfg_path,next) ->
- console.log "Running OpenSIPS with #{cfg_path}"
- exec '/usr/sbin/opensips', ['-D', '-E', '-f', cfg_path], next

0 comments on commit 41b1ac9

Please sign in to comment.