I am happy to announce release of ejabberdctl.py - Python client for Ejabberd XML-RPC Administration API.

I have been using Ejabberd for a number of years. It is a powerful XMPP server. One of its core features is Administration API which is exposed via the ejabberdctl command, but it is also exposed as XML-RPC API and since Ejabberd 15.09 also as ReST API with OAuth 2.0 support.

Last year, I started using pyejabberd to automate some administration tasks via the XML-RPC API in Python.

When Ejabberd 15.09 introduced the ReST API with OAuth 2.0 support, it unfortunately also meant an introduction of a few backwards incompatible changes to the Administration API, which made my life a bit more exciting.


I tried to get pyejabberd to work with both the old and the new version of the administration commands, but it proved to be a difficult task.

I quite liked the simplicity of Ejabberd’s Python XML-RPC example script:

# Modified version of the Ejabberd XML-RPC example Python script
import xmlrpclib

server = xmlrpclib.ServerProxy('http://127.0.0.1:4560')

CREDENTIALS = {'user': 'admin',
               'password': 'A$swOrd%^',
               'server': 'example.com',
               'admin', True}

def ctl(command, payload):
    fn = getattr(server, command)
    return fn(CREDENTIALS, payload)

result = ctl('status')
result = ctl('get_roster', {'user': 'test',
                            'server': 'example.com'})

That gave me an idea to write a new library that would “just work” ie. automatically handle differences between the different versions of the Administration API.

I wrote a simple client object and for implementation of the API commands I used output of ejabberdctl help command.

I made a few VIM macros to convert the help output into class methods and using a few more macros I also added skeleton for tests.

I added implementation for more than a half of available commands and wrote basic tests for nearly half of the commands that I implemented.

I tested the new code with pre/post 15.09 versions of Ejabberd to make sure it handles all differences correctly.

ejabberdctl.py seemed like a good name for the new library, because it basically provides the same functionality as ejabberdctl does, but in Python.

In terms of coverage in numbers, 72 out of 126 commands are implemented. 54 to go. 31 out of 126 tests are implemented. 95 to go. More complex tests should be eventually written, too.

The code is available at https://gitlab.com/markuz/ejabberdctl.py.

Packaged version is available at https://pypi.python.org/pypi/ejabberdctl.py and can be installed via pip:

pip install ejabberdctl.py

I wanted to try out the new package on a testing Debian server.

There is a few things that need to be set in ejabberd.yml so I wanted to see what it takes to get ejabberdctl.py going on a vanilla installation of Ejabberd 16.09.

Firstly, I installed Ejabberd 16.09 from a Debian package that I got from the Ejabberd download archive:

marek@kuziel:~$ sudo su -
root@kuziel:~# wget https://www.process-one.net/downloads/ejabberd/16.09/ejabberd_16.09-0_amd64.deb
root@kuziel:~# dpkg -i ejabberd_16.09-0_amd64.deb

Ejabberd got installed to /opt/ejabberd-16.09. I started it up, keeping in the default configuration, which has XML-RPC disabled just to see ejabberdctl.py fail to connect:

root@kuziel:~# cd /opt/ejabberd-16.09/
root@kuziel:/opt/ejabberd-16.09# ./bin/ejabberdctl start   # start may take few moments
root@kuziel:/opt/ejabberd-16.09# ./bin/ejabberdctl status  # check few times until it says...
The node ejabberd@localhost is started with status: started
ejabberd 16.09 is running in that node

I installed the new ejabberdctl.py package into a testing virtualenv in pyenv:

marek@kuziel:~/.pyenv/versions/ejabberdctlpy_test$ pip install ejabberdctl.py

Made a test script:

from ejabberdctl import ejabberdctl
ctl = ejabberdctl('kuziel.nz', 'marek', 'a$Kw0rD%^')
print ctl.status()

And ran it:

marek@kuziel:~/.pyenv/versions/ejabberdctlpy_test$ python test.py
Traceback (most recent call last):
  File "test.py", line 4, in <module>
    print ctl.status()
  File "/home/marek/.pyenv/versions/ejabberdctlpy_test/lib/python2.7/site-packages/ejabberdctl/ejabberdctl.py", line 725, in status
    return self.ctl('status')
  File "/home/marek/.pyenv/versions/ejabberdctlpy_test/lib/python2.7/site-packages/ejabberdctl/ejabberdctl.py", line 40, in ctl
    return fn(self.params)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/xmlrpclib.py", line 1240, in __call__
    return self.__send(self.__name, args)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/xmlrpclib.py", line 1599, in __request
    verbose=self.__verbose
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/xmlrpclib.py", line 1280, in request
    return self.single_request(host, handler, request_body, verbose)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/xmlrpclib.py", line 1308, in single_request
    self.send_content(h, request_body)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/xmlrpclib.py", line 1456, in send_content
    connection.endheaders(request_body)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/httplib.py", line 1049, in endheaders
    self._send_output(message_body)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/httplib.py", line 893, in _send_output
    self.send(msg)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/httplib.py", line 855, in send
    self.connect()
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/httplib.py", line 832, in connect
    self.timeout, self.source_address)
  File "/home/marek/.pyenv/versions/2.7.10/lib/python2.7/socket.py", line 575, in create_connection
    raise err
socket.error: [Errno 111] Connection refused

XML-RPC is not configured and running on 127.0.0.1, port 4560 so I got correctly Connection refused back.

I changed the configuration in /opt/ejabberd-16.09/conf/ejabberd.yml and enabled XML-RPC by uncommenting the XML-RPC section, knowing this will fail, too:

##
## To handle XML-RPC requests that provide admin credentials:
##
-
 port: 4560
 module: ejabberd_xmlrpc
 maxsessions: 10
 timeout: 5000
 access_commands:
   admin:
     commands: all
     options: []

I stopped and then started Ejabberd to load in the new configuration:

root@kuziel:/opt/ejabberd-16.09# ./bin/ejabberdctl stop    # stop may take few moments
root@kuziel:/opt/ejabberd-16.09# ./bin/ejabberdctl status  # check few times until it says...
Failed RPC connection to the node ejabberd@localhost: nodedown

Commands to start an ejabberd node:
  start      Start an ejabberd node in server mode
  debug      Attach an interactive Erlang shell to a running ejabberd node
  iexdebug   Attach an interactive Elixir shell to a running ejabberd node
  live       Start an ejabberd node in live (interactive) mode
  iexlive    Start an ejabberd node in live (interactive) mode, within an Elixir shell
  foreground Start an ejabberd node in server mode (attached)

Optional parameters when starting an ejabberd node:
  --config-dir dir   Config ejabberd:    /opt/ejabberd-16.09/conf
  --config file      Config ejabberd:    /opt/ejabberd-16.09/conf/ejabberd.yml
  --ctl-config file  Config ejabberdctl: /opt/ejabberd-16.09/conf/ejabberdctl.cfg
  --logs dir         Directory for logs: /opt/ejabberd-16.09/logs
  --spool dir        Database spool dir: /opt/ejabberd-16.09/database/ejabberd@localhost
  --node nodename    ejabberd node name: ejabberd@localhost

root@kuziel:/opt/ejabberd-16.09# ./bin/ejabberdctl start   # start may take few moments
root@kuziel:/opt/ejabberd-16.09# ./bin/ejabberdctl status  # check few times until it says...
The node ejabberd@localhost is started with status: started
ejabberd 16.09 is running in that node

And re-ran the test.py:

marek@kuziel:~/.pyenv/versions/ejabberdctlpy_test$ python test.py
Traceback (most recent call last):
  File "test.py", line 4, in <module>
    print ctl.status()
  File "/home/marek/.pyenv/versions/ejabberdctlpy_test/lib/python2.7/site-packages/ejabberdctl/ejabberdctl.py", line 725, in status
    return self.ctl('status')
  File "/home/marek/.pyenv/versions/ejabberdctlpy_test/lib/python2.7/site-packages/ejabberdctl/ejabberdctl.py", line 54, in ctl
    raise Exception(e)
Exception: <Fault -118: "Error -118\nA problem '{error,account_unprivileged}' occurred executing the command status with arguments\n[]">

Exception: <Fault -118: “Error -118nA problem ‘{error,account_unprivileged}’ occurred executing the command status with argumentsn[]”> means that the test script could not execute the XML-RPC command (status), because of insufficient permissions.

The default XML-RPC config has access_commands set to admin and further down in the acl section, admin has myself set as a member.

The ejabberd.yml parts related to XML-RPC setup in ejabberd.yml currently look like this:

##
## listen: The ports ejabberd will listen on, which service each is handled
## by and what options to start it with.
##
listen:
  ##
  ## To handle XML-RPC requests that provide admin credentials:
  ##
  -
   port: 4560
   module: ejabberd_xmlrpc
   maxsessions: 10
   timeout: 5000
   access_commands:
     admin:
       commands: all
       options: []

###.   ====================
###'   ACCESS CONTROL LISTS
acl:
  ##
  ## The 'admin' ACL grants administrative privileges to XMPP accounts.
  ## You can put here as many accounts as you want.
  ##
  admin:
    user:
      - "marek@kuziel.nz"

Even though it looks like everything is setup correctly, the test script still fails with Exception: <Fault -118: “Error -118nA problem ‘{error,account_unprivileged}’ occurred executing the command status with argumentsn[]”>

The problem is that admin in the two places in the config means two different things and they need to be binded in the missing access section.

The access_commands group can be called anything, so here is modified version, which should better illustrate how the setup is tied together:

##
## listen: The ports ejabberd will listen on, which service each is handled
## by and what options to start it with.
##
listen:
  ##
  ## To handle XML-RPC requests that provide admin credentials:
  ##
  -
   port: 4560
   module: ejabberd_xmlrpc
   maxsessions: 10
   timeout: 5000
   access_commands:
     xmlrpc_access:
       commands: all
       options: []


## allow xmlrpc_access to execute admin commands
commands_admin_access: xmlrpc_access

## allow users in admin group to commands with xmlrpc_access
access:
  xmlrpc_access:
    admin: allow


###.   ====================
###'   ACCESS CONTROL LISTS
acl:
  ##
  ## The 'admin' ACL grants administrative privileges to XMPP accounts.
  ## You can put here as many accounts as you want.
  ##
  admin:
    user:
      - "marek@kuziel.nz"

After applying the changes above to ejabberd.yml, I restarted Ejabberd and re-ran the test:

marek@kuziel:~/.pyenv/versions/ejabberdctlpy_test$ python test.py
{'res': 0, 'text': 'The node ejabberd@localhost is started. Status: startedejabberd 16.09 is running in that node'}

There is still plenty of code missing, but the current implementation covers all important parts of the available command set.

I welcome any contributions. Please have a look at todo.txt for the current list of open tasks to do.