Ver código fonte

imported initial kamcli files

Daniel-Constantin Mierla 10 anos atrás
pai
commit
84e05ef577

+ 7 - 0
.gitignore

@@ -1,6 +1,13 @@
+# Editor tmp files
+*~
+*.swp
+*.swo
+
+# Python specific tmp files
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
+*$py.class
 
 # C extensions
 *.so

+ 74 - 1
README.md

@@ -1,2 +1,75 @@
-# kamcli
+## KAMCLI
+
 Kamailio Command Line Interface Control Tool
+
+### Installation
+
+#### Requirements
+
+OS Packages (install via apt, yum, ...):
+
+  * python
+  * python-pip
+  * python-dev (optional - needed to install mysql-python via pip)
+
+PIP Packages (install via pip):
+
+  * _to install in a virtual environment (see next)_
+    * virtualenv
+  * _extra packages requied by kamcli (part of OS or  virtual environment)_
+    * click
+    * sqlalchemy
+    * mysql-python (optional - needed if you want to connect to MySQL database)
+
+#### Install VirtualEnv
+
+It is recommended to install in a virtual environment at least for development.
+Some useful details about installing Click in virtual environament are
+available at:
+
+  * http://click.pocoo.org/4/quickstart/#virtualenv
+
+For example, create the virtual environemnt in the folder kamclienv
+
+```
+  $ pip install virtualenv
+  $ mkdir kamclienv
+  $ cd kamclienv
+  $ virtualenv venv
+```
+
+To activate the virtual environment:
+
+```
+  $ . venv/bin/activate
+```
+
+Clone kamcli and install it. The commands can be done inside the virtual
+environment if activate to be available only there or without virtual
+environment to be installed in the system.
+
+```
+  $ git clone ...
+  $ cd kamcli
+  $ pip install --editable .
+```
+
+To deactivate the virtual environment, run:
+
+```
+  $ deactivate
+```
+
+### Usage
+
+```
+  $ kamcli --help
+  $ kamcli <command> --help
+  $ kamcli <command> <subcommand> --help
+```
+
+### License
+
+GPLv2
+
+Copyright: asipto.com

+ 27 - 0
docs/Devel.md

@@ -0,0 +1,27 @@
+## KAMCLI
+
+Kamailio Command Line Interface Control Tool
+
+
+### Development Guidelines
+
+#### Indentation
+
+  * user 4 whitespaces for indentation
+
+#### Plugins
+
+Development of kamcli has its starting point in the *complex* example of Click:
+
+  * https://github.com/mitsuhiko/click/tree/master/examples/complex
+
+Other examples provided by Click are good source of inspiration:
+
+  * https://github.com/mitsuhiko/click/tree/master/examples
+
+In short, thttps://github.com/mitsuhiko/click/tree/master/exampleshe steps for adding a plugin:
+
+  * add you new comand in kamcli/commands/ folder
+  * name the file cmd_newcommand.py
+  * define cli(...) function, which can be a command or group of commands
+

+ 0 - 0
kamcli/__init__.py


+ 169 - 0
kamcli/cli.py

@@ -0,0 +1,169 @@
+import os
+import sys
+import click
+
+try:
+    import ConfigParser as configparser
+except ImportError:
+    import configparser
+
+def read_global_config(config_paths):
+    """Get config."""
+    parser = configparser.SafeConfigParser()
+    if config_paths:
+        parser.read(config_paths)
+    else:
+        parser.read(["kamcli.ini"])
+    return parser
+
+
+#        try:
+#            self.optmain.update(parser.items('main'))
+#        except configparser.NoSectionError:
+#            pass
+
+
+def parse_user_spec(ctx, ustr):
+    """Get details of the user from ustr (username, aor or sip uri)"""
+    udata = { }
+    if ":" in ustr:
+        uaor = ustr.split(":")[1]
+    else:
+        uaor = ustr
+    if "@" in uaor:
+        udata['username'] = uaor.split("@")[0]
+        udata['domain'] = uaor.split("@")[1]
+    else:
+        udata['username'] = uaor.split("@")[0]
+        try:
+            udata['domain'] = ctx.gconfig.get('main', 'domain')
+        except configparser.NoOptionError:
+            ctx.log("Default domain not set in config file")
+            sys.exit()
+    if udata['username'] is None:
+        ctx.log("Failed to get username")
+        sys.exit()
+    if udata['domain'] is None:
+        ctx.log("Failed to get domain")
+        sys.exit()
+    udata['username'] = udata['username'].encode('ascii','ignore')
+    udata['domain'] = udata['domain'].encode('ascii','ignore')
+    return udata
+
+
+
+
+CONTEXT_SETTINGS = dict(auto_envvar_prefix='KAMCLI')
+
+COMMAND_ALIASES = {
+    "subs": "subscriber",
+    "fifo": "mi",
+    "rpc":  "jsonrpc",
+}
+
+class Context(object):
+
+    def __init__(self):
+        self.verbose = False
+        self.wdir = os.getcwd()
+        self.gconfig_paths = []
+        self._gconfig = None
+
+    def log(self, msg, *args):
+        """Logs a message to stderr."""
+        if args:
+            msg %= args
+        click.echo(msg, file=sys.stderr)
+
+    def vlog(self, msg, *args):
+        """Logs a message to stderr only if verbose is enabled."""
+        if self.verbose:
+            self.log(msg, *args)
+
+    @property
+    def gconfig(self):
+        if self._gconfig is None:
+            self._gconfig = read_global_config(self.gconfig_paths)
+        return self._gconfig
+
+
+pass_context = click.make_pass_decorator(Context, ensure=True)
+cmd_folder = os.path.abspath(os.path.join(os.path.dirname(__file__),
+                                          'commands'))
+
+class KamCLI(click.MultiCommand):
+
+    def list_commands(self, ctx):
+        rv = []
+        for filename in os.listdir(cmd_folder):
+            if filename.endswith('.py') and \
+               filename.startswith('cmd_'):
+                rv.append(filename[4:-3])
+        rv.sort()
+        return rv
+
+    def get_command(self, ctx, name):
+        if name in COMMAND_ALIASES:
+            name = COMMAND_ALIASES[name]
+        try:
+            if sys.version_info[0] == 2:
+                name = name.encode('ascii', 'replace')
+            mod = __import__('kamcli.commands.cmd_' + name,
+                             None, None, ['cli'])
+        except ImportError:
+            return
+        return mod.cli
+
+
+def global_read_config(ctx, param, value):
+    """Callback that is used whenever --config is passed.  We use this to
+    always load the correct config.  This means that the config is loaded
+    even if the group itself never executes so our aliases stay always
+    available.
+    """
+    if value is None:
+        value = os.path.join(os.path.dirname(__file__), 'kamcli.ini')
+    ctx.read_config(value)
+    return value
+
+
[email protected](cls=KamCLI, context_settings=CONTEXT_SETTINGS,
+                short_help='Kamailio command line interface control tool')
[email protected]('--wdir', type=click.Path(exists=True, file_okay=False,
+                                        resolve_path=True),
+              help='Working directory.')
[email protected]('-v', '--verbose', is_flag=True,
+              help='Enable verbose mode.')
[email protected]('--config', '-c',
+              default=None, help="Configuration file.")
[email protected]('nodefaultconfigs', '--no-default-configs', is_flag=True,
+            help='Skip loading default configuration files.')
+@pass_context
+def cli(ctx, verbose, wdir, config, nodefaultconfigs):
+    """Kamailio command line interface control tool.
+
+    \b
+    Help per command: kamcli <command> --help
+
+    \b
+    Default configuration files:
+        - /etc/kamcli/kamcli.ini
+        - ~/.kamcli/kamctli.ini
+    Configs loading order: default configs, then --config option
+
+    \b
+    License: GPLv2
+    Copyright: asipto.com
+    """
+    ctx.verbose = verbose
+    if wdir is not None:
+        ctx.wdir = wdir
+    if not nodefaultconfigs:
+        if os.path.isfile("/etc/kamcli/kamcli.ini"):
+            ctx.gconfig_paths.append("/etc/kamcli/kamcli.ini")
+        tpath = os.path.expanduser("~/.kamcli/kamacli.ini")
+        if os.path.isfile(tpath):
+            ctx.gconfig_paths.append(tpath)
+    if config is not None:
+        ctx.gconfig_paths.append(os.path.expanduser(config))
+

+ 0 - 0
kamcli/commands/__init__.py


+ 42 - 0
kamcli/commands/cmd_config.py

@@ -0,0 +1,42 @@
+import os
+import sys
+import click
+from kamcli.cli import pass_context
+
[email protected]('config', help='Manage the config file')
+@pass_context
+def cli(ctx):
+    pass
+
+
[email protected]('raw', short_help='Display raw content of configuration file')
+@pass_context
+def config_raw(ctx):
+    """Show content of configuration file for kamcli"""
+    ctx.log('\n---')
+    ctx.gconfig.write(sys.stdout)
+    ctx.log('\n---')
+
+
[email protected]('show', short_help='Show expanded content of configuration file sections')
[email protected]('sections', nargs=-1, metavar='<sections>')
+@pass_context
+def config_show(ctx, sections):
+    """Show expanded content of configuration file section"""
+    if sections:
+        ctx.log('\n---')
+    for s in sections:
+        ctx.log('[%s]', s)
+        for k, v in ctx.gconfig.items(s):
+            ctx.log("%s= %s", k, v)
+        ctx.log('\n---')
+
+
[email protected]('paths', short_help='Show the paths of configuration files')
+@pass_context
+def config_paths(ctx):
+    """Show the patsh of configuration files for kamcli"""
+    print
+    print ctx.gconfig_paths
+    print
+

+ 137 - 0
kamcli/commands/cmd_db.py

@@ -0,0 +1,137 @@
+import os
+import sys
+import click
+import hashlib
+import pprint
+import json
+from sqlalchemy import create_engine
+from sqlalchemy.schema import CreateTable
+from kamcli.cli import pass_context
+from kamcli.cli import parse_user_spec
+from kamcli.ioutils import ioutils_dbres_print
+from kamcli.ioutils import ioutils_formats_list
+
+##
+#
+#
[email protected]('db', help='Raw database operations')
+@pass_context
+def cli(ctx):
+    pass
+
+
+##
+#
+#
[email protected]('connect', help='Launch db cli and connect to database')
+@pass_context
+def db_connect(ctx):
+    dbtype = ctx.gconfig.get('db', 'type')
+    if dbtype.lower() == "mysql":
+        scmd = "mysql -h {0} -u {1} -p{2} {3}".format(ctx.gconfig.get('db', 'host'),
+                ctx.gconfig.get('db', 'rwuser'), ctx.gconfig.get('db', 'rwpassword'), ctx.gconfig.get('db', 'dbname'))
+    elif dbtype == "postgres":
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    else:
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    os.system(scmd)
+
+
+##
+#
+#
[email protected]('clirun', help='Run SQL statement via cli')
[email protected]('query', metavar='<query>')
+@pass_context
+def db_clirun(ctx, query):
+    dbtype = ctx.gconfig.get('db', 'type')
+    if dbtype == "mysql":
+        scmd = 'mysql -h {0} -u {1} -p{2} -e "{3} ;" {4}'.format(ctx.gconfig.get('db', 'host'),
+                ctx.gconfig.get('db', 'rwuser'), ctx.gconfig.get('db', 'rwpassword'), query, ctx.gconfig.get('db', 'dbname'))
+    elif dbtype == "postgres":
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    else:
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    os.system(scmd)
+
+
+##
+#
+#
[email protected]('clishow', help='Show content of table via cli')
[email protected]('table', metavar='table>')
+@pass_context
+def db_clishow(ctx, table):
+    dbtype = ctx.gconfig.get('db', 'type')
+    if dbtype == "mysql":
+        scmd = 'mysql -h {0} -u {1} -p{2} -e "select * from {3} ;" {4}'.format(ctx.gconfig.get('db', 'host'),
+                ctx.gconfig.get('db', 'rwuser'), ctx.gconfig.get('db', 'rwpassword'), table, ctx.gconfig.get('db', 'dbname'))
+    elif dbtype == "postgres":
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    else:
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    os.system(scmd)
+
+
+##
+#
+#
[email protected]('clishowg', help='Show content of table via cli')
[email protected]('table', metavar='table>')
+@pass_context
+def db_clishowg(ctx, table):
+    dbtype = ctx.gconfig.get('db', 'type')
+    if dbtype == "mysql":
+        scmd = 'mysql -h {0} -u {1} -p{2} -e "select * from {3} \G" {4}'.format(ctx.gconfig.get('db', 'host'),
+                ctx.gconfig.get('db', 'rwuser'), ctx.gconfig.get('db', 'rwpassword'), table, ctx.gconfig.get('db', 'dbname'))
+    elif dbtype == "postgres":
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    else:
+        ctx.log("unsupported database type [%s]", dbtype)
+        sys.exit()
+    os.system(scmd)
+
+
+##
+#
+#
[email protected]('show', help='Show content of a table')
[email protected]('oformat', '--output-format', '-F',
+                type=click.Choice(ioutils_formats_list),
+                default=None, help='Format the output')
[email protected]('ostyle', '--output-style', '-S',
+                default=None, help='Style of the output (tabulate table format)')
[email protected]('table', metavar='<table>')
+@pass_context
+def db_show(ctx, oformat, ostyle, table):
+    ctx.vlog('Content of database table [%s]', table)
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    res = e.execute('select * from {0}'.format(table))
+    ioutils_dbres_print(ctx, oformat, ostyle, res)
+
+
+##
+#
+#
[email protected]('showcreate', help='Show content of a table')
[email protected]('oformat', '--output-format', '-F',
+                type=click.Choice(ioutils_formats_list),
+                default=None, help='Format the output')
[email protected]('ostyle', '--output-style', '-S',
+                default=None, help='Style of the output (tabulate table format)')
[email protected]('table', metavar='<table>')
+@pass_context
+def db_showcreate(ctx, oformat, ostyle, table):
+    ctx.vlog('Show create of database table [%s]', table)
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    res = e.execute('show create table {0}'.format(table))
+    ioutils_dbres_print(ctx, oformat, ostyle, res)
+
+

+ 34 - 0
kamcli/commands/cmd_jsonrpc.py

@@ -0,0 +1,34 @@
+import os
+import sys
+import click
+from kamcli.cli import pass_context
+from kamcli.iorpc import command_jsonrpc_fifo
+
[email protected]('jsonrpc', short_help='Execute JSONRPC command')
[email protected]('dryrun', '--dry-run', is_flag=True,
+            help='Do not execute the command, only print it')
[email protected]('cmd', nargs=1, metavar='[<command>]')
[email protected]('params', nargs=-1, metavar='[<params>]')
+@pass_context
+def cli(ctx, dryrun, cmd, params):
+    """Execute JSONRPC command
+
+        \b
+        Parameters:
+            - <command> - the JSONRPC command
+            - <params>  - parameters for JSONRPC command
+                        - by default the value of a parameter is considered
+                          of type string
+                        - to enforce integer value prefix with 'i:' (e.g., i:10)
+                        - string values can be also prefixed with 's:'
+                        - if a parameter starts with 's:', prefix it with 's:'
+        Examples:
+            - jsonrpc system.listMethods
+            - jsonrpc core.psx
+            - jsonrpc stats.get_statistics all
+            - jsonrpc pv.shvSet counter i:123
+    """
+    ctx.log("Running JSONRPC command: [%s]", cmd)
+    command_jsonrpc_fifo(ctx, dryrun, "/var/run/kamailio/kamailio_jsonrpc_fifo",
+            "kamailio_jsonrpc_fifo_reply", "json", cmd, params)
+

+ 23 - 0
kamcli/commands/cmd_mi.py

@@ -0,0 +1,23 @@
+import os
+import sys
+import click
+from kamcli.cli import pass_context
+from kamcli.iorpc import command_mi_fifo
+
[email protected]('mi', short_help='Execute raw MI command')
[email protected]('dryrun', '--dry-run', is_flag=True,
+            help='Do not execute the command, only print it')
[email protected]('cmd', nargs=1, metavar='[<command>]')
[email protected]('params', nargs=-1, metavar='[<params>]')
+@pass_context
+def cli(ctx, dryrun, cmd, params):
+    """Execute raw MI command
+
+        \b
+        Parameters:
+            - <command> - the MI command
+            - <params>  - parameters for command
+    """
+    ctx.log("Running MI command: [%s]", cmd)
+    command_mi_fifo(ctx, dryrun, "/var/run/kamailio/kamailio_fifo", "kamailio_fifo_reply", "raw", cmd, params)
+

+ 37 - 0
kamcli/commands/cmd_stats.py

@@ -0,0 +1,37 @@
+import click
+from kamcli.cli import pass_context
+from kamcli.iorpc import command_ctl
+
+
[email protected]('stats', short_help='Print internal statistics')
[email protected]('single', '--single', '-s', is_flag=True,
+            help='The name belong to one statistic (otherwise the name is for a group)')
[email protected]('names', nargs=-1, metavar='[<name>]')
+@pass_context
+def cli(ctx, single, names):
+    """Print internal statistics
+
+        \b
+        Parameters:
+            - [<name>]  - name of statistic or statistics group
+                        - if missing, all statistics are printed
+                        - it can be a list of names
+    """
+    if names:
+        for n in names:
+            if n.endswith(":"):
+                # enforce group name by ending with ':'
+                command_ctl(ctx, 'get_statistics', [ n ])
+            elif n.find(":")>0:
+                # get only stat name, when providing 'group:stat'
+                command_ctl(ctx, 'get_statistics', [ n.split(":")[1] ])
+            elif single:
+                # single stat name flag
+                command_ctl(ctx, 'get_statistics', [ n ])
+            else:
+                # default is group name
+                command_ctl(ctx, 'get_statistics', [ n+":" ])
+    else:
+        # no name, print all
+        command_ctl(ctx, 'get_statistics', [ 'all' ])
+

+ 194 - 0
kamcli/commands/cmd_subscriber.py

@@ -0,0 +1,194 @@
+import click
+import hashlib
+import json
+from sqlalchemy import create_engine
+from kamcli.ioutils import ioutils_dbres_print
+from kamcli.cli import pass_context
+from kamcli.cli import parse_user_spec
+
+
+##
+#
+#
[email protected]('subscriber', help='Manage the subscribers')
+@pass_context
+def cli(ctx):
+    pass
+
+
+##
+#
+#
[email protected]('add', short_help='Add a new subscriber')
[email protected]('pwtext', '--text-password', '-t',
+                type=click.Choice(['yes', 'no']),
+                default='yes', help='Store password in clear text (default yes)')
[email protected]('userid', metavar='<userid>')
[email protected]('password', metavar='<password>')
+@pass_context
+def subscriber_add(ctx, pwtext, userid, password):
+    """Add a new subscriber
+
+    \b
+    Parameters:
+        <userid> - username, AoR or SIP URI for subscriber
+        <password> - the password
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.vlog('Adding subscriber [%s] in domain [%s] with password [%s]', udata['username'], udata['domain'], password)
+    ha1 = hashlib.md5(udata['username'] + ":" + udata['domain'] + ":" + password).hexdigest()
+    ha1b = hashlib.md5(udata['username'] + "@" + udata['domain'] + ":" + udata['domain'] + ":" + password).hexdigest()
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    if pwtext == 'yes' :
+        e.execute('insert into subscriber (username, domain, password, ha1, ha1b) values (%s, %s, %s, %s, %s)', udata['username'], udata['domain'], password, ha1, ha1b)
+    else:
+        e.execute('insert into subscriber (username, domain, ha1, ha1b) values (%s, %s, %s, %s)', udata['username'], udata['domain'], ha1, ha1b)
+
+
+##
+#
+#
[email protected]('rm', short_help='Remove an existing subscriber')
[email protected]('userid', metavar='<userid>')
+@pass_context
+def subscriber_rm(ctx, userid):
+    """Remove an existing subscriber
+
+    \b
+    Parameters:
+        <userid> - username, AoR or SIP URI for subscriber
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.log('Removing subscriber [%s@%s]', udata['username'], udata['domain'])
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    e.execute('delete from subscriber where username=%s and domain=%s', udata['username'], udata['domain'])
+
+
+##
+#
+#
[email protected]('passwd', short_help='Update the password for a subscriber')
[email protected]('pwtext', '--text-password', '-t', type=click.Choice(['yes', 'no']), default='yes', help='Store password in clear text (default yes)')
[email protected]('userid', metavar='<userid>')
[email protected]('password', metavar='<password>')
+@pass_context
+def subscriber_passwd(ctx, pwtext, userid, password):
+    """Update password for a subscriber
+
+    \b
+    Parameters:
+        <userid> - username, AoR or SIP URI for subscriber
+        <password> - the password
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.log('Updating subscriber [%s@%s] with password [%s]', udata['username'], udata['domain'], password)
+    ha1 = hashlib.md5(udata['username'] + ":" + udata['domain'] + ":" + password).hexdigest()
+    ha1b = hashlib.md5(udata['username'] + "@" + udata['domain'] + ":" + udata['domain'] + ":" + password).hexdigest()
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    if pwtext == 'yes' :
+        e.execute('update subscriber set password=%s, ha1=%s, ha1b=%s where username=%s and domain=%s', password, ha1, ha1b, udata['username'], udata['domain'])
+    else:
+        e.execute('update subscriber set ha1=%s, ha1b=%s where username=%s and domain=%s', ha1, ha1b, udata['username'], udata['domain'])
+
+
+##
+#
+#
[email protected]('show', short_help='Show details for subscribers')
[email protected]('oformat', '--output-format', '-F',
+                type=click.Choice(['raw', 'json', 'table', 'dict']),
+                default=None, help='Format the output')
[email protected]('ostyle', '--output-style', '-S',
+                default=None, help='Style of the output (tabulate table format)')
[email protected]('userid', nargs=-1, metavar='[<userid>]')
+@pass_context
+def subscriber_show(ctx, oformat, ostyle, userid):
+    """Show details for subscribers
+
+    \b
+    Parameters:
+        [<userid>] - username, AoR or SIP URI for subscriber
+                   - it can be a list of userids
+                   - if not provided then all subscribers are shown
+    """
+    if not userid:
+        ctx.vlog('Showing all subscribers')
+        e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+        res = e.execute('select * from subscriber')
+        ioutils_dbres_print(ctx, oformat, ostyle, res)
+    else:
+        for u in userid:
+            udata = parse_user_spec(ctx, u)
+            ctx.vlog('Showing subscriber [%s@%s]', udata['username'], udata['domain'])
+            e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+            res = e.execute('select * from subscriber where username=%s and domain=%s', udata['username'], udata['domain'])
+            ioutils_dbres_print(ctx, oformat, ostyle, res)
+
+
+##
+#
+#
[email protected]('setattrs', short_help='Set a string attribute for a subscriber')
[email protected]('userid', metavar='<userid>')
[email protected]('attr', metavar='<attribute>')
[email protected]('val', metavar='<value>')
+@pass_context
+def subscriber_setattrs(ctx, userid, attr, val):
+    """Set a string attribute a subscriber
+
+    \b
+    Parameters:
+        <userid> - username, AoR or SIP URI for subscriber
+        <attribute> - the name of the attribute (column name in subscriber table)
+        <value> - the value to be set for the attribute
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.log('Updating subscriber [%s@%s] with str attribute [%s]=[%s]', udata['username'], udata['domain'], attr, val)
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    e.execute('update subscriber set {0}={1!r} where username={2!r} and domain={3!r}'.format(attr.encode('ascii','ignore'), val.encode('ascii','ignore'), udata['username'], udata['domain']))
+
+
+##
+#
+#
[email protected]('setattri', short_help='Set an integer attribute for a subscriber')
[email protected]('userid', metavar='<userid>')
[email protected]('attr', metavar='<attribute>')
[email protected]('val', metavar='<value>')
+@pass_context
+def subscriber_setattri(ctx, userid, attr, val):
+    """Set an integer attribute a subscriber
+
+    \b
+    Parameters:
+        <userid> - username, AoR or SIP URI for subscriber
+        <attribute> - the name of the attribute (column name in subscriber table)
+        <value> - the value to be set for the attribute
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.log('Updating subscriber [%s@%s] with int attribute [%s]=[%s]', udata['username'], udata['domain'], attr, val)
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    e.execute('update subscriber set {0}={1} where username={2!r} and domain={3!r}'.format(attr.encode('ascii','ignore'), val.encode('ascii','ignore'), udata['username'], udata['domain']))
+
+
+##
+#
+#
[email protected]('setattrnull', short_help='Set an attribute to NULL for a subscriber')
[email protected]('userid', metavar='<userid>')
[email protected]('attr', metavar='<attribute>')
+@pass_context
+def subscriber_setattrnull(ctx, userid, attr):
+    """Set an attribute to NULL for a subscriber
+
+    \b
+    Parameters:
+        <userid> - username, AoR or SIP URI for subscriber
+        <attribute> - the name of the attribute (column name in subscriber table)
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.log('Updating subscriber [%s@%s] with attribute [%s]=NULL', udata['username'], udata['domain'], attr)
+    e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+    e.execute('update subscriber set {0}=NULL where username={1!r} and domain={2!r}'.format(attr.encode('ascii','ignore'), udata['username'], udata['domain']))
+
+

+ 150 - 0
kamcli/commands/cmd_ul.py

@@ -0,0 +1,150 @@
+import click
+from sqlalchemy import create_engine
+from kamcli.ioutils import ioutils_dbres_print
+from kamcli.ioutils import ioutils_formats_list
+from kamcli.cli import pass_context
+from kamcli.cli import parse_user_spec
+from kamcli.iorpc import command_ctl
+
+
+##
+#
+#
[email protected]('ul', help='Manage user location records')
+@pass_context
+def cli(ctx):
+    pass
+
+
+##
+#
+#
[email protected]('show', short_help='Show details for location records in memory')
[email protected]('brief', '--brief', is_flag=True,
+            help='Show brief format of the records.')
[email protected]('table', '--table', default='location',
+            help='Name of location table (default: location)')
[email protected]('userid', nargs=-1, metavar='[<userid>]')
+@pass_context
+def ul_show(ctx, brief, table, userid):
+    """Show details for location records in memory
+
+    \b
+    Parameters:
+        [<userid>] - username, AoR or SIP URI for subscriber
+                   - it can be a list of userids
+                   - if not provided then all records are shown
+    """
+    if not userid:
+        ctx.vlog('Showing all records')
+        if brief:
+            command_ctl(ctx, 'ul_dump', [ 'brief' ])
+        else:
+            command_ctl(ctx, 'ul_dump', [ ])
+    else:
+        for u in userid:
+            udata = parse_user_spec(ctx, u)
+            ctx.vlog('Showing record for [%s@%s]', udata['username'], udata['domain'])
+            aor =  udata['username'] + '@' + udata['domain']
+            command_ctl(ctx, 'ul_show_contact', [ table, aor ])
+
+
+##
+#
+#
[email protected]('add', short_help='Add location record')
[email protected]('table', '--table', default='location',
+            help='Name of location table (default: location)')
[email protected]('expires', '--expires', type=int, default=0,
+            help='Expires value')
[email protected]('qval', '--q', type=float, default=1.0,
+            help='Q value')
[email protected]('cpath', '--path', default='',
+            help='Path value')
[email protected]('flags', '--flags', type=int, default=0,
+            help='Flags value')
[email protected]('bflags', '--bflags', type=int, default=0,
+            help='Branch flags value')
[email protected]('methods', '--methods', type=int, default=4294967295,
+            help='Methods value')
[email protected]('userid', nargs=1, metavar='<userid>')
[email protected]('curi', nargs=1, metavar='<contact-uri>')
+@pass_context
+def ul_add(ctx, table, expires, qval, cpath, flags, bflags, methods, userid, curi):
+    """Add location record
+
+    \b
+    Parameters:
+        <userid>       - username, AoR or SIP URI for subscriber
+        <contact-uri>  - contact SIP URI
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.vlog('Adding record for [%s@%s]', udata['username'], udata['domain'])
+    aor =  udata['username'] + '@' + udata['domain']
+    command_ctl(ctx, 'ul_add', [ table, aor, curi, expires, qval, cpath, flags, bflags, methods ])
+
+
+
+##
+#
+#
[email protected]('rm', short_help='Delete location records')
[email protected]('table', '--table', default='location',
+            help='Name of location table (default: location)')
[email protected]('userid', nargs=1, metavar='<userid>')
[email protected]('curi', nargs=-1, metavar='[<contact-uri>]')
+@pass_context
+def ul_rm(ctx, table, userid, curi):
+    """Show details for location records in memory
+
+    \b
+    Parameters:
+        <userid>         - username, AoR or SIP URI for subscriber
+        [<contact-uri>]  - contact SIP URI
+                         - optional, it can be a list of URIs
+    """
+    udata = parse_user_spec(ctx, userid)
+    ctx.vlog('Showing record for [%s@%s]', udata['username'], udata['domain'])
+    aor =  udata['username'] + '@' + udata['domain']
+    if curi:
+        for c in curi:
+            command_ctl(ctx, 'ul_rm', [ table, aor, c ])
+    else:
+        command_ctl(ctx, 'ul_rm', [ table, aor ])
+
+
+
+##
+#
+#
[email protected]('showdb', short_help='Show details for location records in database')
[email protected]('oformat', '--output-format', '-F',
+                type=click.Choice(ioutils_formats_list),
+                default=None, help='Format the output')
[email protected]('ostyle', '--output-style', '-S',
+                default=None, help='Style of the output (tabulate table format)')
[email protected]('userid', nargs=-1, metavar='[<userid>]')
+@pass_context
+def ul_showdb(ctx, oformat, ostyle, userid):
+    """Show details for location records in database
+
+    \b
+    Parameters:
+        [<userid>] - username, AoR or SIP URI for subscriber
+                   - it can be a list of userids
+                   - if not provided then all records are shown
+    """
+    if not userid:
+        ctx.vlog('Showing all records')
+        e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+        res = e.execute('select * from location')
+        ioutils_dbres_print(ctx, oformat, ostyle, res)
+    else:
+        for u in userid:
+            udata = parse_user_spec(ctx, u)
+            ctx.vlog('Showing records for [%s@%s]', udata['username'], udata['domain'])
+            e = create_engine(ctx.gconfig.get('db', 'rwurl'))
+            res = e.execute('select * from location where username=%s and domain=%s', udata['username'], udata['domain'])
+            ioutils_dbres_print(ctx, oformat, ostyle, res)
+
+

+ 187 - 0
kamcli/iorpc.py

@@ -0,0 +1,187 @@
+import os
+import sys
+import stat
+import time
+import threading
+import json
+from random import randint
+
+
+# Thread to listen on a reply fifo file
+#
+class IOFifoThread (threading.Thread):
+    def __init__(self, rplpath, oformat):
+        threading.Thread.__init__(self)
+        self.rplpath = rplpath
+        self.oformat = oformat
+        self.stop_signal = False
+
+    def run(self):
+        print "Starting to wait for reply on: " + self.rplpath
+        r = os.open( self.rplpath, os.O_RDONLY|os.O_NONBLOCK )
+        scount = 0
+        rcount = 0
+        wcount = 0
+        rdata = ""
+        while not self.stop_signal:
+            rbuf = os.read(r, 4096)
+            if rbuf == "":
+                if rcount != 0 :
+                    wcount += 1
+                    if wcount == 8:
+                        break
+                    time.sleep(0.100)
+                else:
+                    scount += 1
+                    if scount == 50:
+                        break
+                    time.sleep(0.100)
+            else:
+                rcount += 1
+                wcount = 0
+                rdata += rbuf
+
+        if rcount==0 :
+            print "timeout - nothing read"
+        else:
+            print
+            if self.oformat == "json":
+                print json.dumps(json.loads(rdata), indent=4, separators=(',', ': '))
+            else:
+                print rdata
+
+# :command:reply_fifo
+# p1
+# p2
+# p3
+# _empty_line_
+#
+def command_mi_fifo(ctx, dryrun, sndpath, rcvname, oformat, cmd, params):
+    scmd = ":" + cmd + ":" + rcvname + "\n"
+    for p in params:
+        if type(p) is int:
+            scmd += str(p) + "\n"
+        elif type(p) is float:
+            scmd += str(p) + "\n"
+        else:
+            if p == '':
+                scmd += ".\n"
+            else:
+                scmd += p + "\n"
+    scmd += "\n"
+    if dryrun:
+        print
+        print scmd
+        return
+
+    rcvpath = "/tmp/" + rcvname
+    if os.path.exists(rcvpath):
+        if stat.S_ISFIFO(os.stat(rcvpath).st_mode):
+            os.unlink(rcvpath)
+        else:
+            print "File with same name as reply fifo exists"
+            sys.exit()
+
+    os.mkfifo(rcvpath)
+    # create new thread to read from reply firo
+    tiofifo = IOFifoThread(rcvpath, oformat)
+    # start new threadd
+    tiofifo.start()
+
+    w = os.open(sndpath, os.O_WRONLY)
+    os.write(w, scmd)
+
+    waitrun = True
+    while waitrun:
+        try:
+            tiofifo.join(500)
+            if not tiofifo.isAlive():
+                waitrun = False
+                break
+        except KeyboardInterrupt:
+            print "Ctrl-c received! Sending kill to threads..."
+            tiofifo.stop_signal = True
+
+    os.unlink(rcvpath)
+
+
+#{
+# "jsonrpc": "2.0",
+#  "method": "command",
+#  "params": [p1, p2, p3],
+#  "reply_name": "kamailio_jsonrpc_reply_fifo",
+#  "id": 1
+#}
+#
+def command_jsonrpc_fifo(ctx, dryrun, sndpath, rcvname, oformat, cmd, params):
+    scmd = '{\n  "jsonrpc": "2.0",\n  "method": "' + cmd + '",\n'
+    if params:
+        scmd += '  "params": ['
+        comma = 0
+        for p in params:
+            if comma == 1:
+                scmd += ',\n'
+            else:
+                comma = 1
+            if type(p) is int:
+                scmd += str(p)
+            elif type(p) is float:
+                scmd += str(p)
+            else:
+                if p.startswith("i:") :
+                    scmd += p[2:]
+                elif p.startswith("s:") :
+                    scmd += '"' + p[2:] + '"'
+                else :
+                    scmd += '"' + p + '"'
+        scmd += '],\n'
+
+    scmd += '  "reply_name": "' + rcvname + '",\n'
+    scmd += '  "id": ' + str(randint(2,10000)) + '\n'
+    scmd += "}\n"
+    if dryrun:
+        print json.dumps(json.loads(scmd), indent=4, separators=(',', ': '))
+        return
+
+    rcvpath = "/tmp/" + rcvname
+    if os.path.exists(rcvpath):
+        if stat.S_ISFIFO(os.stat(rcvpath).st_mode):
+            os.unlink(rcvpath)
+        else:
+            print "File with same name as reply fifo exists"
+            sys.exit()
+
+    os.mkfifo(rcvpath)
+    # create new thread to read from reply firo
+    tiofifo = IOFifoThread(rcvpath, oformat)
+    # start new threadd
+    tiofifo.start()
+
+    w = os.open(sndpath, os.O_WRONLY)
+    os.write(w, scmd)
+
+    waitrun = True
+    while waitrun:
+        try:
+            tiofifo.join(500)
+            if not tiofifo.isAlive():
+                waitrun = False
+                break
+        except KeyboardInterrupt:
+            print "Ctrl-c received! Sending kill to threads..."
+            tiofifo.stop_signal = True
+
+    os.unlink(rcvpath)
+
+
+##
+#
+#
+def command_ctl(ctx, cmd, params):
+    if ctx.gconfig.get('ctl', 'type') == 'jsonrpc':
+        command_jsonrpc_fifo(ctx, False, "/var/run/kamailio/kamailio_jsonrpc_fifo",
+                "kamailio_jsonrpc_fifo_reply", "json", cmd, params)
+    else:
+        command_mi_fifo(ctx, False, "/var/run/kamailio/kamailio_fifo",
+                "kamailio_fifo_reply", "raw", cmd, params)
+

+ 50 - 0
kamcli/ioutils.py

@@ -0,0 +1,50 @@
+import os
+import sys
+import json
+# import pprint
+
+ioutils_tabulate_format = True
+try:
+    from tabulate import tabulate
+except ImportError, e:
+    ioutils_tabulate_format = False
+    pass # module doesn't exist, deal with it.
+
+
+ioutils_formats_list = ['raw', 'json', 'table', 'dict']
+
+##
+#
+#
+def ioutils_dbres_print(ctx, oformat, ostyle, res):
+    if oformat is None:
+        if ioutils_tabulate_format is True:
+            oformat = 'table'
+        else:
+            oformat = 'json'
+    else:
+       if oformat == 'table':
+            if ioutils_tabulate_format is False:
+                print "Package tabulate is not installed"
+                sys.exit()
+
+    if ostyle is None:
+        ostyle = 'grid'
+
+    if oformat == 'json':
+        for row in res:
+            print json.dumps(dict(row), indent=4)
+            print
+    elif oformat == 'dict':
+        for row in res:
+            print dict(row)
+            # pprint.pprint(dict(row), indent=4)
+            print
+    elif oformat == 'table':
+        allrows = res.fetchall()
+        gstring = tabulate(allrows, headers=res.keys(), tablefmt=ostyle)
+        print(gstring)
+    else:
+        allrows = res.fetchall()
+        print(allrows)
+

+ 29 - 0
kamcli/kamcli.ini

@@ -0,0 +1,29 @@
+[main]
+domain=test.com
+
+[db]
+type=mysql
+driver=mysqldb
+host=localhost
+dbname=kamailio
+rwuser=kamailio
+rwpassword=kamailiorw
+rouser=kamailio
+ropassword=kamailiorw
+rwurl=%(type)s+%(driver)s://%(rwuser)s:%(rwpassword)s@%(host)s/%(dbname)s
+rourl=%(type)s+%(driver)s://%(rouser)s:%(ropassword)s@%(host)s/%(dbname)s
+
+[ctl]
+type=mi
+
+[mi]
+transport=fifo
+path=/var/run/kamailio/kamailio_fifo
+rplnamebase=kamailio_fifo_reply
+rpldir=/tmp
+
+[jsonrpc]
+transport=fifo
+path=/var/run/kamailio/kamailio_jsonrpc_fifo
+rplnamebase=kamailio_json_fifo_reply
+rpldir=/tmp

+ 16 - 0
setup.py

@@ -0,0 +1,16 @@
+from setuptools import setup
+
+setup(
+    name='kamcli',
+    version='1.0',
+    packages=['kamcli', 'kamcli.commands'],
+    include_package_data=True,
+    install_requires=[
+        'click',
+        'sqlalchemy',
+    ],
+    entry_points='''
+        [console_scripts]
+        kamcli=kamcli.cli:cli
+    ''',
+)