diff --git a/call-control b/call-control index 3f4a417..406a8d1 100755 --- a/call-control +++ b/call-control @@ -1,122 +1,82 @@ #!/usr/bin/env python -"""Implementation of a call controller for OpenSIPS.""" - - -def send_command(command, **kwargs): - import socket - - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(process.runtime.file('socket')) - - sock.sendall('%s\r\n' % command + '\r\n'.join(['%s: %s' % (key, value) for key, value in kwargs.items()]) + '\r\n\r\n') - response = '' - while True: - data = sock.recv(4096) - response += data - if not data or data.endswith('\r\n\r\n'): - break - sock.close() - for line in response.splitlines(): - if line: - print line +"""Call control engine for OpenSIPS""" if __name__ == '__main__': + import callcontrol import sys - from optparse import OptionParser - from application.process import process, ProcessError from application import log - import callcontrol + from application.process import process, ProcessError + from argparse import ArgumentParser name = 'call-control' fullname = 'SIP call-control engine' description = 'Implementation of a call-control engine for SIP' process.configuration.user_directory = None process.configuration.subdirectory = 'callcontrol' process.runtime.subdirectory = 'callcontrol' - default_pid = process.runtime.file('{}.pid'.format(name)) - - parser = OptionParser(version="%%prog %s" % callcontrol.__version__) - parser.add_option("--no-fork", action="store_false", dest="fork", default=True, - help="run the process in the foreground") - parser.add_option("--pid", dest="pid_file", default=default_pid, - help="pid file when forking ({})".format(default_pid), - metavar="FILE") - parser.add_option("--debug", dest="debug", default=None, - help="get information about a currently running call-control daemon", - metavar="COMMAND") - parser.add_option("--terminate", dest="terminate", default=None, - help="terminate an on-going session", - metavar="CALLID") - (options, args) = parser.parse_args() + parser = ArgumentParser(usage='%(prog)s [options]') + parser.add_argument('--version', action='version', version='%(prog)s {}'.format(callcontrol.__version__)) + parser.add_argument('--systemd', action='store_true', help='run as a systemd simple service and log to journal') + parser.add_argument('--no-fork', action='store_false', dest='fork', help='run in the foreground and log to the terminal') + parser.add_argument('--config-dir', dest='config_directory', default=None, help='the configuration directory ({})'.format(process.configuration.system_directory), metavar='PATH') + parser.add_argument('--runtime-dir', dest='runtime_directory', default=None, help='the runtime directory ({})'.format(process.runtime.directory), metavar='PATH') + parser.add_argument('--debug', action='store_true', help='enable verbose logging') + parser.add_argument('--debug-memory', action='store_true', help='enable memory debugging') + + options = parser.parse_args() log.Formatter.prefix_format = '{record.levelname:<8s} ' + if options.config_directory is not None: + process.configuration.local_directory = options.config_directory + if options.runtime_directory is not None: + process.runtime.directory = options.runtime_directory + try: process.runtime.create_directory() except ProcessError as e: log.critical('Cannot start %s: %s', fullname, e) sys.exit(1) - if options.debug is not None and options.terminate is not None: - log.error('cannot run with both --debug and --terminate options in the same time') - sys.exit(1) - - if options.debug is not None: - if options.debug == '': - log.error('must specify debug command') - sys.exit(1) - try: - send_command('debug', show=options.debug, **dict([arg.split('=',1) for arg in args if arg.find('=') >= 0])) - except Exception, e: - log.error('failed to complete debug command: %s', e) - sys.exit(1) - else: - sys.exit(0) - - if options.terminate is not None: - if options.terminate == '': - log.error('must specify callid to terminate') - sys.exit(1) - try: - send_command('terminate', callid=options.terminate) - except Exception, e: - log.error('failed to terminate session: %s' % e) - else: - sys.exit(0) - - if options.fork: + if options.systemd: + from systemd.journal import JournalHandler + log.set_handler(JournalHandler(SYSLOG_IDENTIFIER=name)) + log.capture_output() + elif options.fork: try: process.daemonize(options.pid_file) except ProcessError, e: log.critical('Cannot start %s: %s', fullname, e) sys.exit(1) log.use_syslog(name) log.info('Starting %s %s', fullname, callcontrol.__version__) from callcontrol.controller import CallControlServer - if not options.fork: + if options.debug: + log.level.current = log.level.DEBUG + if options.debug_memory: from application.debug.memory import memory_dump try: cserver = CallControlServer() except Exception, e: log.critical('Could not create %s: %s', fullname, e) if type(e) is not RuntimeError: log.exception() sys.exit(1) try: cserver.run() except Exception, e: log.critical('Could not run %s: %s', fullname, e) if type(e) is not RuntimeError: log.exception() - if not options.fork: + if options.debug_memory: memory_dump() diff --git a/call-control-cli b/call-control-cli new file mode 100755 index 0000000..d70a25b --- /dev/null +++ b/call-control-cli @@ -0,0 +1,117 @@ +#!/usr/bin/env python + +import callcontrol +import socket +import sys + +from application import log +from application.process import process +from argparse import ArgumentParser + + +class CallControlCommand(object): + def __init__(self, command, **kw): + self.command = command + self.kw = kw + + def __str__(self): + arguments = [self.command] + arguments.extend('{}: {}'.format(key, value) for key, value in self.kw.items()) + return '\r\n'.join(arguments) + '\r\n\r\n' + + def execute(self): + target = process.runtime.file('socket') + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + try: + # noinspection PyShadowingNames + try: + sock.connect(target) + sock.sendall(str(self)) + response = '' + while True: + data = sock.recv(4096) + response += data + if not data or data.endswith('\r\n\r\n'): + break + except socket.error as e: + raise RuntimeError('could not send command to {}: {}'.format(target, e)) + finally: + sock.close() + for line in response.rstrip().splitlines(): + print(line) + + @classmethod + def handler(cls, options): + raise NotImplementedError + + +class ListCommand(CallControlCommand): + def __init__(self, user=None): + if user is not None: + kw = dict(command='debug', show='sessions', user=user) + else: + kw = dict(command='debug', show='sessions') + super(ListCommand, self).__init__(**kw) + + @classmethod + def handler(cls, options): + return cls(options.user).execute() + + +class ShowCommand(CallControlCommand): + def __init__(self, call_id): + super(ShowCommand, self).__init__(command='debug', show='session', callid=call_id) + + @classmethod + def handler(cls, options): + return cls(options.call_id).execute() + + +class TerminateCommand(CallControlCommand): + def __init__(self, call_id): + super(TerminateCommand, self).__init__(command='terminate', callid=call_id) + + @classmethod + def handler(cls, options): + return cls(options.call_id).execute() + + +if __name__ == '__main__': + + name = 'call-control-cli' + description = 'Command line interface tool for call-control' + + log.Formatter.prefix_format = '{record.levelname:<8s} ' + + process.configuration.user_directory = None + process.configuration.subdirectory = 'callcontrol' + process.runtime.subdirectory = 'callcontrol' + + parser = ArgumentParser(description='This script can issue commands to a running instance of call-control') + parser.add_argument('--version', action='version', version='%(prog)s {}'.format(callcontrol.__version__)) + parser.add_argument('--runtime-dir', dest='runtime_directory', default=None, help='the runtime directory ({})'.format(process.runtime.directory), metavar='PATH') + + subparsers = parser.add_subparsers(title='supported commands', dest='command') + + parser_list = subparsers.add_parser('list', help='list existing sessions') + parser_list.add_argument('user', nargs='?', help='optional user to filter results by') + parser_list.set_defaults(handler=ListCommand.handler) + + parser_show = subparsers.add_parser('show', help='show details about a specific session') + parser_show.add_argument('call_id', help='the call-id for the session') + parser_show.set_defaults(handler=ShowCommand.handler) + + parser_terminate = subparsers.add_parser('terminate', help='terminate a specific session') + parser_terminate.add_argument('call_id', help='the call-id for the session') + parser_terminate.set_defaults(handler=TerminateCommand.handler) + + args = parser.parse_args() + + if args.runtime_directory is not None: + process.runtime.directory = args.runtime_directory + + try: + args.handler(args) + except Exception as e: + log.critical('Failed to execute command: {}'.format(e)) + sys.exit(1) diff --git a/debian/callcontrol.manpages b/debian/callcontrol.manpages index 768c4ee..8d72e17 100644 --- a/debian/callcontrol.manpages +++ b/debian/callcontrol.manpages @@ -1 +1,2 @@ doc/man/call-control.1 +doc/man/call-control-cli.1 diff --git a/debian/control b/debian/control index ab4cb52..c3cda39 100644 --- a/debian/control +++ b/debian/control @@ -1,22 +1,22 @@ Source: callcontrol Section: net Priority: optional Maintainer: Dan Pascu Uploaders: Adrian Georgescu , Tijmen de Mes Build-Depends: debhelper (>= 9), python (>= 2.7) Standards-Version: 3.9.8 Package: callcontrol Architecture: all -Depends: ${python:Depends}, ${misc:Depends}, lsb-base, python-application (>= 2.8.0), python-gnutls (>= 3.0.0), python-twisted-core, python-sqlobject +Depends: ${python:Depends}, ${misc:Depends}, lsb-base, python-application (>= 2.8.0), python-gnutls (>= 3.0.0), python-twisted-core, python-sqlobject, python-systemd Description: Call Control prepaid application for OpenSIPS Call Control is a prepaid application that can be used together with OpenSIPS call_control module and CDRTool rating engine to limit the duration of SIP sessions based on a prepaid balance. It can also be used to limit the duration of any session to a predefined maximum value without debiting a balance. . Call Control achieves this by maintaining a timer for each session and sending BYE messages to both SIP end-points, if the session exceeds its maximum session limit or if the Call Control receives a command to forcefully close the call from outside. diff --git a/doc/man/call-control-cli.1 b/doc/man/call-control-cli.1 new file mode 100644 index 0000000..5b6862b --- /dev/null +++ b/doc/man/call-control-cli.1 @@ -0,0 +1,57 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.TH "Call Control CLI" "1" "Aug 13, 2019" +.\" Please adjust this date whenever revising the manpage. +.\" +.\" Some roff macros, for reference: +.\" .nh disable hyphenation +.\" .hy enable hyphenation +.\" .ad l left justify +.\" .ad b justify to both left and right margins +.\" .nf disable filling +.\" .fi enable filling +.\" .br insert line break +.\" .sp insert n+1 empty lines +.\" for manpage-specific macros, see man(7) +.SH NAME +call\-control\-cli \- Command line interface for Call Control +.SH SYNOPSIS +.B call-control-cli +.RI [ options ] +.RI commands +.SH DESCRIPTION +.PP +.\" TeX users may be more comfortable with the \fB\fP and +.\" \fI\fP escape sequences to invode bold face and italics, +.\" respectively. +The \fBcall\-control\-cli\fP command is used for sending requests to a +running \fBcall\-control\fP server. It can either query the running server +for information about the existing sessions or it can issue commands to +administratively terminate sessions. +.SH OPTIONS +.TP +.B \-h, \-\-help +Show summary of options. +.TP +.B \-\-version +Show the program version. +.TP +.B \-\-runtime\-dir=PATH +Set a custom runtime directory (default: \fB/run/callcontrol\fP). Must +match the runtime directory used by the \fBcall\-control\fP engine. +.SH COMMANDS +.TP +.B list \fI[user]\fP +List existing sessions. If user is not specified list all sessions, else +list sessions filtered by user. The user argument is taken as a prefix, so +it will match any SIP AOR starting with the specified user string. +.TP +.B show \fIcall\-id\fP +Show details about the session identified by \fIcall\-id\fP. +.TP +.B terminate \fIcall\-id\fP +Administratively terminate the session identified by \fIcall\-id\fP. +.SH AUTHORS +Dan Pascu diff --git a/doc/man/call-control.1 b/doc/man/call-control.1 index 7bb93c4..e6c2498 100644 --- a/doc/man/call-control.1 +++ b/doc/man/call-control.1 @@ -1,55 +1,65 @@ .\" Hey, EMACS: -*- nroff -*- .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) -.TH "SIP Call Control" "1" "May 15, 2008" +.TH "SIP Call Control" "1" "Aug 13, 2019" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME -call\-control \- the SIP call control application +call\-control \- A SIP call control engine for OpenSIPS .SH SYNOPSIS .B call-control .RI [ options ] .SH DESCRIPTION .PP .\" TeX users may be more comfortable with the \fB\fP and .\" \fI\fP escape sequences to invode bold face and italics, .\" respectively. -Call Control is a prepaid application that can be used together with -OpenSIPS call_control module and CDRTool rating engine to limit the duration -of SIP sessions based on a prepaid balance. It can also be used to limit the -duration of any session to a predefined maximum value without debiting a -balance. +Call Control is a prepaid application that can be used together with the +OpenSIPS call_control module and the CDRTool rating engine to limit the +duration of SIP sessions based on a prepaid balance. It can also be used +to limit the duration of non-prepaid sessions to a predefined maximum +value. .PP Call Control achieves this by maintaining a timer for each session and -sending BYE messages to both SIP end-points, if the session exceeds its -maximum session limit or if the Call Control receives a command to -forcefully close the call from outside. +terminating the call if the session exceeds its allowed time limit or +if Call Control receives an external request to terminate the session. .SH OPTIONS .TP .B \-h, \-\-help Show summary of options. .TP .B \-\-version -Show version of program. +Show the program version. +.TP +.B \-\-systemd +Run the process as a systemd simple service and log to journal. .TP .B \-\-no\-fork -Run the process in the foreground (for debugging). +Run the process in the foreground and log to the terminal. +.TP +.B \-\-config\-dir=PATH +Set a custom configuration directory (default: \fB/etc/callcontrol\fP). +.TP +.B \-\-runtime\-dir=PATH +Set a custom runtime directory (default: \fB/run/callcontrol\fP). +.TP +.B \-\-debug +Enable verbose logging. .TP -.B \-\-pid=file -Set the proces pid file to \fB`file'\fP. If not specified the default -pid file is \fB/var/run/callcontrol/call\-control.pid\fP . +.B \-\-debug\-memory +Enable memory debugging. .SH AUTHORS Dan Pascu .br Lucian Stanescu . diff --git a/setup.py b/setup.py index 5c755b7..d815396 100755 --- a/setup.py +++ b/setup.py @@ -1,30 +1,30 @@ #!/usr/bin/python import callcontrol from distutils.core import setup setup( name='callcontrol', version=callcontrol.__version__, description='SIP call control engine', long_description='Call control engine for OpenSIPS', url='http://callcontrol.ag-projects.com', license='GPLv2', author='AG Projects', author_email='support@ag-projects.com', platforms=['Platform Independent'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Service Providers', 'License :: GNU General Public License 2 (GPLv2)', 'Operating System :: OS Independent', 'Programming Language :: Python' ], packages=['callcontrol', 'callcontrol.rating', 'callcontrol.rating.backends'], - scripts=['call-control'] + scripts=['call-control', 'call-control-cli'] )