Browse Source

[master] Merge branch 'trac5137' (kea-shell)

Tomek Mrugalski 8 years ago
parent
commit
816dc5ccfa

+ 36 - 38
configure.ac

@@ -473,44 +473,36 @@ case "$host" in
         ;;
 esac
 
-m4_define([_AM_PYTHON_INTERPRETER_LIST], [python python3.4 python3.3 python3.2 python3.1 python3])
-AC_ARG_WITH([pythonpath],
-AC_HELP_STRING([--with-pythonpath=PATH],
-  [specify an absolute path to python executable when automatic version check (incorrectly) fails]),
-  [python_path="$withval"], [python_path="auto"])
-if test "$python_path" = auto; then
-        AM_PATH_PYTHON([3.1],,[PYTHON=no])
+
+# Kea-shell is written in python. It can work with python 2.7 or any 3.x.
+# It may likely work with earlier versions, but 2.7 was the oldest one we tested
+# it with. We require python only if kea-shell was enabled. It is disabled
+# by default to not introduce hard dependency on python.
+AC_ARG_ENABLE(shell, [AC_HELP_STRING([--enable-shell],
+  [enable kea-shell, a text management client for Control Agent [default=no]])],
+  enable_shell=$enableval, enable_shell=no)
+
+if test "x$enable_shell" != xno ; then
+# If kea-shell is enabled, we really need python. 2.7 or anything newer will do.
+# We try to find 3.x first. If not found, we can do with 2.7.
+  AM_PATH_PYTHON([3], [found="yes"], [found="no"])
+  if test "x$found" = xno ; then
+    AM_PATH_PYTHON([2.7])
+  fi
 else
-        # Older versions of automake can't handle python3 well.  This is an
-        # in-house workaround for them.
-        PYTHON=$python_path
-        AC_SUBST(PYTHON)
-        PYTHON_PREFIX='${prefix}'
-        AC_SUBST(PYTHON_PREFIX)
-        PYTHON_EXEC_PREFIX='$(exec_prefix)'
-        AC_SUBST(PYTHON_EXEC_PREFIX)
-        PYTHON_VERSION=[`$PYTHON -c "import sys; sys.stdout.write(sys.version[:3])"`]
-        if test `echo "$PYTHON_VERSION >= 3.1" | bc` != 1 ; then
-                AC_MSG_ERROR(["Python version too old: $PYTHON_VERSION, need 3.1 or higher"])
-        fi
-        AC_SUBST(PYTHON_VERSION)
-        PYTHON_PLATFORM=`$PYTHON -c "import sys; print(sys.platform)"`
-        AC_SUBST(PYTHON_PLATFORM)
-        pythondir='${prefix}/lib/python'$PYTHON_VERSION'/site-packages'
-        AC_SUBST(pythondir)
-        pkgpythondir='${pythondir}/'$PACKAGE
-        AC_SUBST(pkgpythondir)
-        pyexecdir='${exec_prefix}/lib/python'$PYTHON_VERSION'/site-packages'
-        AC_SUBST(pyexecdir)
-        pkgpyexecdir='${pyexecdir}/'$PACKAGE
-        AC_SUBST(pkgpyexecdir)
+  PYTHON=no
 fi
 
+# Export to makefiles the info whether we have shell enabled or not
+AM_CONDITIONAL(KEA_SHELL, test x$enable_shell != xno)
+
 # produce PIC unless we disable shared libraries. need this for python bindings.
 if test $enable_shared != "no" -a "X$GXX" = "Xyes"; then
    KEA_CXXFLAGS="$KEA_CXXFLAGS -fPIC"
 fi
 
+
+
 AC_SUBST(KEA_CXXFLAGS)
 
 # Checks for libraries.
@@ -1653,6 +1645,11 @@ AC_CONFIG_FILES([compatcheck/Makefile
                  src/bin/admin/tests/pgsql_tests.sh
                  src/bin/admin/tests/cql_tests.sh
                  src/bin/agent/tests/test_libraries.h
+                 src/bin/shell/Makefile
+                 src/bin/shell/kea-shell
+                 src/bin/shell/tests/Makefile
+                 src/bin/shell/tests/shell_process_tests.sh
+                 src/bin/shell/tests/shell_unittest.py
                  src/hooks/Makefile
                  src/hooks/dhcp/Makefile
                  src/hooks/dhcp/user_chk/Makefile
@@ -1743,11 +1740,14 @@ AC_CONFIG_FILES([compatcheck/Makefile
 ])
 
  AC_CONFIG_COMMANDS([permissions], [
+           chmod +x src/bin/admin/kea-admin
            chmod +x src/bin/dhcp4/tests/dhcp4_process_tests.sh
            chmod +x src/bin/dhcp6/tests/dhcp6_process_tests.sh
            chmod +x src/bin/keactrl/keactrl
            chmod +x src/bin/keactrl/tests/keactrl_tests.sh
-           chmod +x src/bin/admin/kea-admin
+           chmod +x src/bin/shell/kea-shell
+           chmod +x src/bin/shell/tests/shell_process_tests.sh
+           chmod +x src/bin/shell/tests/shell_unittest.py
            chmod +x src/lib/dns/gen-rdatacode.py
            chmod +x src/lib/log/tests/console_test.sh
            chmod +x src/lib/log/tests/destination_test.sh
@@ -1822,19 +1822,16 @@ END
 if test "$PYTHON" != "no" ; then
 cat >> config.report << END
 
-Python3:
+Python:
+  PYTHON:          ${PYTHON}
   PYTHON_VERSION:  ${PYTHON_VERSION}
-  PYTHON_INCLUDES: ${PYTHON_INCLUDES}
-  PYTHON_CXXFLAGS: ${PYTHON_CXXFLAGS}
-  PYTHON_LDFLAGS:  ${PYTHON_LDFLAGS}
-  PYTHON_LIB:      ${PYTHON_LIB}
 
 END
 else
 cat >> config.report << END
 
-Python3:
-  not installed
+Python:
+  PYTHON_VERSION:  not needed (because kea-shell is disabled)
 
 END
 fi
@@ -1936,6 +1933,7 @@ Developer:
   Logger checks: $enable_logger_checks
   Generate Documentation: $enable_generate_docs
   Parser Generation: $enable_generate_parser
+  Kea-shell: $enable_shell
 
 END
 

+ 1 - 1
src/bin/Makefile.am

@@ -1,4 +1,4 @@
 # The following build order must be maintained.
-SUBDIRS = dhcp4 dhcp6 d2 agent perfdhcp admin lfc keactrl
+SUBDIRS = dhcp4 dhcp6 d2 agent perfdhcp admin lfc keactrl shell
 
 check-recursive: all-recursive

+ 2 - 1
src/bin/agent/tests/Makefile.am

@@ -5,7 +5,6 @@ SHTESTS += ca_process_tests.sh
 
 noinst_SCRIPTS = ca_process_tests.sh
 EXTRA_DIST  = ca_process_tests.sh.in
-noinst_LTLIBRARIES = libbasic.la
 
 # test using command-line arguments, so use check-local target instead of TESTS
 check-local:
@@ -40,6 +39,8 @@ TESTS_ENVIRONMENT = \
 TESTS =
 if HAVE_GTEST
 
+noinst_LTLIBRARIES = libbasic.la
+
 TESTS += ca_unittests
 
 ca_unittests_SOURCES  = ca_cfg_mgr_unittests.cc

+ 3 - 0
src/bin/shell/.gitignore

@@ -0,0 +1,3 @@
+kea-shell
+kea-shell.8
+*.pyc

+ 49 - 0
src/bin/shell/Makefile.am

@@ -0,0 +1,49 @@
+SUBDIRS = . tests
+
+EXTRA_DIST =
+
+if KEA_SHELL
+
+# Kea-shell is enabled, here are proper rules for it.
+kea_shell_PYTHON = kea_conn.py kea_connector2.py kea_connector3.py
+kea_shelldir = @localstatedir@/@PACKAGE@
+
+bin_SCRIPTS = kea-shell
+
+else
+
+# Kea-shell is disabled, simply keep the files for make dist
+EXTRA_DIST += kea-shell kea_conn.py kea_connector2.py kea_connector3.py
+
+endif
+
+CLEANFILES = *.pyc
+
+man_MANS = kea-shell.8
+DISTCLEANFILES = $(man_MANS)
+EXTRA_DIST += $(man_MANS) kea-shell.xml
+
+if GENERATE_DOCS
+kea-shell.8: kea-shell.xml
+	@XSLTPROC@ --novalid --xinclude --nonet -o $@ \
+        http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl \
+	$(srcdir)/kea-shell.xml
+
+else
+
+$(man_MANS):
+	@echo Man generation disabled.  Creating dummy $@.  Configure with --enable-generate-docs to enable it.
+	@echo Man generation disabled.  Remove this file, configure with --enable-generate-docs, and rebuild Kea > $@
+
+endif
+
+install-data-local:
+	$(mkinstalldirs) $(DESTDIR)/@localstatedir@/@PACKAGE@
+
+install-data-hook:
+	-chmod 2770 $(DESTDIR)/@localstatedir@/@PACKAGE@
+
+CLEANDIRS = __pycache__
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 112 - 0
src/bin/shell/kea-shell.in

@@ -0,0 +1,112 @@
+#!@PYTHON@
+
+# Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Text client for Control Agent process
+"""
+
+# First, let's import the right kea_connector.
+# We have two versions: one for python 2.x and another for python 3.x.
+# Sadly, there's no unified way to handle http connections. The recommended
+# way is to use Requests (http://docs.python-requests.org/en/master/), but
+# that's a stand alone package that requires separate installation. One of
+# the design requirements was to not require any additional packages, so
+# the code uses standard libraries available in python. Hence two versions.
+import sys
+import signal
+import argparse
+
+from kea_conn import CARequest # CAResponse
+
+if sys.version_info[0] == 2:
+    # This is Python 2.x
+    import kea_connector2 as kea_connector
+elif sys.version_info[0] == 3:
+    # This is Python 3.x
+    import kea_connector3 as kea_connector
+else:
+    # This is... have no idea what it is.
+    raise SystemExit("Unknown python version:" + str(sys.version_info[0]))
+
+def timeout_handler(signum, frame):
+    """Connection timeoout handler"""
+    del signum, frame
+    print("Connection timeout")
+    sys.exit(1)
+
+VERSION = "@PACKAGE_VERSION@"
+
+def shell_body():
+    """
+    Second step: Need to parse command line parameters. We will use
+    argparse for that purpose. It does great job with having default
+    values, taking care of the help and sanity checking input
+    parameters.
+    """
+    parser = argparse.ArgumentParser(description='kea-shell is a simple text '
+                                     'client that uses REST interface to '
+                                     'connect to Kea Control Agent.')
+    parser.add_argument('--host', type=str, default='127.0.0.1',
+                        help='hostname of the CA to connect to '
+                        '(defaul:; 127.0.0.1)')
+    parser.add_argument('--port', type=int, default=8000,
+                        help='TCP port of the CA to connect to '
+                        '(default: 8000)')
+    parser.add_argument('--timeout', type=int, default='10',
+                        help='Timeout (in seconds) when attempting to '
+                        'connect to CA (default: 10)')
+    parser.add_argument('command', type=str, nargs="?",
+                        default='list-commands',
+                        help='command to be executed. If not specified, '
+                        '"list-commands" is used')
+    parser.add_argument('-v', action="store_true", help="Prints version")
+    cmd_args = parser.parse_args()
+
+    if cmd_args.v:
+        print(VERSION)
+        exit(0)
+
+    # Ok, now it's time to put the parameters parsed into the structure to be
+    # used by the connection.
+    params = CARequest()
+    params.command = cmd_args.command
+    params.http_host = cmd_args.host
+    params.http_port = cmd_args.port
+    params.timeout = cmd_args.timeout
+    params.version = VERSION
+
+    params.generate_body()
+    params.generate_headers()
+
+    # Load command processor
+    # @todo - command specific processing will be added as part of
+    # future work (either #5138 or #5139, whichever is implemented
+    # first)
+
+    # Read parameters from stdin (they're optional for some commands)
+    for line in sys.stdin:
+        params.params += line
+
+    # Set the timeout timer. If the connection takes too long,
+    # it will send a signal to us.
+    signal.signal(signal.SIGALRM, timeout_handler)
+    signal.alarm(params.timeout)
+
+    # Ok, everything is ready. Let's send the command and get a response.
+    try:
+        resp = kea_connector.send_to_control_agent(params)
+    except Exception as exc:
+        print("Failed to run: " + str(exc))
+        sys.exit(1)
+
+    resp.print_response()
+
+    sys.exit(0)
+
+if __name__ == "__main__":
+    shell_body()

+ 227 - 0
src/bin/shell/kea-shell.xml

@@ -0,0 +1,227 @@
+<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
+               "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
+               [<!ENTITY mdash "&#8212;">]>
+<!--
+ - Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+ -
+ - This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/.
+-->
+
+<refentry>
+
+  <refentryinfo>
+    <productname>ISC Kea</productname>
+    <date>2017 Mar 8</date>
+    <edition>1.2.0</edition>
+    <author>
+      <contrib>The Kea software has been written by a number of
+        engineers working for ISC: Tomek Mrugalski, Stephen Morris, Marcin
+        Siodelski, Thomas Markwalder, Francis Dupont, Jeremy C. Reed,
+        Wlodek Wencel and Shawn Routhier.  That list is roughly in the
+        chronological order in which the authors made their first
+        contribution. For a complete list of authors and
+        contributors, see AUTHORS file.</contrib>
+      <orgname>Internet Systems Consortium, Inc.</orgname>
+    </author>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>kea-shell</refentrytitle>
+    <manvolnum>8</manvolnum>
+    <refmiscinfo class="manual">Kea</refmiscinfo>
+  </refmeta>
+
+  <refnamediv>
+    <refname>kea-shell</refname>
+    <refpurpose>Text client for Control Agent process</refpurpose>
+  </refnamediv>
+
+  <docinfo>
+    <copyright>
+      <year>2016</year>
+      <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
+    </copyright>
+  </docinfo>
+
+  <refsynopsisdiv>
+    <cmdsynopsis>
+      <command>kea-shell</command>
+      <arg><option>-h</option></arg>
+      <arg><option>-v</option></arg>
+      <arg><option>--host</option></arg>
+      <arg><option>--port</option></arg>
+      <arg><option>--timeout</option></arg>
+      <arg><option>command</option></arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+
+  <refsect1>
+    <title>DESCRIPTION</title>
+    <para>
+      The <command>kea-shell</command> provides a REST client for the
+      Kea Control Agent (CA). It takes command as a command-line parameter
+      that is being sent to CA with proper JSON
+      encapsulation. Optional parameters may be specified on the
+      standard input. The request it sent of HTTP and a response is
+      retrieved. That response is displayed out on the standard output.
+    </para>
+
+  </refsect1>
+
+  <refsect1>
+    <title>ARGUMENTS</title>
+
+    <para>The arguments are as follows:</para>
+
+    <variablelist>
+
+      <varlistentry>
+        <term><option>-h</option></term>
+        <listitem><para>
+          Displays help regarding command line parameters.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>-v</option></term>
+        <listitem><para>
+          Display the version.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--host</option></term>
+        <listitem><para>
+          Specifies the host to connect to. Control Agent must be
+          running at specified host. If not specified, 127.0.0.1 is used.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--port</option></term>
+        <listitem><para>
+          Specifies the TCP port to connect to. Control Agent must be
+          listening at specified port. If not specified, 8000 is used.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--timeout</option></term>
+        <listitem><para>
+          Specifies the connection timeout in seconds. If not
+          specified, 10 (seconds) is used.
+        </para></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>command</option></term>
+        <listitem><para>
+          Specifies the command to be sent to CA. If not
+          specified, "list-commands" is used.
+        </para></listitem>
+      </varlistentry>
+
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>DOCUMENTATION</title>
+    <para>Kea comes with an extensive Kea User's Guide documentation
+      that covers all aspects of running the Kea software -
+      compilation, installation, configuration, configuration examples
+      and many more. Kea also features a Kea Messages Manual, which
+      lists all possible messages Kea can print with a brief
+      description for each of them. Both documents are typically
+      available in various formats (txt, html, pdf) with your Kea
+      distribution. The on-line version is available at
+      http://kea.isc.org/docs/.</para>
+    <para>
+      Kea source code is documented in the Kea Developer's Guide. It's
+      on-line version is available at http://kea.isc.org. Please
+      follow Developer's Guide link.
+    </para>
+    <para>
+      Kea project website is available at: http://kea.isc.org.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>MAILING LISTS AND SUPPORT</title>
+    <para>
+      There are two mailing lists available for Kea project. kea-users
+      (kea-users at lists.isc.org) is intended for Kea users, while kea-dev
+      (kea-dev at lists.isc.org) is intended for Kea developers, prospective
+      contributors and other advanced users.  Both lists are available at
+      http://lists.isc.org. The community provides best effort type of support
+      on both of those lists.
+    </para>
+    <para>
+      ISC provides professional support for Kea services. See
+      https://www.isc.org/kea/ for details.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>HISTORY</title>
+    <para>
+      The <command>kea-shell</command> was first coded in March 2017
+      by Tomek Mrugalski.
+    </para>
+  </refsect1>
+
+  <refsect1>
+    <title>SEE ALSO</title>
+    <para>
+      <citerefentry>
+        <refentrytitle>kea-ctrl-agent</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citerefentry>
+        <refentrytitle>kea-dhcp4</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citerefentry>
+        <refentrytitle>kea-dhcp6</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citerefentry>
+        <refentrytitle>kea-dhcp-ddns</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citerefentry>
+        <refentrytitle>kea-admin</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citerefentry>
+        <refentrytitle>keactrl</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citerefentry>
+        <refentrytitle>perfdhcp</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citerefentry>
+        <refentrytitle>kea-lfc</refentrytitle>
+        <manvolnum>8</manvolnum>
+      </citerefentry>,
+
+      <citetitle>Kea Administrator's Guide</citetitle>.
+
+    </para>
+  </refsect1>
+
+</refentry><!--
+ - Local variables:
+ - mode: sgml
+ - End:
+-->

+ 89 - 0
src/bin/shell/kea_conn.py

@@ -0,0 +1,89 @@
+# Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+This file contains classes used for communication with Control Agent.
+"""
+
+class CARequest:
+    """
+    This class defines the HTTP request to be sent.
+    The supported parameters listed are:
+     - path (specifies the path on the server, CA uses only /)
+     - http_host - hostname of the CA
+     - http-port - TCP port of the CA
+     - command - specifies the command to send (e.g. list-commands)
+     - timeout - timeout (in ms)
+     - headers - extra HTTP headers may be added here
+     - version - version to be reported in HTTP header
+    """
+    path = '/'
+    http_host = ''
+    http_port = 0
+    command = ''
+    timeout = 0
+    params = ''
+    headers = {}
+    version = ""
+    # This is a storage for generated command (input data to be sent over POST)
+    content = ''
+
+    def generate_body(self):
+        """
+        Generates the content, out of specified command line
+        and optional content.
+        @todo: Add support for parameters
+        this stores the output in self.content
+        """
+        self.content = '{ "command": "' + self.command + '"'
+        if len(self.params):
+            self.content += ', "parameters": { ' + self.params + ' }'
+        self.content += ' }'
+
+    def generate_headers(self):
+        """
+        Generate HTTP headers
+
+        In particular, this method generates Content-Length and its value.
+        """
+        self.headers['Content-Type'] = 'application/json'
+        self.headers['User-Agent'] = "Kea-shell/%s"%(self.version)
+        self.headers['Accept'] = '*/*'
+        self.headers['Content-Length'] = "%d"%(len(self.content))
+
+
+class CAResponse:
+    """
+    This class represents the HTTP response
+    """
+
+    def __init__(self, status, reason, body):
+        """
+        Constructor
+
+        Three mandatory parameters are:
+         status - numerical number the describe the status (e.g. 200 = OK)
+         reason - textual explanation of what happened
+         body - the actual body structure of the response
+        """
+        self.status = status
+        self.reason = reason
+        self.body = body
+
+    status = 0
+    reason = ''
+    body = ''
+
+    def print_response(self, debug=False):
+        """
+        Used for debugging
+
+        if debug is true, this prints even more information
+        """
+        if debug:
+            print(self.status)
+            print(self.reason)
+        print(self.body)

+ 41 - 0
src/bin/shell/kea_connector2.py

@@ -0,0 +1,41 @@
+# Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+This is PYTHON 2.x version of HTTP connection establishment
+"""
+
+import httplib
+
+from kea_conn import CAResponse # CARequest
+
+def send_to_control_agent(params):
+    """ Sends a request to Control Agent, receives a response and returns it."""
+
+    # Establish HTTP connection first.
+    conn = httplib.HTTPConnection(params.http_host, params.http_port)
+    conn.connect()
+
+    # Use POST to send it
+    _ = conn.putrequest('POST', params.path)
+
+    # Send the headers first
+    for k in params.headers:
+        conn.putheader(k, params.headers[k])
+    conn.endheaders()
+
+    # Send the body (i.e. the actual content)
+    conn.send(params.content)
+
+    # Now get the response
+    resp = conn.getresponse()
+
+    # Now get the response details, put it in CAResponse and
+    # return it
+    result = CAResponse(resp.status, resp.reason, resp.read())
+    conn.close()
+
+    return result

+ 34 - 0
src/bin/shell/kea_connector3.py

@@ -0,0 +1,34 @@
+# Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+This is PYTHON 3.x version of HTTP connection establishment
+"""
+
+import urllib.request
+
+from kea_conn import CAResponse # CARequest
+
+def send_to_control_agent(params):
+    """ Sends a request to Control Agent, receives a response and returns it."""
+
+    # First, create the URL
+    url = "http://" + params.http_host + ":"
+    url += str(params.http_port) + str(params.path)
+
+    # Now preprare the request (URL, headers and body)
+    req = urllib.request.Request(url=url,
+                                 data=str.encode(params.content),
+                                 headers=params.headers)
+
+    # Establish connection, send the request.
+    resp = urllib.request.urlopen(req)
+
+    # Now get the response details, put it in CAResponse and return it
+    result = CAResponse(resp.getcode(), resp.reason,
+                        resp.read().decode("utf-8"))
+
+    return result

+ 1 - 0
src/bin/shell/tests/.gitignore

@@ -0,0 +1 @@
+shell_process_tests.sh

+ 42 - 0
src/bin/shell/tests/Makefile.am

@@ -0,0 +1,42 @@
+PYTESTS =
+SHTESTS =
+EXTRA_DIST =
+
+if KEA_SHELL
+
+# Ok, shell is enabled. Let's run those tests.
+PYTESTS += shell_unittest.py
+SHTESTS += shell_process_tests.sh
+noinst_SCRIPTS = $(PYTESTS) $(SHTESTS)
+
+# test using command-line arguments, so use check-local target instead of TESTS
+check-local: check-shell check-python
+
+check-python:
+	@for pytest in $(PYTESTS) ; do \
+	echo Running python test: $$pytest ; \
+	chmod +x $(abs_builddir)/$$pytest ; \
+	PYTHONPATH=$(PYTHONPATH):$(abs_top_builddir)/src/bin/shell python $(abs_builddir)/$$pytest || exit ; \
+	done
+
+check-shell:
+	@for shtest in $(SHTESTS) ; do \
+	echo Running shell test: $$shtest ; \
+	export KEA_LOCKFILE_DIR=$(abs_top_builddir); \
+	export KEA_PIDFILE_DIR=$(abs_top_builddir); \
+	${SHELL} $(abs_builddir)/$$shtest || exit ; \
+	done
+
+else
+
+# Nope, shell is disabled. Let's keep the files in EXTRA_DIST, so they get
+# included in make dist, but don't do anything special about them.
+EXTRA_DIST += shell_unittest.py shell_process_tests.sh
+
+endif
+
+
+CLEANDIRS = __pycache__
+
+clean-local:
+	rm -rf $(CLEANDIRS)

+ 183 - 0
src/bin/shell/tests/shell_process_tests.sh.in

@@ -0,0 +1,183 @@
+# Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Path to the temporary configuration file.
+CFG_FILE=@abs_top_builddir@/src/bin/shell/tests/test_config.json
+# Path to the Control Agent log file.
+LOG_FILE=@abs_top_builddir@/src/bin/shell/tests/test.log
+# Expected version
+EXPECTED_VERSION="@PACKAGE_VERSION@"
+
+# Control Agent configuration to be stored in the configuration file.
+# todo: use actual configuration once we support it.
+CONFIG="{
+    \"Control-agent\":
+    {
+        \"http-host\": \"127.0.0.1\",
+        \"http-port\": 8081
+    },
+    \"Logging\":
+    {
+        \"loggers\": [
+        {
+            \"name\": \"kea-ctrl-agent\",
+            \"output_options\": [
+                {
+                    \"output\": \"$LOG_FILE\"
+                }
+            ],
+            \"severity\": \"DEBUG\"
+        }
+        ]
+    }
+}"
+
+# In these tests we need to use two binaries: Control Agent and Kea shell.
+# Using bin and bin_path would be confusing, so we omit defining bin
+# and bin_path on purpose.
+ca_bin="kea-ctrl-agent"
+ca_bin_path=@abs_top_builddir@/src/bin/agent
+
+shell_bin="kea-shell"
+shell_bin_path=@abs_top_builddir@/src/bin/shell
+
+tmpfile_path=@abs_top_builddir@/src/bin/shell/tests
+
+# Import common test library.
+. @abs_top_builddir@/src/lib/testutils/dhcp_test_lib.sh
+
+# This test verifies that Control Agent is shut down gracefully when it
+# receives a SIGINT or SIGTERM signal.
+shell_command_test() {
+    test_name=${1}  # Test name
+    cmd=${2}        # Command to be sent
+    exp_rsp=${3}    # Expected response
+    params=${4}     # Any extra parameters
+
+    # Setup phase: start CA.
+
+    # Log the start of the test and print test name.
+    test_start ${test_name}
+    
+    # Remove any dangling CA instances and remove log files.
+    cleanup
+
+    # Create new configuration file.
+    create_config "${CONFIG}"
+
+    # Instruct Control Agent to log to the specific file.
+    set_logger
+    # Start Control Agent.
+    start_kea ${ca_bin_path}/${ca_bin}
+    # Wait up to 20s for Control Agent to start.
+    wait_for_kea 20
+    if [ ${_WAIT_FOR_KEA} -eq 0 ]; then
+        printf "ERROR: timeout waiting for Control Agent to start.\n"
+        clean_exit 1
+    fi
+
+    # Check if it is still running. It could have terminated (e.g. as a result
+    # of configuration failure).
+    get_pid ${ca_bin}
+    if [ ${_GET_PIDS_NUM} -ne 1 ]; then
+        printf "ERROR: expected one Control Agent process to be started.\
+ Found %d processes started.\n" ${_GET_PIDS_NUM}
+        clean_exit 1
+    fi
+
+    # Check in the log file, how many times server has been configured.
+    # It should be just once on startup.
+    get_reconfigs
+    if [ ${_GET_RECONFIGS} -ne 1 ]; then
+        printf "ERROR: server been configured ${_GET_RECONFIGS} time(s),\
+ but exactly 1 was expected.\n"
+        clean_exit 1
+    else
+        printf "Server successfully configured.\n"
+    fi
+
+    # Main test phase: send command, check response.
+    tmp="echo \"${params}\" | ${shell_bin_path}/${shell_bin} --host \
+ 127.0.0.1 --port 8081 ${cmd} > ${tmpfile_path}/shell-stdout.txt"
+    echo "Executing kea-shell ($tmp)"
+    
+    echo "${params}" | ${shell_bin_path}/${shell_bin} --host 127.0.0.1 \
+ --port 8081 ${cmd} > ${tmpfile_path}/shell-stdout.txt
+
+    # Check the exit code
+    shell_exit_code=$?
+    if [ ${shell_exit_code} -ne 0 ]; then
+        echo "ERROR:" \
+	"kea-shell returned ${shell_exit_code} exit code,  expected 0."
+    else
+        echo "kea-shell returned ${shell_exit_code} exit code as expected."
+    fi
+
+    # Now check the response
+    rm -f ${tmpfile_path}/shell-expected.txt
+    echo ${exp_rsp} > ${tmpfile_path}/shell-expected.txt
+    diff ${tmpfile_path}/shell-stdout.txt ${tmpfile_path}/shell-expected.txt
+    diff_code=$?
+    if [ ${diff_code} -ne 0 ]; then
+        echo "ERROR:" \
+	"content returned is different than expected." \
+	"See ${tmpfile_path}/shell-*.txt"
+        echo "EXPECTED:"
+        cat ${tmpfile_path}/shell-expected.txt
+        echo "ACTUAL RESULT:"
+        cat ${tmpfile_path}/shell-stdout.txt
+        clean_exit 1
+    else
+        echo "Content returned by kea-shell meets expectation."
+        rm ${tmpfile_path}/shell-*.txt
+    fi
+    # Main test phase ends.
+
+    # Cleanup phase: shutdown CA
+    # Send SIGTERM signal to Control Agent
+    send_signal 15 ${ca_bin}
+
+    # Now wait for process to log that it is exiting.
+    wait_for_message 10 "DCTL_SHUTDOWN" 1
+    if [ ${_WAIT_FOR_MESSAGE} -eq 0 ]; then
+        printf "ERROR: Control Agent did not log shutdown.\n"
+        clean_exit 1
+    fi
+
+    # Make sure the server is down.
+    wait_for_server_down 5 ${ca_bin}
+    assert_eq 1 ${_WAIT_FOR_SERVER_DOWN} \
+        "Expected wait_for_server_down return %d, returned %d"
+
+    test_finish 0
+}
+
+# This test verifies that the binary is reporting its version properly.
+version_test() {
+    test_name=${1}  # Test name
+
+    # Log the start of the test and print test name.
+    test_start ${test_name}
+
+    # Remove dangling Kea instances and remove log files.
+    cleanup
+
+    REPORTED_VERSION="`${shell_bin_path}/${shell_bin} -v`"
+
+    if test "${REPORTED_VERSION}" == "${EXPECTED_VERSION}"; then
+        test_finish 0
+    else
+        echo "ERROR:" \
+	"Expected version ${EXPECTED_VERSION}, got ${REPORTED_VERSION}"
+        test_finish 1
+    fi
+}
+
+version_test "shell.version"
+shell_command_test "shell.list-commands" "list-commands" \
+    "[ { \"arguments\": [ \"list-commands\" ], \"result\": 0 } ]" ""
+shell_command_test "shell.bogus" "give-me-a-beer" \
+    "[ { \"result\": 1, \"text\": \"'give-me-a-beer' command not supported.\" } ]" ""

+ 115 - 0
src/bin/shell/tests/shell_unittest.py.in

@@ -0,0 +1,115 @@
+#!@PYTHON@
+
+# Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Kea shell unittest (python part)
+"""
+
+import unittest
+
+from kea_conn import CARequest
+
+class CARequestUnitTest(unittest.TestCase):
+    """
+    This class is dedicated to testing CARequest class. That class
+    is responsible for generation of the body and headers.
+    """
+
+    def setUp(self):
+        """
+        This method is called before each test. Currently it does nothing.
+        """
+        pass
+
+    def test_body_without_params(self):
+        """
+        This test verifies if the CARequest object generates the request
+        content properly when there are no parameters.
+        """
+        request = CARequest()
+        request.command = "foo"
+        request.generate_body()
+        self.assertEqual(request.content, '{ "command": "foo" }')
+
+    def test_body_with_params(self):
+        """
+        This test verifies if the CARequest object generates the request
+        content properly when there are parameters.
+        """
+        request = CARequest()
+        request.command = "foo"
+        request.params = '"bar": "baz"'
+        request.generate_body()
+        self.assertEqual(request.content,
+                         '{ "command": "foo", "parameters": { "bar": "baz" } }')
+
+    @staticmethod
+    def check_header(headers, header_name, value):
+        """
+        Checks if headers array contains an entry specified by
+        header_name and that its value matches specified value
+        """
+        if header_name in headers:
+            if headers[header_name] == value:
+                return True
+            else:
+                print("Expected value: " + value +
+                      " does not match actual value: " +
+                      headers[header_name])
+            return False
+        else:
+            print("Expected header: " + header_name + " missing")
+            return False
+
+    def test_headers(self):
+        """
+        This test checks if the headers are generated properly. Note that since
+        the content is not specified, it is 0. Therefore Content-Length is 0.
+        """
+        request = CARequest()
+        request.generate_headers()
+
+        self.assertTrue(self.check_header(request.headers,
+                                          'Content-Type', 'application/json'))
+        self.assertTrue(self.check_header(request.headers,
+                                          'Accept', '*/*'))
+        self.assertTrue(self.check_header(request.headers,
+                                          'Content-Length', '0'))
+
+    def test_header_length(self):
+        """
+        This test checks if the headers are generated properly. In
+        this test there is specific content of non-zero length, and
+        its size should be reflected in the header.
+        """
+        request = CARequest()
+        request.content = '{ "command": "foo" }'
+        request.generate_headers()
+
+        self.assertTrue(self.check_header(request.headers, 'Content-Length',
+                                          str(len(request.content))))
+
+    def test_header_version(self):
+        """
+        This test checks if the version reported in HTTP headers is
+        generated properly.
+        """
+        request = CARequest()
+        request.version = "1.2.3"
+        request.generate_headers()
+        self.assertTrue(self.check_header(request.headers, 'User-Agent',
+                                          'Kea-shell/1.2.3'))
+
+    def tearDown(self):
+        """
+        This method is called after each test. Currently it does nothing.
+        """
+        pass
+
+if __name__ == '__main__':
+    unittest.main()