# Process this file with autoconf to produce a configure script.
AC_PREREQ([2.59])
-AC_INIT(bind10, 20140313, kea-dev@isc.org)
+AC_INIT(kea, 20140602-git, kea-dev@isc.org)
AC_CONFIG_SRCDIR(README)
# serial-tests is not available in automake version before 1.13, so
ext/Makefile
m4macros/Makefile
Makefile
- src/bin/bind10/bind10
- src/bin/bind10/init.py
- src/bin/bind10/Makefile
- src/bin/bind10/run_bind10.sh
- src/bin/bind10/tests/init_test.py
- src/bin/bind10/tests/Makefile
- src/bin/bindctl/bindctl_main.py
- src/bin/bindctl/Makefile
- src/bin/bindctl/run_bindctl.sh
- src/bin/bindctl/tests/bindctl_test
- src/bin/bindctl/tests/Makefile
- src/bin/cfgmgr/b10-cfgmgr.py
- src/bin/cfgmgr/local_plugins/Makefile
- src/bin/cfgmgr/Makefile
- src/bin/cfgmgr/plugins/datasrc.spec.pre
- src/bin/cfgmgr/plugins/Makefile
- src/bin/cfgmgr/plugins/tests/Makefile
- src/bin/cfgmgr/tests/b10-cfgmgr_test.py
- src/bin/cfgmgr/tests/Makefile
- src/bin/cmdctl/cmdctl.py
- src/bin/cmdctl/cmdctl.spec.pre
- src/bin/cmdctl/Makefile
- src/bin/cmdctl/run_b10-cmdctl.sh
- src/bin/cmdctl/tests/cmdctl_test
- src/bin/cmdctl/tests/Makefile
src/bin/d2/Makefile
src/bin/d2/spec_config.h.pre
src/bin/d2/tests/Makefile
src/bin/dhcp6/tests/test_data_files_config.h
src/bin/dhcp6/tests/test_libraries.h
src/bin/Makefile
- src/bin/msgq/Makefile
- src/bin/msgq/msgq.py
- src/bin/msgq/run_msgq.sh
- src/bin/msgq/tests/Makefile
src/bin/sockcreator/Makefile
src/bin/sockcreator/tests/Makefile
- src/bin/stats/Makefile
- src/bin/stats/stats_httpd.py
- src/bin/stats/stats.py
- src/bin/stats/tests/Makefile
- src/bin/stats/tests/testdata/Makefile
- src/bin/sysinfo/Makefile
- src/bin/sysinfo/run_sysinfo.sh
- src/bin/sysinfo/sysinfo.py
- src/bin/tests/Makefile
- src/bin/tests/process_rename_test.py
- src/bin/usermgr/b10-cmdctl-usermgr.py
- src/bin/usermgr/Makefile
- src/bin/usermgr/run_b10-cmdctl-usermgr.sh
- src/bin/usermgr/tests/Makefile
src/hooks/dhcp/Makefile
src/hooks/dhcp/user_chk/Makefile
src/hooks/dhcp/user_chk/tests/Makefile
])
AC_CONFIG_COMMANDS([permissions], [
- chmod +x src/bin/bind10/bind10
- chmod +x src/bin/bind10/run_bind10.sh
- chmod +x src/bin/bindctl/run_bindctl.sh
- chmod +x src/bin/bindctl/tests/bindctl_test
- chmod +x src/bin/cmdctl/run_b10-cmdctl.sh
- chmod +x src/bin/cmdctl/tests/cmdctl_test
- chmod +x src/bin/msgq/run_msgq.sh
- chmod +x src/bin/sysinfo/run_sysinfo.sh
- chmod +x src/bin/usermgr/run_b10-cmdctl-usermgr.sh
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
# The following build order must be maintained.
-SUBDIRS = bind10 bindctl cfgmgr msgq cmdctl \
- usermgr stats tests sockcreator dhcp4 dhcp6 \
- d2 sysinfo
+SUBDIRS = sockcreator dhcp4 dhcp6 d2
check-recursive: all-recursive
+++ /dev/null
-/run_bind10.sh
-/bind10
-/bind10.8
-/b10-init
-/b10-init.8
-/init.py
+++ /dev/null
-SUBDIRS = . tests
-
-sbin_SCRIPTS = bind10
-pkglibexec_SCRIPTS = b10-init
-CLEANFILES = b10-init b10-init.pyc init.pyc
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/init_messages.py
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/init_messages.pyc
-
-pkglibexecdir = $(libexecdir)/@PACKAGE@
-
-nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/init_messages.py
-pylogmessagedir = $(pyexecdir)/isc/log_messages/
-
-noinst_SCRIPTS = run_bind10.sh
-
-bind10dir = $(pkgdatadir)
-bind10_DATA = init.spec
-EXTRA_DIST = init.spec bind10.in
-
-man_MANS = b10-init.8 bind10.8
-DISTCLEANFILES = $(man_MANS) bind10
-EXTRA_DIST += $(man_MANS) b10-init.xml bind10.xml init_messages.mes
-
-if GENERATE_DOCS
-
-bind10.8: bind10.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/bind10.xml
-
-b10-init.8: b10-init.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-init.xml
-
-#dist-local-check-mans-enabled:
-# @if grep "Man generation disabled" $(man_MANS) >/dev/null; then $(RM) $(man_MANS); fi
-
-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 BIND 10 > $@
-
-#dist-local-check-mans-enabled:
-# @echo "*** --enable-generate-docs must be used in order to make dist"
-# @false
-
-endif
-
-$(PYTHON_LOGMSGPKG_DIR)/work/init_messages.py : init_messages.mes
- $(top_builddir)/src/lib/log/compiler/message \
- -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/init_messages.mes
-
-# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
-b10-init: init.py $(PYTHON_LOGMSGPKG_DIR)/work/init_messages.py
- $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
- -e "s|@@LIBDIR@@|$(libdir)|" \
- -e "s|@@LIBEXECDIR@@|$(pkglibexecdir)|" init.py >$@
- chmod a+x $@
-
-pytest:
- $(SHELL) tests/bind10_test
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-This directory contains the source for the "b10-init" program, as well as
-the "bind10" script that runs it.
-
-Files:
- Makefile.am - build information
- README - this file
- TODO - remaining development tasks for this program
- bind10.py.in - used to make bind10.py with proper Python paths
- init.spec - defines the options and commands
- run_bind10.sh.in - use to make run_bind10.sh with proper Python paths
-
-The "tests" directory contains unit tests for the application.
-
-If you want to run the bind10 program from the source directory, then
-use the "run_bind10.sh" script. To run it in the installed location,
-use the "bind10" program.
+++ /dev/null
-- Read msgq configuration from configuration manager (Trac #213)
- https://bind10.isc.org/ticket/213
-- Provide more administrator options:
- - Get information on a process (returns list of times started & stopped,
- plus current information such as PID)
- - Stop a component
- - Force-stop a component
-- Mechanism to wait for child to start before continuing
-- Use .spec file to define commands
-- Rename "c-channel" stuff to msgq for clarity
-- Reply to shutdown message?
-- Some sort of group creation so termination signals can be sent to
- children of children processes (if any)
-
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2010-2012 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>February 5, 2013</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-init</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-init</refname>
- <refpurpose>BIND 10 Init process</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2010-2013</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>b10-init</command>
- <arg><option>-c <replaceable>config-filename</replaceable></option></arg>
- <arg><option>-i</option></arg>
- <arg><option>-m <replaceable>file</replaceable></option></arg>
- <arg><option>-p <replaceable>data_path</replaceable></option></arg>
- <arg><option>-u <replaceable>user</replaceable></option></arg>
- <arg><option>-v</option></arg>
- <arg><option>-w <replaceable>wait_time</replaceable></option></arg>
- <arg><option>--clear-config</option></arg>
- <arg><option>--cmdctl-port</option> <replaceable>port</replaceable></arg>
- <arg><option>--config-file</option> <replaceable>config-filename</replaceable></arg>
- <arg><option>--data-path</option> <replaceable>directory</replaceable></arg>
- <arg><option>--msgq-socket-file <replaceable>file</replaceable></option></arg>
- <arg><option>--no-kill</option></arg>
- <arg><option>--pid-file</option> <replaceable>filename</replaceable></arg>
- <arg><option>--pretty-name <replaceable>name</replaceable></option></arg>
- <arg><option>--user <replaceable>user</replaceable></option></arg>
- <arg><option>--verbose</option></arg>
- <arg><option>--wait <replaceable>wait_time</replaceable></option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
-
- <para>The <command>b10-init</command> daemon starts up other
- BIND 10 required daemons. It handles restarting of exiting
- programs and also the shutdown of all managed daemons.</para>
-
-<!-- TODO: list what it starts here? -->
-
-<!-- TODO
- <para>The configuration of the <command>b10-init</command> daemon
- is defined in the TODO configuration file, as described in the
- <citerefentry><refentrytitle>TODO</refentrytitle><manvolnum>5</manvolnum></citerefentry>
- manual page.</para>
--->
-
- </refsect1>
-
- <refsect1>
- <title>ARGUMENTS</title>
-
- <para>The arguments are as follows:</para>
-
- <variablelist>
-
- <varlistentry>
- <term>
- <option>-c</option> <replaceable>config-filename</replaceable>,
- <option>--config-file</option> <replaceable>config-filename</replaceable>
- </term>
- <listitem>
- <para>The configuration filename to use. Can be either absolute or
- relative to data path. In case it is absolute, value of data path is
- not considered.
- Defaults to <filename>b10-config.db</filename>.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>--clear-config</option>
- </term>
- <listitem>
- <para>
- This will create a backup of the existing configuration
- file, remove it and start
- <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
- with the default configuration.
- The name of the backup file can be found in the logs
- (<varname>CFGMGR_BACKED_UP_CONFIG_FILE</varname>).
- (It will append a number to the backup filename if a
- previous backup file exists.)
-
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>--cmdctl-port</option> <replaceable>port</replaceable>
- </term>
- <listitem>
- <para>The <command>b10-cmdctl</command> daemon will listen
- on this port.
- (See
- <refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum>
- for the default.)
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>-p</option> <replaceable>directory</replaceable>,
- <option>--data-path</option> <replaceable>directory</replaceable>
- </term>
- <listitem>
- <para>The path where BIND 10 programs look for various data files.
- Currently only
- <citerefentry><refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum></citerefentry>
- uses it to locate the configuration file, but the usage
- might be extended for other programs and other types of
- files.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-m</option> <replaceable>file</replaceable>,
- <option>--msgq-socket-file</option> <replaceable>file</replaceable></term>
-
- <listitem>
- <para>The UNIX domain socket file for the
- <citerefentry><refentrytitle>b10-msgq</refentrytitle><manvolnum>8</manvolnum></citerefentry>
- daemon to use.
- The default is
- <filename>/usr/local/var/bind10/msg_socket</filename>.
-<!-- @localstatedir@/@PACKAGE_NAME@/msg_socket -->
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-i</option>, <option>--no-kill</option></term>
- <listitem>
- <para>When this option is passed, <command>b10-init</command>
- does not send SIGTERM and SIGKILL signals to modules during
- shutdown. (This option was introduced for use during
- testing.)</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-u</option> <replaceable>user</replaceable>, <option>--user</option> <replaceable>name</replaceable></term>
-<!-- TODO: example more detail. -->
- <listitem>
- <para>The username for <command>b10-init</command> to run as.
- <command>b10-init</command> must be initially ran as the
- root user to use this option.
- The default is to run as the current user.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>--pid-file</option> <replaceable>filename</replaceable></term>
- <listitem>
- <para>If defined, the PID of the <command>b10-init</command> is stored
- in this file.
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>--pretty-name <replaceable>name</replaceable></option></term>
-
- <listitem>
- <para>The name this process should have in tools like
- <command>ps</command> or <command>top</command>. This
- is handy if you have multiple versions/installations
- of <command>b10-init</command>.
-<!-- TODO: only supported with setproctitle feature
-The default is the basename of ARG 0.
--->
-</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-v</option>, <option>--verbose</option></term>
- <listitem>
- <para>Display more about what is going on for
- <command>b10-init</command> and its child processes.</para>
-<!-- TODO: not true about all children yet -->
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-w</option> <replaceable>wait_time</replaceable>, <option>--wait</option> <replaceable>wait_time</replaceable></term>
- <listitem>
- <para>Sets the amount of time that BIND 10 will wait for
- the configuration manager (a key component of BIND 10)
- to initialize itself before abandoning the start up and
- terminating with an error. The
- <replaceable>wait_time</replaceable> is specified in
- seconds and has a default value of 10.
- </para>
- </listitem>
- </varlistentry>
-
- </variablelist>
- </refsect1>
-
-<!--
-TODO: configuration section
--->
-
- <refsect1>
- <title>CONFIGURATION AND COMMANDS</title>
-
- <para>
- The configuration provides settings for components for
- <command>b10-init</command> to manage under
- <varname>/Init/components/</varname>.
- The default elements are:
- </para>
-
- <itemizedlist>
-
- <listitem>
- <para> <varname>/Init/components/b10-cmdctl</varname> </para>
- </listitem>
-
- <listitem>
- <para> <varname>/Init/components/b10-stats</varname> </para>
- </listitem>
-
- </itemizedlist>
-
- <para>
- (Note that the startup of <command>b10-sockcreator</command>,
- <command>b10-cfgmgr</command>, and <command>b10-msgq</command>
- is not configurable. They are hardcoded and <command>b10-init</command>
- will not run without them.)
- </para>
-
- <para>
- The named sets for components contain the following settings:
- </para>
-
- <variablelist>
-
- <varlistentry>
- <term><varname>address</varname></term>
- <listitem>
- <para>The name used for communicating to it on the message
- bus.</para>
-<!-- NOTE: vorner said:
-These can be null, because the components are special ones, and
-the special class there already knows the address. It is (I hope)
-explained in the guide. I'd like to get rid of the special components
-sometime and I'd like it to teach to guess the address.
--->
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><varname>kind</varname></term>
- <listitem>
- <para>
- This defines how required a component is.
- The possible settings for <varname>kind</varname> are:
- <varname>core</varname> (system won't start if it won't
- start and <command>b10-init</command> will shutdown if
- a <quote>core</quote> component crashes),
- <varname>dispensable</varname> (<command>b10-init</command>
- will restart failing component),
- and
- <varname>needed</varname> (<command>b10-init</command>
- will shutdown if component won't initially start, but
- if crashes later, it will attempt to restart).
- This setting is required.
-<!-- TODO: formatting -->
- </para>
- </listitem>
- </varlistentry>
-
-<!--
-TODO: currently not used
- <varlistentry>
- <term> <varname>params</varname> </term>
- <listitem>
- <para>
-list
-</para>
- </listitem>
- </varlistentry>
--->
-
- <varlistentry>
- <term> <varname>priority</varname> </term>
- <listitem>
- <para>This is an integer. <command>b10-init</command>
- will start the components with largest priority numbers first.
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term> <varname>process</varname> </term>
- <listitem>
- <para>This is the filename of the executable to be started.
- If not defined, then <command>b10-init</command> will
- use the component name instead.
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term> <varname>special</varname> </term>
- <listitem>
- <para>
- This defines if the component is started a special, hardcoded
- way.
-<!--
-TODO: document this ... but maybe some of these will be removed
-once we get rid of some using switches for components?
-
-auth
-cfgmgr
-cmdctl
-msgq
-resolver
-sockcreator
-xfrin
--->
-
-</para>
- </listitem>
- </varlistentry>
-
- </variablelist>
-
-<!-- TODO: formating -->
- <para>
- The <varname>Init</varname> configuration commands are:
- </para>
-
-<!-- TODO -->
-<!--
- <para>
- <command>drop_socket</command>
- This is an internal command and not exposed to the administrator.
- </para>
--->
-
-<!-- TODO -->
-<!--
- <para>
- <command>get_socket</command>
- This is an internal command and not exposed to the administrator.
- </para>
--->
-
- <para>
- <command>getstats</command> tells <command>b10-init</command>
- to send its statistics data to the <command>b10-stats</command>
- daemon.
- This is an internal command and not exposed to the administrator.
-<!-- not defined in spec -->
- </para>
-
- <para>
- <command>ping</command> is used to check the connection with the
- <command>b10-init</command> daemon.
- It returns the text <quote>pong</quote>.
- </para>
-
- <para>
- <command>show_processes</command> lists the current processes
- managed by <command>b10-init</command>.
- The output is an array in JSON format containing the process
- ID, the name for each and the address name used on each message bus.
-<!-- TODO: what is name? -->
-<!-- TODO: change to JSON object format? -->
-<!-- TODO: ticket #1406 -->
- </para>
-
- <para>
- <command>shutdown</command> tells <command>b10-init</command>
- to shutdown the BIND 10 servers.
- It will tell each process it manages to shutdown and, when
- complete, <command>b10-init</command> will exit.
- </para>
-
- </refsect1>
-
- <refsect1>
- <title>STATISTICS DATA</title>
-
- <para>
- The statistics data collected by the <command>b10-stats</command>
- daemon for <quote>Init</quote> include:
- </para>
-
- <variablelist>
-
- <varlistentry>
- <term>boot_time</term>
- <listitem><para>
- The date and time that the <command>b10-init</command>
- process started.
- This is represented in ISO 8601 format.
- </para></listitem>
- </varlistentry>
-
- </variablelist>
-
- </refsect1>
-
- <refsect1>
- <title>FILES</title>
- <para><filename>sockcreator-XXXXXX/sockcreator</filename>
- —
- the Unix Domain socket located in a temporary file directory for
- <command>b10-sockcreator</command>
-<!-- <citerefentry><refentrytitle>b10-sockcreator</refentrytitle><manvolnum>8</manvolnum></citerefentry> -->
- communication.
- </para>
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-auth</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-msgq</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-xfrin</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-xfrout</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-zonemgr</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-stats</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-stats-httpd</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
- </refsect1>
-<!-- <citerefentry>
- <refentrytitle>b10-sockcreator</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>, -->
-
- <refsect1 id='history'><title>HISTORY</title>
- <para>The development of <command>b10-init</command>
- was started in October 2009.
- It was renamed and its configuration identifier changed
- in February 2013.
- </para>
- </refsect1>
-
- <refsect1>
- <title>AUTHORS</title>
- <para>
- The <command>b10-init</command>
- daemon was initially designed by Shane Kerr of ISC.
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-#!/bin/sh
-
-# We use this wrapper script both for production and in-source tests; in
-# the latter case B10_FROM_BUILD environment is expected to be defined.
-if test -n "${B10_FROM_BUILD}"; then
- exec ${B10_FROM_BUILD}/src/bin/bind10/b10-init $*
-else
- prefix=@prefix@
- exec_prefix=@exec_prefix@
- exec @libexecdir@/@PACKAGE@/b10-init $*
-fi
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2013 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>February 5, 2013</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>bind10</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>bind10</refname>
- <refpurpose>BIND 10 start script</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2013</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>bind10</command>
- <arg><option>options</option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
-
- <para>The <command>bind10</command> script is a simple wrapper that
- starts BIND 10 by running the <command>b10-init</command> daemon. All
- options passed to <command>bind10</command> are directly passed on to
- <command>b10-init</command>.</para>
-
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-init</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
- </refsect1>
-
- <refsect1 id='history'><title>HISTORY</title>
- <para>The <command>bind10</command> script was added in February 2013.
- </para>
- </refsect1>
-
-</refentry>
+++ /dev/null
-Socket creator API
-==================
-
-This API is between Init and other modules to allow them requesting of sockets.
-For simplicity, we will use the socket creator for all (even non-privileged)
-ports for now, but we should have some function where we can abstract it later.
-
-Goals
------
-* Be able to request a socket of any combination IPv4/IPv6 UDP/TCP bound to given
- port and address (sockets that are not bound to anything can be created
- without privileges, therefore are not requested from the socket creator).
-* Allow to provide the same socket to multiple modules (eg. multiple running
- auth servers).
-* Allow releasing the sockets (in case all modules using it give it up,
- terminate or crash).
-* Allow restricting of the sharing (don't allow shared socket between auth
- and recursive, as the packets would often get to the wrong application,
- show error instead).
-* Get the socket to the application.
-
-Transport of sockets
---------------------
-It seems we are stuck with current msgq for a while and there's a chance the
-new replacement will not be able to send sockets inbound. So, we need another
-channel.
-
-b10-init will create a unix-domain socket and listen on it. When something
-requests a socket over the command channel and the socket is created, some kind
-of token is returned to the application (which will represent the future
-socket). The application then connects to the unix-domain socket, sends the
-token over the connection (so Init will know which socket to send there, in case
-multiple applications ask for sockets simultaneously) and Init sends the socket
-in return.
-
-In theory, we could send the requests directly over the unix-domain
-socket, but it has two disadvantages:
-* The msgq handles serializing/deserializing of structured
- information (like the parameters to be used), we would have to do it
- manually on the socket.
-* We could place some kind of security in front of msgq (in case file
- permissions are not enough, for example if they are not honored on
- socket files, as indicated in the first paragraph of:
- http://lkml.indiana.edu/hypermail/linux/kernel/0505.2/0008.html).
- The socket would have to be secured separately. With the tokens,
- there's some level of security already - someone not having the
- token can't request a priviledged socket.
-
-Caching of sockets
-------------------
-To allow sending the same socket to multiple application, the Init process will
-hold a cache. Each socket that is created and sent is kept open in Init and
-preserved there as well. A reference count is kept with each of them.
-
-When another application asks for the same socket, it is simply sent from the
-cache instead of creating it again by the creator.
-
-When application gives the socket willingly (by sending a message over the
-command channel), the reference count can be decreased without problems. But
-when the application terminates or crashes, we need to decrease it as well.
-There's a problem, since we don't know which command channel connection (eg.
-lname) belongs to which PID. Furthermore, the applications don't need to be
-started by b10-init.
-
-There are two possibilities:
-* Let the msgq send messages about disconnected clients (eg. group message to
- some name). This one is better if we want to migrate to dbus, since dbus
- already has this capability as well as sending the sockets inbound (at least it
- seems so on unix) and we could get rid of the unix-domain socket completely.
-* Keep the unix-domain connections open forever. Init can remember which socket
- was sent to which connection and when the connection closes (because the
- application crashed), it can drop all the references on the sockets. This
- seems easier to implement.
-
-The commands
-------------
-* Command to release a socket. This one would have single parameter, the token
- used to get the socket. After this, b10-init would decrease its reference
- count and if it drops to zero, close its own copy of the socket. This
- should be used when the module stops using the socket (and after closes
- it). The library could remember the file-descriptor to token mapping (for
- common applications that don't request the same socket multiple times in
- parallel).
-* Command to request a socket. It would have parameters to specify which socket
- (IP address, address family, port) and how to allow sharing. Sharing would be
- one of:
- - None
- - Same kind of application (however, it is not entirely clear what
- this means, in case it won't work out intuitively, we'll need to
- define it somehow)
- - Any kind of application
- And a kind of application would be provided, to decide if the sharing is
- possible (eg. if auth allows sharing with the same kind and something else
- allows sharing with anything, the sharing is not possible, two auths can).
-
- It would return either error (the socket can't be created or sharing is not
- possible) or the token. Then there would be some time for the application to
- pick up the requested socket.
-
-Examples
---------
-We probably would have a library with blocking calls to request the
-sockets, so a code could look like:
-
-(socket_fd, token) = request_socket(address, port, 'UDP', SHARE_SAMENAME, 'test-application')
-sock = socket.fromfd(socket_fd)
-
-# Some sock.send and sock.recv stuff here
-
-sock.close()
-release_socket(socket_fd) # or release_socket(token)
-
-Known limitations
------------------
-Currently the socket creator doesn't support specifying any socket
-options. If it turns out there are any options that need to be set
-before bind(), we'll need to extend it (and extend the protocol as
-well). If we want to support them, we'll have to solve a possible
-conflict (what to do when two applications request the same socket and
-want to share it, but want different options).
-
-The current socket creator doesn't know raw sockets, but if they are
-needed, it should be easy to add.
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2010,2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-This file implements the b10-init program.
-
-Its purpose is to start up the BIND 10 system, and then manage the
-processes, by starting and stopping processes, plus restarting
-processes that exit.
-
-To start the system, it first runs the c-channel program (msgq), then
-connects to that. It then runs the configuration manager, and reads
-its own configuration. Then it proceeds to starting other modules.
-
-The Python subprocess module is used for starting processes, but
-because this is not efficient for managing groups of processes,
-SIGCHLD signals are caught and processed using the signal module.
-
-Most of the logic is contained in the Init class. However, since Python
-requires that signal processing happen in the main thread, we do
-signal handling outside of that class, in the code running for
-__main__.
-"""
-
-import sys; sys.path.append ('@@PYTHONPATH@@')
-import os
-from isc.util.address_formatter import AddressFormatter
-
-# If B10_FROM_SOURCE is set in the environment, we use data files
-# from a directory relative to that, otherwise we use the ones
-# installed on the system
-if "B10_FROM_SOURCE" in os.environ:
- SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] +\
- "/src/bin/bind10/init.spec"
-else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- SPECFILE_LOCATION = "@datadir@/@PACKAGE@/init.spec"\
- .replace("${datarootdir}", DATAROOTDIR)\
- .replace("${prefix}", PREFIX)
-
-import subprocess
-import signal
-import re
-import errno
-import time
-import select
-import random
-import socket
-from optparse import OptionParser, OptionValueError
-import io
-import pwd
-import posix
-import copy
-
-from bind10_config import LIBEXECPATH
-import bind10_config
-import isc.cc
-import isc.util.process
-import isc.net.parse
-import isc.log
-import isc.config
-from isc.log_messages.init_messages import *
-import isc.bind10.component
-import isc.bind10.special_component
-import isc.bind10.socket_cache
-import isc.util.traceback_handler
-import libutil_io_python
-import tempfile
-
-isc.log.init("b10-init", buffer=True)
-logger = isc.log.Logger("init")
-
-# Pending system-wide debug level definitions, the ones we
-# use here are hardcoded for now
-DBG_PROCESS = logger.DBGLVL_TRACE_BASIC
-DBG_COMMANDS = logger.DBGLVL_TRACE_DETAIL
-
-# Messages sent over the unix domain socket to indicate if it is followed by a
-# real socket
-CREATOR_SOCKET_OK = b"1\n"
-CREATOR_SOCKET_UNAVAILABLE = b"0\n"
-
-# RCodes of known exceptions for the get_token command
-CREATOR_SOCKET_ERROR = 2
-CREATOR_SHARE_ERROR = 3
-
-# Assign this process some longer name
-isc.util.process.rename()
-
-# This is the version that gets displayed to the user.
-# The VERSION string consists of the module name, the module version
-# number, and the overall BIND 10 version number (set in configure.ac).
-VERSION = "bind10 20110223 (BIND 10 @PACKAGE_VERSION@)"
-
-# This is for boot_time of Init
-_BASETIME = time.gmtime()
-
-# Detailed error message commonly used on startup failure, possibly due to
-# permission issue regarding log lock file. We dump verbose message because
-# it may not be clear exactly what to do if it simply says
-# "failed to open <filename>: permission denied"
-NOTE_ON_LOCK_FILE = """\
-TIP: if this is about permission error for a lock file, check if the directory
-of the file is writable for the user of the bind10 process; often you need
-to start bind10 as a super user. Also, if you specify the -u option to
-change the user and group, the directory must be writable for the group,
-and the created lock file must be writable for that user. Finally, make sure
-the lock file is not left in the directly before restarting.
-"""
-
-class ProcessInfoError(Exception): pass
-
-class ChangeUserError(Exception):
- '''Exception raised when setuid/setgid fails.
-
- When raised, it's expected to be propagated via underlying component
- management modules to the top level so that it will help provide useful
- fatal error message.
-
- '''
- pass
-
-class ProcessInfo:
- """Information about a process"""
-
- dev_null = open(os.devnull, "w")
-
- def __init__(self, name, args, env={}, dev_null_stdout=False,
- dev_null_stderr=False):
- self.name = name
- self.args = args
- self.env = env
- self.dev_null_stdout = dev_null_stdout
- self.dev_null_stderr = dev_null_stderr
- self.process = None
- self.pid = None
-
- def _preexec_work(self):
- """Function used before running a program that needs to run as a
- different user."""
- # First, put us into a separate process group so we don't get
- # SIGINT signals on Ctrl-C (b10-init will shut everything down by
- # other means).
- os.setpgrp()
-
- def _spawn(self):
- if self.dev_null_stdout:
- spawn_stdout = self.dev_null
- else:
- spawn_stdout = None
- if self.dev_null_stderr:
- spawn_stderr = self.dev_null
- else:
- spawn_stderr = None
- # Environment variables for the child process will be a copy of those
- # of the b10-init process with any additional specific variables given
- # on construction (self.env).
- spawn_env = copy.deepcopy(os.environ)
- spawn_env.update(self.env)
- spawn_env['PATH'] = LIBEXECPATH + ':' + spawn_env['PATH']
- self.process = subprocess.Popen(self.args,
- stdin=subprocess.PIPE,
- stdout=spawn_stdout,
- stderr=spawn_stderr,
- close_fds=True,
- env=spawn_env,
- preexec_fn=self._preexec_work)
- self.pid = self.process.pid
-
- # spawn() and respawn() are the same for now, but in the future they
- # may have different functionality
- def spawn(self):
- self._spawn()
-
- def respawn(self):
- self._spawn()
-
-class CChannelConnectError(Exception): pass
-
-class ProcessStartError(Exception): pass
-
-class Init:
- """Init of BIND class."""
-
- def __init__(self, msgq_socket_file=None, data_path=None,
- config_filename=None, clear_config=False,
- verbose=False, nokill=False, setuid=None, setgid=None,
- username=None, cmdctl_port=None, wait_time=10):
- """
- Initialize the Init of BIND. This is a singleton (only one can
- run).
-
- The msgq_socket_file specifies the UNIX domain socket file that the
- msgq process listens on. If verbose is True, then b10-init reports
- what it is doing.
-
- Data path and config filename are passed through to config manager
- (if provided) and specify the config file to be used.
-
- The cmdctl_port is passed to cmdctl and specify on which port it
- should listen.
-
- wait_time controls the amount of time (in seconds) that Init waits
- for selected processes to initialize before continuing with the
- initialization. Currently this is only the configuration manager.
- """
- self.cc_session = None
- self.ccs = None
- self.curproc = None
- self.msgq_socket_file = msgq_socket_file
- self.component_config = {}
- # Some time in future, it may happen that a single component has
- # multple processes (like a pipeline-like component). If so happens,
- # name "components" may be inappropriate. But as the code isn't
- # probably completely ready for it, we leave it at components for
- # now. We also want to support multiple instances of a single
- # component. If it turns out that we'll have a single component with
- # multiple same processes or if we start multiple components with the
- # same configuration (we do this now, but it might change) is an open
- # question.
- self.components = {}
- # Simply list of components that died and need to wait for a
- # restart. Components manage their own restart schedule now
- self.components_to_restart = []
- self.runnable = False
- self.__uid = setuid
- self.__gid = setgid
- self.username = username
- self.verbose = verbose
- self.nokill = nokill
- self.data_path = data_path
- self.config_filename = config_filename
- self.clear_config = clear_config
- self.cmdctl_port = cmdctl_port
- self.wait_time = wait_time
- self.msgq_timeout = 5
-
- # _run_under_unittests is only meant to be used when testing. It
- # bypasses execution of some code to help with testing.
- self._run_under_unittests = False
-
- self._component_configurator = isc.bind10.component.Configurator(self,
- isc.bind10.special_component.get_specials())
- # The priorities here make them start in the correct order. First
- # the socket creator (which would drop root privileges by then),
- # then message queue and after that the config manager (which uses
- # the config manager)
- self.__core_components = {
- 'sockcreator': {
- 'kind': 'core',
- 'special': 'sockcreator',
- 'priority': 200
- },
- 'msgq': {
- 'kind': 'core',
- 'special': 'msgq',
- 'priority': 199
- },
- 'cfgmgr': {
- 'kind': 'core',
- 'special': 'cfgmgr',
- 'priority': 198
- }
- }
- self.__started = False
- self.exitcode = 0
-
- # If -v was set, enable full debug logging.
- if self.verbose:
- logger.set_severity("DEBUG", 99)
- # This is set in init_socket_srv
- self._socket_path = None
- self._socket_cache = None
- self._tmpdir = None
- self._srv_socket = None
- self._unix_sockets = {}
-
- def __propagate_component_config(self, config):
- comps = dict(config)
- # Fill in the core components, so they stay alive
- for comp in self.__core_components:
- if comp in comps:
- raise Exception(comp + " is core component managed by " +
- "b10-init, do not set it")
- comps[comp] = self.__core_components[comp]
- # Update the configuration
- self._component_configurator.reconfigure(comps)
-
- def change_user(self):
- '''Change the user and group to those specified on construction.
-
- This method is expected to be called by a component on initial
- startup when the system is ready to switch the user and group
- (i.e., once all components that need the privilege of the original
- user have started).
- '''
- try:
- if self.__gid is not None:
- logger.info(BIND10_SETGID, self.__gid)
- posix.setgid(self.__gid)
- except Exception as ex:
- raise ChangeUserError('failed to change group: ' + str(ex))
-
- try:
- if self.__uid is not None:
- posix.setuid(self.__uid)
- # We use one-shot logger after setuid here. This will
- # detect any permission issue regarding logging due to the
- # result of setuid at the earliest opportunity.
- isc.log.Logger("b10-init").info(BIND10_SETUID, self.__uid)
- except Exception as ex:
- raise ChangeUserError('failed to change user: ' + str(ex))
-
- def config_handler(self, new_config):
- # If this is initial update, don't do anything now, leave it to startup
- if not self.runnable:
- return
- logger.debug(DBG_COMMANDS, BIND10_RECEIVED_NEW_CONFIGURATION,
- new_config)
- try:
- if 'components' in new_config:
- self.__propagate_component_config(new_config['components'])
- return isc.config.ccsession.create_answer(0)
- except Exception as e:
- logger.error(BIND10_RECONFIGURE_ERROR, e)
- return isc.config.ccsession.create_answer(1, str(e))
-
- def get_processes(self):
- pids = list(self.components.keys())
- pids.sort()
- process_list = [ ]
- for pid in pids:
- process_list.append([pid, self.components[pid].name(),
- self.components[pid].address()])
- return process_list
-
- def _get_stats_data(self):
- return { 'boot_time':
- time.strftime('%Y-%m-%dT%H:%M:%SZ', _BASETIME)
- }
-
- def command_handler(self, command, args):
- logger.debug(DBG_COMMANDS, BIND10_RECEIVED_COMMAND, command)
- answer = isc.config.ccsession.create_answer(1,
- "command not implemented")
- if type(command) != str:
- answer = isc.config.ccsession.create_answer(1, "bad command")
- else:
- if command == "shutdown":
- self.runnable = False
- answer = isc.config.ccsession.create_answer(0)
- elif command == "getstats":
- answer = isc.config.ccsession.create_answer(
- 0, self._get_stats_data())
- elif command == "ping":
- answer = isc.config.ccsession.create_answer(0, "pong")
- elif command == "show_processes":
- answer = isc.config.ccsession. \
- create_answer(0, self.get_processes())
- elif command == "get_socket":
- answer = self._get_socket(args)
- elif command == "drop_socket":
- if "token" not in args:
- answer = isc.config.ccsession. \
- create_answer(1, "Missing token parameter")
- else:
- try:
- self._socket_cache.drop_socket(args["token"])
- answer = isc.config.ccsession.create_answer(0)
- except Exception as e:
- answer = isc.config.ccsession.create_answer(1, str(e))
- else:
- answer = isc.config.ccsession.create_answer(1,
- "Unknown command")
- return answer
-
- def kill_started_components(self):
- """
- Called as part of the exception handling when a process fails to
- start, this runs through the list of started processes, killing
- each one. It then clears that list.
- """
- logger.info(BIND10_KILLING_ALL_PROCESSES)
- self.__kill_children(True)
- self.components = {}
-
- def _read_bind10_config(self):
- """
- Reads the parameters associated with the Init module itself.
-
- This means the list of components we should start now.
-
- This could easily be combined into start_all_processes, but
- it stays because of historical reasons and because the tests
- replace the method sometimes.
- """
- logger.info(BIND10_READING_INIT_CONFIGURATION)
-
- config_data = self.ccs.get_full_config()
- self.__propagate_component_config(config_data['components'])
-
- def log_starting(self, process, port = None, address = None):
- """
- A convenience function to output a "Starting xxx" message if the
- logging is set to DEBUG with debuglevel DBG_PROCESS or higher.
- Putting this into a separate method ensures
- that the output form is consistent across all processes.
-
- The process name (passed as the first argument) is put into
- self.curproc, and is used to indicate which process failed to
- start if there is an error (and is used in the "Started" message
- on success). The optional port and address information are
- appended to the message (if present).
- """
- self.curproc = process
- if port is None and address is None:
- logger.info(BIND10_STARTING_PROCESS, self.curproc)
- elif address is None:
- logger.info(BIND10_STARTING_PROCESS_PORT, self.curproc,
- port)
- else:
- logger.info(BIND10_STARTING_PROCESS_PORT_ADDRESS,
- self.curproc, AddressFormatter((address, port)))
-
- def log_started(self, pid = None):
- """
- A convenience function to output a 'Started xxxx (PID yyyy)'
- message. As with starting_message(), this ensures a consistent
- format.
- """
- if pid is None:
- logger.debug(DBG_PROCESS, BIND10_STARTED_PROCESS, self.curproc)
- else:
- logger.debug(DBG_PROCESS, BIND10_STARTED_PROCESS_PID, self.curproc,
- pid)
-
- def process_running(self, msg, who):
- """
- Some processes return a message to the Init after they have
- started to indicate that they are running. The form of the
- message is a dictionary with contents {"running:", "<process>"}.
- This method checks the passed message and returns True if the
- "who" process is contained in the message (so is presumably
- running). It returns False for all other conditions and will
- log an error if appropriate.
- """
- if msg is not None:
- try:
- if msg["running"] == who:
- return True
- else:
- logger.error(BIND10_STARTUP_UNEXPECTED_MESSAGE, msg)
- except:
- logger.error(BIND10_STARTUP_UNRECOGNISED_MESSAGE, msg)
-
- return False
-
- # The next few methods start the individual processes of BIND-10. They
- # are called via start_all_processes(). If any fail, an exception is
- # raised which is caught by the caller of start_all_processes(); this kills
- # processes started up to that point before terminating the program.
-
- def _make_process_info(self, name, args, env,
- dev_null_stdout=False, dev_null_stderr=False):
- """
- Wrapper around ProcessInfo(), useful to override
- ProcessInfo() creation during testing.
- """
- return ProcessInfo(name, args, env, dev_null_stdout, dev_null_stderr)
-
- def start_msgq(self):
- """
- Start the message queue and connect to the command channel.
- """
- self.log_starting("b10-msgq")
- msgq_proc = self._make_process_info("b10-msgq", ["b10-msgq"],
- self.c_channel_env,
- not self.verbose, not self.verbose)
- msgq_proc.spawn()
- self.log_started(msgq_proc.pid)
-
- # Now connect to the c-channel
- cc_connect_start = time.time()
- while self.cc_session is None:
- # if we are run under unittests, break
- if self._run_under_unittests:
- break
-
- # if we have been trying for "a while" give up
- if (time.time() - cc_connect_start) > self.msgq_timeout:
- if msgq_proc.process:
- msgq_proc.process.kill()
- logger.error(BIND10_CONNECTING_TO_CC_FAIL)
- raise CChannelConnectError("Unable to connect to c-channel " +
- "after 5 seconds")
-
- # try to connect, and if we can't wait a short while
- try:
- self.cc_session = isc.cc.Session(self.msgq_socket_file)
- except isc.cc.session.SessionError:
- time.sleep(0.1)
-
- # Subscribe to the message queue. The only messages we expect to
- # receive on this channel are once relating to process startup.
- if self.cc_session is not None:
- self.cc_session.group_subscribe("Init")
-
- return msgq_proc
-
- def wait_msgq(self):
- """
- Wait for the message queue to fully start. It does so only after
- the config manager connects to it. We know it is ready when it
- starts answering commands.
-
- We don't add a specific command for it here, an error response is
- as good as positive one to know it is alive.
- """
- # We do 10 times shorter sleep here (since the start should be fast
- # now), so we have 10 times more attempts.
- time_remaining = self.wait_time * 10
- retry = True
- while time_remaining > 0 and retry:
- try:
- self.ccs.rpc_call('AreYouThere?', 'Msgq')
- # We don't expect this to succeed. If it does, it's programmer
- # error
- raise Exception("Non-existing RPC call succeeded")
- except isc.config.RPCRecipientMissing:
- retry = True # Not there yet
- time.sleep(0.1)
- time_remaining -= 1
- except isc.config.RPCError:
- retry = False # It doesn't like the RPC, so it's alive now
-
- if retry: # Still not started
- raise ProcessStartError("Msgq didn't complete the second stage " +
- "of startup")
-
- def start_cfgmgr(self):
- """
- Starts the configuration manager process
- """
- self.log_starting("b10-cfgmgr")
- args = ["b10-cfgmgr"]
- if self.data_path is not None:
- args.append("--data-path=" + self.data_path)
- if self.config_filename is not None:
- args.append("--config-filename=" + self.config_filename)
- if self.clear_config:
- args.append("--clear-config")
- bind_cfgd = self._make_process_info("b10-cfgmgr", args,
- self.c_channel_env)
- bind_cfgd.spawn()
- self.log_started(bind_cfgd.pid)
-
- # Wait for the configuration manager to start up as
- # subsequent initialization cannot proceed without it. The
- # time to wait can be set on the command line.
- time_remaining = self.wait_time
- msg, env = self.cc_session.group_recvmsg()
- while time_remaining > 0 and not self.process_running(msg,
- "ConfigManager"):
- logger.debug(DBG_PROCESS, BIND10_WAIT_CFGMGR)
- time.sleep(1)
- time_remaining = time_remaining - 1
- msg, env = self.cc_session.group_recvmsg()
-
- if not self.process_running(msg, "ConfigManager"):
- raise ProcessStartError("Configuration manager process has not " +
- "started")
-
- return bind_cfgd
-
- def start_ccsession(self, c_channel_env):
- """
- Start the CC Session
-
- The argument c_channel_env is unused but is supplied to keep the
- argument list the same for all start_xxx methods.
-
- With regards to logging, note that as the CC session is not a
- process, the log_starting/log_started methods are not used.
- """
- logger.info(BIND10_STARTING_CC)
-
- # Unsubscribe from the other CC session first, because we only
- # monitor one and msgq expects all data sent to us to be read,
- # or it will close its side of the socket.
- if self.cc_session is not None:
- self.cc_session.group_unsubscribe("Init")
-
- self.ccs = isc.config.ModuleCCSession(SPECFILE_LOCATION,
- self.config_handler,
- self.command_handler,
- socket_file = self.msgq_socket_file)
- self.ccs.start()
- logger.debug(DBG_PROCESS, BIND10_STARTED_CC)
-
- # A couple of utility methods for starting processes...
-
- def start_process(self, name, args, c_channel_env, port=None,
- address=None):
- """
- Given a set of command arguments, start the process and output
- appropriate log messages. If the start is successful, the process
- is added to the list of started processes.
-
- The port and address arguments are for log messages only.
- """
- self.log_starting(name, port, address)
- newproc = self._make_process_info(name, args, c_channel_env)
- newproc.spawn()
- self.log_started(newproc.pid)
- return newproc
-
- def register_process(self, pid, component):
- """
- Put another process into b10-init to watch over it. When the process
- dies, the component.failed() is called with the exit code.
-
- It is expected the info is a isc.bind10.component.BaseComponent
- subclass (or anything having the same interface).
- """
- self.components[pid] = component
-
- def start_simple(self, name):
- """
- Most of the BIND-10 processes are started with the command:
-
- <process-name> [-v]
-
- ... where -v is appended if verbose is enabled. This method
- generates the arguments from the name and starts the process.
-
- The port and address arguments are for log messages only.
- """
- # Set up the command arguments.
- args = [name]
- if self.verbose:
- args += ['-v']
-
- # ... and start the process
- return self.start_process(name, args, self.c_channel_env)
-
- # The next few methods start up the rest of the BIND-10 processes.
- # Although many of these methods are little more than a call to
- # start_simple, they are retained (a) for testing reasons and (b) as a
- # place where modifications can be made if the process start-up sequence
- # changes for a given process.
-
- def start_auth(self):
- """
- Start the Authoritative server
- """
- authargs = ['b10-auth']
- if self.verbose:
- authargs += ['-v']
-
- # ... and start
- return self.start_process("b10-auth", authargs, self.c_channel_env)
-
- def start_resolver(self):
- """
- Start the Resolver. At present, all these arguments and switches
- are pure speculation. As with the auth daemon, they should be
- read from the configuration database.
- """
- self.curproc = "b10-resolver"
- # XXX: this must be read from the configuration manager in the future
- resargs = ['b10-resolver']
- if self.verbose:
- resargs += ['-v']
-
- # ... and start
- return self.start_process("b10-resolver", resargs, self.c_channel_env)
-
- def start_cmdctl(self):
- """
- Starts the command control process
- """
- args = ["b10-cmdctl"]
- if self.cmdctl_port is not None:
- args.append("--port=" + str(self.cmdctl_port))
- if self.verbose:
- args.append("-v")
- return self.start_process("b10-cmdctl", args, self.c_channel_env,
- self.cmdctl_port)
-
- def start_all_components(self):
- """
- Starts up all the components. Any exception generated during the
- starting of the components is handled by the caller.
- """
- # Start the real core (sockcreator, msgq, cfgmgr)
- self._component_configurator.startup(self.__core_components)
-
- # Connect to the msgq. This is not a process, so it's not handled
- # inside the configurator.
- self.start_ccsession(self.c_channel_env)
-
- # Make sure msgq is fully started before proceeding to the rest
- # of the components.
- self.wait_msgq()
-
- # Extract the parameters associated with Init. This can only be
- # done after the CC Session is started. Note that the logging
- # configuration may override the "-v" switch set on the command line.
- self._read_bind10_config()
-
- # TODO: Return the dropping of privileges
-
- def startup(self):
- """
- Start the Init instance.
-
- Returns None if successful, otherwise an string describing the
- problem.
- """
- # Try to connect to the c-channel daemon, to see if it is already
- # running
- c_channel_env = {}
- if self.msgq_socket_file is not None:
- c_channel_env["BIND10_MSGQ_SOCKET_FILE"] = self.msgq_socket_file
- logger.debug(DBG_PROCESS, BIND10_CHECK_MSGQ_ALREADY_RUNNING)
- try:
- self.cc_session = isc.cc.Session(self.msgq_socket_file)
- logger.fatal(BIND10_MSGQ_ALREADY_RUNNING)
- if self.msgq_socket_file is not None:
- socket_name = "socket file '" + self.msg_socket_file + "'"
- else:
- socket_name = "default socket file"
- return "b10-msgq already running, or " + socket_name +\
- " not cleaned - cannot start"
- except isc.cc.session.SessionError:
- # this is the case we want, where the msgq is not running
- pass
-
- # Start all components. If any one fails to start, kill all started
- # components and exit with an error indication.
- try:
- self.c_channel_env = c_channel_env
- self.start_all_components()
- except ChangeUserError as e:
- self.kill_started_components()
- return str(e) + '; ' + NOTE_ON_LOCK_FILE.replace('\n', ' ')
- except Exception as e:
- self.kill_started_components()
- return "Unable to start " + self.curproc + ": " + str(e)
-
- # Started successfully
- self.runnable = True
- self.__started = True
- return None
-
- def stop_process(self, process, recipient, pid):
- """
- Stop the given process, friendly-like. The process is the name it has
- (in logs, etc), the recipient is the address on msgq. The pid is the
- pid of the process (if we have multiple processes of the same name,
- it might want to choose if it is for this one).
- """
- logger.info(BIND10_STOP_PROCESS, process)
- try:
- self.cc_session.group_sendmsg(isc.config.ccsession.
- create_command('shutdown',
- {'pid': pid}),
- recipient, recipient)
- except:
- logger.error(BIND10_COMPONENT_SHUTDOWN_ERROR, process)
- raise
-
- def component_shutdown(self, exitcode=0):
- """
- Stop the Init instance from a components' request. The exitcode
- indicates the desired exit code.
-
- If we did not start yet, it raises an exception, which is meant
- to propagate through the component and configurator to the startup
- routine and abort the startup immediately. If it is started up already,
- we just mark it so we terminate soon.
-
- It does set the exit code in both cases.
- """
- self.exitcode = exitcode
- if not self.__started:
- raise Exception("Component failed during startup");
- else:
- self.runnable = False
-
- def shutdown(self):
- """Stop the Init instance."""
- logger.info(BIND10_SHUTDOWN)
- # If ccsession is still there, inform rest of the system this module
- # is stopping. Since everything will be stopped shortly, this is not
- # really necessary, but this is done to reflect that b10-init is also
- # 'just' a module.
- self.ccs.send_stopping()
-
- # try using the BIND 10 request to stop
- try:
- self._component_configurator.shutdown()
- except:
- pass
- # XXX: some delay probably useful... how much is uncertain
- # I have changed the delay from 0.5 to 1, but sometime it's
- # still not enough.
- time.sleep(1)
- self.reap_children()
-
- # Send TERM and KILL signals to modules if we're not prevented
- # from doing so
- if not self.nokill:
- # next try sending a SIGTERM
- self.__kill_children(False)
- # finally, send SIGKILL (unmaskable termination) until everybody
- # dies
- while self.components:
- # XXX: some delay probably useful... how much is uncertain
- time.sleep(0.1)
- self.reap_children()
- self.__kill_children(True)
- logger.info(BIND10_SHUTDOWN_COMPLETE)
-
- def __kill_children(self, forceful):
- '''Terminate remaining subprocesses by sending a signal.
-
- The forceful paramter will be passed Component.kill().
- This is a dedicated subroutine of shutdown(), just to unify two
- similar cases.
-
- '''
- logmsg = BIND10_SEND_SIGKILL if forceful else BIND10_SEND_SIGTERM
- # We need to make a copy of values as the components may be modified
- # in the loop.
- for component in list(self.components.values()):
- logger.info(logmsg, component.name(), component.pid())
- try:
- component.kill(forceful)
- except OSError as ex:
- # If kill() failed due to EPERM, it doesn't make sense to
- # keep trying, so we just log the fact and forget that
- # component. Ignore other OSErrors (usually ESRCH because
- # the child finally exited)
- signame = "SIGKILL" if forceful else "SIGTERM"
- logger.info(BIND10_SEND_SIGNAL_FAIL, signame,
- component.name(), component.pid(), ex)
- if ex.errno == errno.EPERM:
- del self.components[component.pid()]
-
- def _get_process_exit_status(self):
- return os.waitpid(-1, os.WNOHANG)
-
- def reap_children(self):
- """Check to see if any of our child processes have exited,
- and note this for later handling.
- """
- while True:
- try:
- (pid, exit_status) = self._get_process_exit_status()
- except OSError as o:
- if o.errno == errno.ECHILD:
- break
- # XXX: should be impossible to get any other error here
- raise
- if pid == 0:
- break
- if pid in self.components:
- # One of the components we know about. Get information on it.
- component = self.components.pop(pid)
- logger.info(BIND10_PROCESS_ENDED, component.name(), pid,
- exit_status)
- if component.is_running() and self.runnable:
- # Tell it it failed. But only if it matters (we are
- # not shutting down and the component considers itself
- # to be running.
- component_restarted = component.failed(exit_status);
- # if the process wants to be restarted, but not just yet,
- # it returns False
- if not component_restarted:
- self.components_to_restart.append(component)
- else:
- logger.info(BIND10_UNKNOWN_CHILD_PROCESS_ENDED, pid)
-
- def restart_processes(self):
- """
- Restart any dead processes:
-
- * Returns the time when the next process is ready to be restarted.
- * If the server is shutting down, returns 0.
- * If there are no processes, returns None.
-
- The values returned can be safely passed into select() as the
- timeout value.
-
- """
- if not self.runnable:
- return 0
- still_dead = []
- # keep track of the first time we need to check this queue again,
- # if at all
- next_restart_time = None
- now = time.time()
- for component in self.components_to_restart:
- # If the component was removed from the configurator between since
- # scheduled to restart, just ignore it. The object will just be
- # dropped here.
- if not self._component_configurator.has_component(component):
- logger.info(BIND10_RESTART_COMPONENT_SKIPPED, component.name())
- elif not component.restart(now):
- still_dead.append(component)
- if next_restart_time is None or\
- next_restart_time > component.get_restart_time():
- next_restart_time = component.get_restart_time()
- self.components_to_restart = still_dead
-
- return next_restart_time
-
- def _get_socket(self, args):
- """
- Implementation of the get_socket CC command. It asks the cache
- to provide the token and sends the information back.
- """
- try:
- try:
- addr = isc.net.parse.addr_parse(args['address'])
- port = isc.net.parse.port_parse(args['port'])
- protocol = args['protocol']
- if protocol not in ['UDP', 'TCP']:
- raise ValueError("Protocol must be either UDP or TCP")
- share_mode = args['share_mode']
- if share_mode not in ['ANY', 'SAMEAPP', 'NO']:
- raise ValueError("Share mode must be one of ANY, SAMEAPP" +
- " or NO")
- share_name = args['share_name']
- except KeyError as ke:
- return \
- isc.config.ccsession.create_answer(1,
- "Missing parameter " +
- str(ke))
-
- # FIXME: This call contains blocking IPC. It is expected to be
- # short, but if it turns out to be problem, we'll need to do
- # something about it.
- token = self._socket_cache.get_token(protocol, addr, port,
- share_mode, share_name)
- return isc.config.ccsession.create_answer(0, {
- 'token': token,
- 'path': self._socket_path
- })
- except isc.bind10.socket_cache.SocketError as e:
- return isc.config.ccsession.create_answer(CREATOR_SOCKET_ERROR,
- str(e))
- except isc.bind10.socket_cache.ShareError as e:
- return isc.config.ccsession.create_answer(CREATOR_SHARE_ERROR,
- str(e))
- except Exception as e:
- return isc.config.ccsession.create_answer(1, str(e))
-
- def socket_request_handler(self, token, unix_socket):
- """
- This function handles a token that comes over a unix_domain socket.
- The function looks into the _socket_cache and sends the socket
- identified by the token back over the unix_socket.
- """
- try:
- token = str(token, 'ASCII') # Convert from bytes to str
- fd = self._socket_cache.get_socket(token, unix_socket.fileno())
- # FIXME: These two calls are blocking in their nature. An OS-level
- # buffer is likely to be large enough to hold all these data, but
- # if it wasn't and the remote application got stuck, we would have
- # a problem. If there appear such problems, we should do something
- # about it.
- unix_socket.sendall(CREATOR_SOCKET_OK)
- libutil_io_python.send_fd(unix_socket.fileno(), fd)
- except Exception as e:
- logger.info(BIND10_NO_SOCKET, token, e)
- unix_socket.sendall(CREATOR_SOCKET_UNAVAILABLE)
-
- def socket_consumer_dead(self, unix_socket):
- """
- This function handles when a unix_socket closes. This means all
- sockets sent to it are to be considered closed. This function signals
- so to the _socket_cache.
- """
- logger.info(BIND10_LOST_SOCKET_CONSUMER, unix_socket.fileno())
- try:
- self._socket_cache.drop_application(unix_socket.fileno())
- except ValueError:
- # This means the application holds no sockets. It's harmless, as it
- # can happen in real life - for example, it requests a socket, but
- # get_socket doesn't find it, so the application dies. It should be
- # rare, though.
- pass
-
- def set_creator(self, creator):
- """
- Registeres a socket creator into the b10-init. The socket creator is
- not used directly, but through a cache. The cache is created in this
- method.
-
- If called more than once, it raises a ValueError.
- """
- if self._socket_cache is not None:
- raise ValueError("A creator was inserted previously")
- self._socket_cache = isc.bind10.socket_cache.Cache(creator)
-
- def init_socket_srv(self):
- """
- Creates and listens on a unix-domain socket to be able to send out
- the sockets.
-
- This method should be called after switching user, or the switched
- applications won't be able to access the socket.
- """
- self._srv_socket = socket.socket(socket.AF_UNIX)
- # We create a temporary directory somewhere safe and unique, to avoid
- # the need to find the place ourself or bother users. Also, this
- # secures the socket on some platforms, as it creates a private
- # directory.
- self._tmpdir = tempfile.mkdtemp(prefix='sockcreator-')
- # Get the name
- self._socket_path = os.path.join(self._tmpdir, "sockcreator")
- # And bind the socket to the name
- self._srv_socket.bind(self._socket_path)
- self._srv_socket.listen(5)
-
- def remove_socket_srv(self):
- """
- Closes and removes the listening socket and the directory where it
- lives, as we created both.
-
- It does nothing if the _srv_socket is not set (eg. it was not yet
- initialized).
- """
- if self._srv_socket is not None:
- self._srv_socket.close()
- if os.path.exists(self._socket_path):
- os.remove(self._socket_path)
- if os.path.isdir(self._tmpdir):
- os.rmdir(self._tmpdir)
-
- def _srv_accept(self):
- """
- Accept a socket from the unix domain socket server and put it to the
- others we care about.
- """
- (socket, conn) = self._srv_socket.accept()
- self._unix_sockets[socket.fileno()] = (socket, b'')
-
- def _socket_data(self, socket_fileno):
- """
- This is called when a socket identified by the socket_fileno needs
- attention. We try to read data from there. If it is closed, we remove
- it.
- """
- (sock, previous) = self._unix_sockets[socket_fileno]
- while True:
- try:
- data = sock.recv(1, socket.MSG_DONTWAIT)
- except socket.error as se:
- # These two might be different on some systems
- if se.errno == errno.EAGAIN or se.errno == errno.EWOULDBLOCK:
- # No more data now. Oh, well, just store what we have.
- self._unix_sockets[socket_fileno] = (sock, previous)
- return
- else:
- data = b'' # Pretend it got closed
- if len(data) == 0: # The socket got to it's end
- del self._unix_sockets[socket_fileno]
- self.socket_consumer_dead(sock)
- sock.close()
- return
- else:
- if data == b"\n":
- # Handle this token and clear it
- self.socket_request_handler(previous, sock)
- previous = b''
- else:
- previous += data
-
- def run(self, wakeup_fd):
- """
- The main loop, waiting for sockets, commands and dead processes.
- Runs as long as the runnable is true.
-
- The wakeup_fd descriptor is the read end of pipe where CHLD signal
- handler writes.
- """
- ccs_fd = self.ccs.get_socket().fileno()
- while self.runnable:
- # clean up any processes that exited
- self.reap_children()
- next_restart = self.restart_processes()
- if next_restart is None:
- wait_time = None
- else:
- wait_time = max(next_restart - time.time(), 0)
-
- # select() can raise EINTR when a signal arrives,
- # even if they are resumable, so we have to catch
- # the exception
- try:
- (rlist, wlist, xlist) = \
- select.select([wakeup_fd, ccs_fd,
- self._srv_socket.fileno()] +
- list(self._unix_sockets.keys()), [], [],
- wait_time)
- except select.error as err:
- if err.args[0] == errno.EINTR:
- (rlist, wlist, xlist) = ([], [], [])
- else:
- logger.fatal(BIND10_SELECT_ERROR, err)
- break
-
- for fd in rlist + xlist:
- if fd == ccs_fd:
- try:
- self.ccs.check_command()
- except isc.cc.session.ProtocolError:
- logger.fatal(BIND10_MSGQ_DISAPPEARED)
- self.runnable = False
- break
- elif fd == wakeup_fd:
- os.read(wakeup_fd, 32)
- elif fd == self._srv_socket.fileno():
- self._srv_accept()
- elif fd in self._unix_sockets:
- self._socket_data(fd)
-
-# global variables, needed for signal handlers
-options = None
-b10_init = None
-
-def reaper(signal_number, stack_frame):
- """A child process has died (SIGCHLD received)."""
- # don't do anything...
- # the Python signal handler has been set up to write
- # down a pipe, waking up our select() bit
- pass
-
-def get_signame(signal_number):
- """Return the symbolic name for a signal."""
- for sig in dir(signal):
- if sig.startswith("SIG") and sig[3].isalnum():
- if getattr(signal, sig) == signal_number:
- return sig
- return "Unknown signal %d" % signal_number
-
-# XXX: perhaps register atexit() function and invoke that instead
-def fatal_signal(signal_number, stack_frame):
- """We need to exit (SIGINT or SIGTERM received)."""
- global options
- global b10_init
- logger.info(BIND10_RECEIVED_SIGNAL, get_signame(signal_number))
- signal.signal(signal.SIGCHLD, signal.SIG_DFL)
- b10_init.runnable = False
-
-def process_rename(option, opt_str, value, parser):
- """Function that renames the process if it is requested by a option."""
- isc.util.process.rename(value)
-
-def parse_args(args=sys.argv[1:], Parser=OptionParser):
- """
- Function for parsing command line arguments. Returns the
- options object from OptionParser.
- """
- parser = Parser(version=VERSION)
- parser.add_option("-m", "--msgq-socket-file", dest="msgq_socket_file",
- type="string", default=None,
- help="UNIX domain socket file the b10-msgq daemon " +
- "will use")
- parser.add_option("-i", "--no-kill", action="store_true", dest="nokill",
- default=False,
- help="do not send SIGTERM and SIGKILL signals to " +
- "modules during shutdown")
- parser.add_option("-u", "--user", dest="user", type="string", default=None,
- help="Change user after startup (must run as root)")
- parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
- help="display more about what is going on")
- parser.add_option("--pretty-name", type="string", action="callback",
- callback=process_rename,
- help="Set the process name (displayed in ps, top, ...)")
- parser.add_option("-c", "--config-file", action="store",
- dest="config_file", default=None,
- help="Configuration database filename")
- parser.add_option("--clear-config", action="store_true",
- dest="clear_config", default=False,
- help="Create backup of the configuration file and " +
- "start with a clean configuration")
- parser.add_option("-p", "--data-path", dest="data_path",
- help="Directory to search for configuration files",
- default=None)
- parser.add_option("--cmdctl-port", dest="cmdctl_port", type="int",
- default=None, help="Port of command control")
- parser.add_option("--pid-file", dest="pid_file", type="string",
- default=None,
- help="file to dump the PID of the BIND 10 process")
- parser.add_option("-w", "--wait", dest="wait_time", type="int",
- default=10,
- help="Time (in seconds) to wait for config manager to "
- "start up")
-
- (options, args) = parser.parse_args(args)
-
- if options.cmdctl_port is not None:
- try:
- isc.net.parse.port_parse(options.cmdctl_port)
- except ValueError as e:
- parser.error(e)
-
- if args:
- parser.print_help()
- sys.exit(1)
-
- return options
-
-def dump_pid(pid_file):
- """
- Dump the PID of the current process to the specified file. If the given
- file is None this function does nothing. If the file already exists,
- the existing content will be removed. If a system error happens in
- creating or writing to the file, the corresponding exception will be
- propagated to the caller.
- """
- if pid_file is None:
- return
- f = open(pid_file, "w")
- f.write('%d\n' % os.getpid())
- f.close()
-
-def unlink_pid_file(pid_file):
- """
- Remove the given file, which is basically expected to be the PID file
- created by dump_pid(). The specified may or may not exist; if it
- doesn't this function does nothing. Other system level errors in removing
- the file will be propagated as the corresponding exception.
- """
- if pid_file is None:
- return
- try:
- os.unlink(pid_file)
- except OSError as error:
- if error.errno is not errno.ENOENT:
- raise
-
-def remove_lock_files():
- """
- Remove various lock files which were created by code such as in the
- logger. This function should be called after BIND 10 shutdown.
- """
-
- lockfiles = ["logger_lockfile"]
-
- lpath = bind10_config.DATA_PATH
- if "B10_FROM_BUILD" in os.environ:
- lpath = os.environ["B10_FROM_BUILD"]
- if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
- lpath = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"]
- if "B10_LOCKFILE_DIR_FROM_BUILD" in os.environ:
- lpath = os.environ["B10_LOCKFILE_DIR_FROM_BUILD"]
-
- for f in lockfiles:
- fname = lpath + '/' + f
- if os.path.isfile(fname):
- try:
- os.unlink(fname)
- except OSError as e:
- # We catch and ignore permission related error on unlink.
- # This can happen if bind10 started with -u, created a lock
- # file as a privileged user, but the directory is not writable
- # for the changed user. This setup will cause immediate
- # start failure, and we leave verbose error message including
- # the leftover lock file, so it should be acceptable to ignore
- # it (note that it doesn't make sense to log this event at
- # this poitn)
- if e.errno != errno.EPERM and e.errno != errno.EACCES:
- raise
-
- return
-
-def main():
- global options
- global b10_init
- # Enforce line buffering on stdout, even when not a TTY
- sys.stdout = io.TextIOWrapper(sys.stdout.detach(), line_buffering=True)
-
- options = parse_args()
-
- # Announce startup. Making this is the first log message.
- try:
- logger.info(BIND10_STARTING, VERSION)
- except RuntimeError as e:
- sys.stderr.write('ERROR: failed to write the initial log: %s\n' %
- str(e))
- sys.stderr.write(NOTE_ON_LOCK_FILE)
- sys.exit(1)
-
- # Check user ID.
- setuid = None
- setgid = None
- username = None
- if options.user:
- # Try getting information about the user, assuming UID passed.
- try:
- pw_ent = pwd.getpwuid(int(options.user))
- setuid = pw_ent.pw_uid
- setgid = pw_ent.pw_gid
- username = pw_ent.pw_name
- except ValueError:
- pass
- except KeyError:
- pass
-
- # Next try getting information about the user, assuming user name
- # passed.
- # If the information is both a valid user name and user number, we
- # prefer the name because we try it second. A minor point, hopefully.
- try:
- pw_ent = pwd.getpwnam(options.user)
- setuid = pw_ent.pw_uid
- setgid = pw_ent.pw_gid
- username = pw_ent.pw_name
- except KeyError:
- pass
-
- if setuid is None:
- logger.fatal(BIND10_INVALID_USER, options.user)
- sys.exit(1)
-
- # Create wakeup pipe for signal handlers
- wakeup_pipe = os.pipe()
- signal.set_wakeup_fd(wakeup_pipe[1])
-
- # Set signal handlers for catching child termination, as well
- # as our own demise.
- signal.signal(signal.SIGCHLD, reaper)
- signal.siginterrupt(signal.SIGCHLD, False)
- signal.signal(signal.SIGINT, fatal_signal)
- signal.signal(signal.SIGTERM, fatal_signal)
-
- # Block SIGPIPE, as we don't want it to end this process
- signal.signal(signal.SIGPIPE, signal.SIG_IGN)
-
- try:
- b10_init = Init(options.msgq_socket_file, options.data_path,
- options.config_file, options.clear_config,
- options.verbose, options.nokill,
- setuid, setgid, username, options.cmdctl_port,
- options.wait_time)
- startup_result = b10_init.startup()
- if startup_result:
- logger.fatal(BIND10_STARTUP_ERROR, startup_result)
- sys.exit(1)
- b10_init.init_socket_srv()
- logger.info(BIND10_STARTUP_COMPLETE)
- dump_pid(options.pid_file)
-
- # Let it run
- b10_init.run(wakeup_pipe[0])
-
- # shutdown
- signal.signal(signal.SIGCHLD, signal.SIG_DFL)
- b10_init.shutdown()
- finally:
- # Clean up the filesystem
- unlink_pid_file(options.pid_file)
- remove_lock_files()
- if b10_init is not None:
- b10_init.remove_socket_srv()
- sys.exit(b10_init.exitcode)
-
-if __name__ == "__main__":
- isc.util.traceback_handler.traceback_handler(main)
+++ /dev/null
-{
- "module_spec": {
- "module_name": "Init",
- "module_description": "Init process",
- "config_data": [
- {
- "item_name": "components",
- "item_type": "named_set",
- "item_optional": false,
- "item_default": {
- "b10-stats": { "address": "Stats", "kind": "dispensable" },
- "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
- },
- "named_set_item_spec": {
- "item_name": "component",
- "item_type": "map",
- "item_optional": false,
- "item_default": { },
- "map_item_spec": [
- {
- "item_name": "special",
- "item_optional": true,
- "item_type": "string"
- },
- {
- "item_name": "process",
- "item_optional": true,
- "item_type": "string"
- },
- {
- "item_name": "kind",
- "item_optional": false,
- "item_type": "string",
- "item_default": "dispensable"
- },
- {
- "item_name": "address",
- "item_optional": true,
- "item_type": "string"
- },
- {
- "item_name": "params",
- "item_optional": true,
- "item_type": "list",
- "list_item_spec": {
- "item_name": "param",
- "item_optional": false,
- "item_type": "string",
- "item_default": ""
- }
- },
- {
- "item_name": "priority",
- "item_optional": true,
- "item_type": "integer"
- }
- ]
- }
- }
- ],
- "commands": [
- {
- "command_name": "shutdown",
- "command_description": "Shut down BIND 10",
- "command_args": []
- },
- {
- "command_name": "ping",
- "command_description": "Ping the b10-init process",
- "command_args": []
- },
- {
- "command_name": "show_processes",
- "command_description": "List the running BIND 10 processes",
- "command_args": []
- }
- ],
- "statistics": [
- {
- "item_name": "boot_time",
- "item_type": "string",
- "item_optional": false,
- "item_default": "1970-01-01T00:00:00Z",
- "item_title": "Boot time",
- "item_description": "A date time when bind10 process starts initially",
- "item_format": "date-time"
- }
- ]
- }
-}
-
-
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-# PERFORMANCE OF THIS SOFTWARE.
-
-# No namespace declaration - these constants go in the global namespace
-# of the xfrin messages python module.
-
-% BIND10_CHECK_MSGQ_ALREADY_RUNNING checking if msgq is already running
-The b10-init process is starting up and will now check if the message bus
-daemon is already running. If so, it will not be able to start, as it
-needs a dedicated message bus.
-
-% BIND10_COMPONENT_FAILED component %1 (pid %2) failed: %3
-The process terminated, but b10-init didn't expect it to, which means
-it must have failed.
-
-% BIND10_COMPONENT_RESTART component %1 is about to restart
-The named component failed previously and we will try to restart it to provide
-as flawless service as possible, but it should be investigated what happened,
-as it could happen again.
-
-% BIND10_COMPONENT_SHUTDOWN_ERROR An error occurred stopping component %1
-An attempt to gracefully shutdown a component failed.
-
-% BIND10_COMPONENT_START component %1 is starting
-The named component is about to be started by the b10-init process.
-
-% BIND10_COMPONENT_START_EXCEPTION component %1 failed to start: %2
-An exception (mentioned in the message) happened during the startup of the
-named component. The component is not considered started and further actions
-will be taken about it.
-
-% BIND10_COMPONENT_STOP component %1 is being stopped
-A component is about to be asked to stop willingly by the b10-init.
-
-% BIND10_COMPONENT_UNSATISFIED component %1 is required to run and failed
-A component failed for some reason (see previous messages). It is either a core
-component or needed component that was just started. In any case, the system
-can't continue without it and will terminate.
-
-% BIND10_CONFIGURATOR_BUILD building plan '%1' -> '%2'
-A debug message. This indicates that the configurator is building a plan
-how to change configuration from the older one to newer one. This does no
-real work yet, it just does the planning what needs to be done.
-
-% BIND10_CONFIGURATOR_PLAN_INTERRUPTED configurator plan interrupted, only %1 of %2 done
-There was an exception during some planned task. The plan will not continue and
-only some tasks of the plan were completed. The rest is aborted. The exception
-will be propagated.
-
-% BIND10_CONFIGURATOR_RECONFIGURE reconfiguring running components
-A different configuration of which components should be running is being
-installed. All components that are no longer needed will be stopped and
-newly introduced ones started. This happens at startup, when the configuration
-is read the first time, or when an operator changes configuration of the b10-init.
-
-% BIND10_CONFIGURATOR_RUN running plan of %1 tasks
-A debug message. The configurator is about to execute a plan of actions it
-computed previously.
-
-% BIND10_CONFIGURATOR_START bind10 component configurator is starting up
-The part that cares about starting and stopping the right component from
-the b10-init process is starting up. This happens only once at the startup
-of the b10-init process. It will start the basic set of processes now (the
-ones b10-init needs to read the configuration), the rest will be started
-after the configuration is known.
-
-% BIND10_CONFIGURATOR_STOP bind10 component configurator is shutting down
-The part that cares about starting and stopping processes in the b10-init is
-shutting down. All started components will be shut down now (more precisely,
-asked to terminate by their own, if they fail to comply, other parts of
-the b10-init process will try to force them).
-
-% BIND10_CONFIGURATOR_TASK performing task %1 on %2
-A debug message. The configurator is about to perform one task of the plan it
-is currently executing on the named component.
-
-% BIND10_CONNECTING_TO_CC_FAIL failed to connect to configuration/command channel; try -v to see output from msgq
-The b10-init process tried to connect to the communication channel for
-commands and configuration updates during initialization, but it
-failed. This is a fatal startup error, and process will soon
-terminate after some cleanup. There can be several reasons for the
-failure, but the most likely cause is that the msgq daemon failed to
-start, and the most likely cause of the msgq failure is that it
-doesn't have a permission to create a socket file for the
-communication. To confirm that, you can see debug messages from msgq
-by starting BIND 10 with the -v command line option. If it indicates
-permission problem for msgq, make sure the directory where the socket
-file is to be created is writable for the msgq process. Note that if
-you specify the -u option to change process users, the directory must
-be writable for that user.
-
-% BIND10_INVALID_STATISTICS_DATA invalid specification of statistics data specified
-An error was encountered when the b10-init module specified
-statistics data which is invalid for the b10-init specification file.
-
-% BIND10_INVALID_USER invalid user: %1
-The b10-init process was started with the -u option, to drop root privileges
-and continue running as the specified user, but the user is unknown.
-
-% BIND10_KILLING_ALL_PROCESSES killing all started processes
-The b10-init module was not able to start every process it needed to start
-during startup, and will now kill the processes that did get started.
-
-% BIND10_LOST_SOCKET_CONSUMER consumer %1 of sockets disconnected, considering all its sockets closed
-A connection from one of the applications which requested a socket was
-closed. This means the application has terminated, so all the sockets it was
-using are now closed and bind10 process can release them as well, unless the
-same sockets are used by yet another application.
-
-% BIND10_MSGQ_ALREADY_RUNNING msgq daemon already running, cannot start
-There already appears to be a message bus daemon running. Either an
-old process was not shut down correctly, and needs to be killed, or
-another instance of BIND10, with the same msgq domain socket, is
-running, which needs to be stopped.
-
-% BIND10_MSGQ_DISAPPEARED msgq channel disappeared
-While listening on the message bus channel for messages, it suddenly
-disappeared. The msgq daemon may have died. This might lead to an
-inconsistent state of the system, and BIND 10 will now shut down.
-
-% BIND10_NO_SOCKET couldn't send a socket for token %1 because of error: %2
-An error occurred when the bind10 process was asked to send a socket file
-descriptor. The error is mentioned, most common reason is that the request
-is invalid and may not come from bind10 process at all.
-
-% BIND10_PROCESS_ENDED process %2 of %1 ended with status %3
-This indicates a process started previously terminated. The process id
-and component owning the process are indicated, as well as the exit code.
-This doesn't distinguish if the process was supposed to terminate or not.
-
-% BIND10_READING_INIT_CONFIGURATION reading b10-init configuration
-The b10-init process is starting up, and will now process the initial
-configuration, as received from the configuration manager.
-
-% BIND10_RECEIVED_COMMAND received command: %1
-The b10-init module received a command and shall now process it. The command
-is printed.
-
-% BIND10_RECEIVED_NEW_CONFIGURATION received new configuration: %1
-The b10-init module received a configuration update and is going to apply
-it now. The new configuration is printed.
-
-% BIND10_RECEIVED_SIGNAL received signal %1
-The b10-init module received the given signal.
-
-% BIND10_RECONFIGURE_ERROR Error applying new config: %1
-A new configuration was received, but there was an error doing the
-re-configuration.
-
-% BIND10_RESTART_COMPONENT_SKIPPED Skipped restarting a component %1
-The b10-init module tried to restart a component after it failed (crashed)
-unexpectedly, but the b10-init then found that the component had been removed
-from its local configuration of components to run. This is an unusual
-situation but can happen if the administrator removes the component from
-the configuration after the component's crash and before the restart time.
-The b10-init module simply skipped restarting that module, and the whole system
-went back to the expected state (except that the crash itself is likely
-to be a bug).
-
-% BIND10_RESURRECTED_PROCESS resurrected %1 (PID %2)
-The given process has been restarted successfully, and is now running
-with the given process id.
-
-% BIND10_RESURRECTING_PROCESS resurrecting dead %1 process...
-The given process has ended unexpectedly, and is now restarted.
-
-% BIND10_SELECT_ERROR error in select() call: %1
-There was a fatal error in the call to select(), used to see if a child
-process has ended or if there is a message on the message bus. This
-should not happen under normal circumstances and is considered fatal,
-so BIND 10 will now shut down. The specific error is printed.
-
-% BIND10_SEND_SIGKILL sending SIGKILL to %1 (PID %2)
-The b10-init module is sending a SIGKILL signal to the given process.
-
-% BIND10_SEND_SIGNAL_FAIL sending %1 to %2 (PID %3) failed: %4
-The b10-init module sent a single (either SIGTERM or SIGKILL) to a process,
-but it failed due to some system level error. There are two major cases:
-the target process has already terminated but the b10-init module had sent
-the signal before it noticed the termination. In this case an error
-message should indicate something like "no such process". This can be
-safely ignored. The other case is that the b10-init module doesn't have
-the privilege to send a signal to the process. It can typically
-happen when the b10-init module started as a privileged process, spawned a
-subprocess, and then dropped the privilege. It includes the case for
-the socket creator when the b10-init process runs with the -u command line
-option. In this case, the b10-init module simply gives up to terminate
-the process explicitly because it's unlikely to succeed by keeping
-sending the signal. Although the socket creator is implemented so
-that it will terminate automatically when the b10-init process exits
-(and that should be the case for any other future process running with
-a higher privilege), but it's recommended to check if there's any
-remaining BIND 10 process if this message is logged. For all other
-cases, the b10-init module will keep sending the signal until it confirms
-all child processes terminate. Although unlikely, this could prevent
-the b10-init module from exiting, just keeping sending the signals. So,
-again, it's advisable to check if it really terminates when this
-message is logged.
-
-% BIND10_SEND_SIGTERM sending SIGTERM to %1 (PID %2)
-The b10-init module is sending a SIGTERM signal to the given process.
-
-% BIND10_SETGID setting GID to %1
-The b10-init switches the process group ID to the given value. This happens
-when BIND 10 starts with the -u option, and the group ID will be set to
-that of the specified user.
-
-% BIND10_SETUID setting UID to %1
-The b10-init switches the user it runs as to the given UID.
-
-% BIND10_SHUTDOWN stopping the server
-The b10-init process received a command or signal telling it to shut down.
-It will send a shutdown command to each process. The processes that do
-not shut down will then receive a SIGTERM signal. If that doesn't work,
-it shall send SIGKILL signals to the processes still alive.
-
-% BIND10_SHUTDOWN_COMPLETE all processes ended, shutdown complete
-All child processes have been stopped, and the b10-init process will now
-stop itself.
-
-% BIND10_SOCKCREATOR_BAD_CAUSE unknown error cause from socket creator: %1
-The socket creator reported an error when creating a socket. But the function
-which failed is unknown (not one of 'S' for socket or 'B' for bind).
-
-% BIND10_SOCKCREATOR_BAD_RESPONSE unknown response for socket request: %1
-The b10-init requested a socket from the creator, but the answer is unknown. This
-looks like a programmer error.
-
-% BIND10_SOCKCREATOR_EOF eof while expecting data from socket creator
-There should be more data from the socket creator, but it closed the socket.
-It probably crashed.
-
-% BIND10_SOCKCREATOR_INIT initializing socket creator parser
-The b10-init module initializes routines for parsing the socket creator
-protocol.
-
-% BIND10_SOCKCREATOR_KILL killing the socket creator
-The socket creator is being terminated the aggressive way, by sending it
-sigkill. This should not happen usually.
-
-% BIND10_SOCKCREATOR_TERMINATE terminating socket creator
-The b10-init module sends a request to terminate to the socket creator.
-
-% BIND10_SOCKCREATOR_TRANSPORT_ERROR transport error when talking to the socket creator: %1
-Either sending or receiving data from the socket creator failed with the given
-error. The creator probably crashed or some serious OS-level problem happened,
-as the communication happens only on local host.
-
-% BIND10_SOCKET_CREATED successfully created socket %1
-The socket creator successfully created and sent a requested socket, it has
-the given file number.
-
-% BIND10_SOCKET_ERROR error on %1 call in the creator: %2/%3
-The socket creator failed to create the requested socket. It failed on the
-indicated OS API function with given error.
-
-% BIND10_SOCKET_GET requesting socket [%1]:%2 of type %3 from the creator
-The b10-init forwards a request for a socket to the socket creator.
-
-% BIND10_STARTED_CC started configuration/command session
-Debug message given when BIND 10 has successfully started the object that
-handles configuration and commands.
-
-% BIND10_STARTED_PROCESS started %1
-The given process has successfully been started.
-
-% BIND10_STARTED_PROCESS_PID started %1 (PID %2)
-The given process has successfully been started, and has the given PID.
-
-% BIND10_STARTING starting BIND10: %1
-Informational message on startup that shows the full version.
-
-% BIND10_STARTING_CC starting configuration/command session
-Informational message given when BIND 10 is starting the session object
-that handles configuration and commands.
-
-% BIND10_STARTING_PROCESS starting process %1
-The b10-init module is starting the given process.
-
-% BIND10_STARTING_PROCESS_PORT starting process %1 (to listen on port %2)
-The b10-init module is starting the given process, which will listen on the
-given port number.
-
-% BIND10_STARTING_PROCESS_PORT_ADDRESS starting process %1 (to listen on %2)
-The b10-init module is starting the given process, which will listen on the
-given address and port number (written as <address>:<port>).
-
-% BIND10_STARTUP_COMPLETE BIND 10 started
-All modules have been successfully started, and BIND 10 is now running.
-
-% BIND10_STARTUP_ERROR error during startup: %1
-There was a fatal error when BIND10 was trying to start. The error is
-shown, and BIND10 will now shut down.
-
-% BIND10_STARTUP_UNEXPECTED_MESSAGE unrecognised startup message %1
-During the startup process, a number of messages are exchanged between the
-Init process and the processes it starts. This error is output when a
-message received by the Init process is recognised as being of the
-correct format but is unexpected. It may be that processes are starting
-of sequence.
-
-% BIND10_STARTUP_UNRECOGNISED_MESSAGE unrecognised startup message %1
-During the startup process, a number of messages are exchanged between the
-Init process and the processes it starts. This error is output when a
-message received by the Init process is not recognised.
-
-% BIND10_STOP_PROCESS asking %1 to shut down
-The b10-init module is sending a shutdown command to the given module over
-the message channel.
-
-% BIND10_UNKNOWN_CHILD_PROCESS_ENDED unknown child pid %1 exited
-An unknown child process has exited. The PID is printed, but no further
-action will be taken by the b10-init process.
-
-% BIND10_WAIT_CFGMGR waiting for configuration manager process to initialize
-The configuration manager process is so critical to operation of BIND 10
-that after starting it, the Init module will wait for it to initialize
-itself before continuing. This debug message is produced during the
-wait and may be output zero or more times depending on how long it takes
-the configuration manager to start up. The total length of time Init
-will wait for the configuration manager before reporting an error is
-set with the command line --wait switch, which has a default value of
-ten seconds.
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-BIND10_PATH=@abs_top_builddir@/src/bin/bind10
-
-PATH=@abs_top_builddir@/src/bin/msgq:@abs_top_builddir@/src/bin/auth:@abs_top_builddir@/src/bin/resolver:@abs_top_builddir@/src/bin/cfgmgr:@abs_top_builddir@/src/bin/cmdctl:@abs_top_builddir@/src/bin/stats:@abs_top_builddir@/src/bin/xfrin:@abs_top_builddir@/src/bin/xfrout:@abs_top_builddir@/src/bin/zonemgr:@abs_top_builddir@/src/bin/ddns:@abs_top_builddir@/src/bin/dhcp6:@abs_top_builddir@/src/bin/sockcreator:@abs_top_builddir@/src/bin/memmgr:$PATH
-export PATH
-
-PYTHONPATH=@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python/isc/cc:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/python/isc/config:@abs_top_builddir@/src/lib/python/isc/acl/.libs:@abs_top_builddir@/src/lib/python/isc/datasrc/.libs
-export PYTHONPATH
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-SET_ENV_LIBRARY_PATH=@SET_ENV_LIBRARY_PATH@
-if test $SET_ENV_LIBRARY_PATH = yes; then
- @ENV_LIBRARY_PATH@=@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/cryptolink/.libs:@abs_top_builddir@/src/lib/cc/.libs:@abs_top_builddir@/src/lib/config/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/acl/.libs:@abs_top_builddir@/src/lib/util/.libs:@abs_top_builddir@/src/lib/util/threads/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/exceptions/.libs:@abs_top_builddir@/src/lib/datasrc/.libs:$@ENV_LIBRARY_PATH@
- export @ENV_LIBRARY_PATH@
-fi
-
-B10_FROM_SOURCE=@abs_top_srcdir@
-export B10_FROM_SOURCE
-# TODO: We need to do this feature based (ie. no general from_source)
-# But right now we need a second one because some spec files are
-# generated and hence end up under builddir
-B10_FROM_BUILD=@abs_top_builddir@
-export B10_FROM_BUILD
-
-BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
-export BIND10_MSGQ_SOCKET_FILE
-
-exec ${BIND10_PATH}/b10-init "$@"
-
+++ /dev/null
-/init_test.py
+++ /dev/null
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-#PYTESTS = args_test.py bind10_test.py
-# NOTE: this has a generated test found in the builddir
-PYTESTS = init_test.py
-noinst_SCRIPTS = $(PYTESTS)
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- chmod +x $(abs_builddir)/$$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
- B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
- BIND10_MSGQ_SOCKET_FILE=$(abs_top_builddir)/msgq_socket \
- $(PYCOVERAGE_RUN) $(abs_builddir)/$$pytest || exit ; \
- done
+++ /dev/null
-# Copyright (C) 2010,2013 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-This program tests the b10-init process to make sure that it runs while
-dropping permissions. It must be run as a user that can set permission.
-"""
-import unittest
-import os
-import sys
-import subprocess
-import select
-import time
-import pwd
-
-# Set to a valid user name on the system to run setuid() test
-#SUID_USER=None
-SUID_USER="shane"
-
-BIND10_EXE="../run_bind10.sh"
-TIMEOUT=3
-
-class TestInitArgs(unittest.TestCase):
- def _waitForString(self, init, s):
- found_string = False
- start_time = time.time()
- while time.time() < start_time + TIMEOUT:
- (r,w,x) = select.select((init.stdout,), (), (), TIMEOUT)
- if init.stdout in r:
- s = init.stdout.readline()
- if s == '':
- break
- if s.startswith(s):
- found_string = True
- break
- return found_string
-
- def testNoArgs(self):
- """Run bind10 without any arguments"""
- init = subprocess.Popen(args=(BIND10_EXE,),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- started_ok = self._waitForString(init, '[bind10] BIND 10 started')
- time.sleep(0.1)
- init.terminate()
- init.wait()
- self.assertTrue(started_ok)
-
- def testBadOption(self):
- """Run bind10 with a bogus option"""
- init = subprocess.Popen(args=(BIND10_EXE, "--badoption"),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- failed = self._waitForString(init, 'bind10: error: no such option: --badoption')
- time.sleep(0.1)
- init.terminate()
- self.assertTrue(init.wait() == 2)
- self.assertTrue(failed)
-
- def testArgument(self):
- """Run bind10 with an argument (this is not allowed)"""
- init = subprocess.Popen(args=(BIND10_EXE, "argument"),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- failed = self._waitForString(init, 'Usage: bind10 [options]')
- time.sleep(0.1)
- init.terminate()
- self.assertTrue(init.wait() == 1)
- self.assertTrue(failed)
-
- def testBadUser(self):
- """Run bind10 with a bogus user"""
- init = subprocess.Popen(args=(BIND10_EXE, "-u", "bogus_user"),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- failed = self._waitForString(init, "bind10: invalid user: 'bogus_user'")
- time.sleep(0.1)
- init.terminate()
- self.assertTrue(init.wait() == 1)
- self.assertTrue(failed)
-
- def testBadUid(self):
- """Run bind10 with a bogus user ID"""
- init = subprocess.Popen(args=(BIND10_EXE, "-u", "999999999"),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- failed = self._waitForString(init, "bind10: invalid user: '999999999'")
- time.sleep(0.1)
- init.terminate()
- self.assertTrue(init.wait() == 1)
- self.assertTrue(failed)
-
- def testFailSetUser(self):
- """Try the -u option when we don't run as root"""
- global SUID_USER
- if SUID_USER is None:
- self.skipTest("test needs a valid user (set when run)")
- if os.getuid() == 0:
- self.skipTest("test must not be run as root (uid is 0)")
- # XXX: we depend on the "nobody" user
- init = subprocess.Popen(args=(BIND10_EXE, "-u", "nobody"),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- failed = self._waitForString(init, "[bind10] Error on startup: Unable to start b10-msgq; Unable to change to user nobody")
- time.sleep(0.1)
- init.terminate()
- self.assertTrue(init.wait() == 1)
- self.assertTrue(failed)
-
- def testSetUser(self):
- """Try the -u option"""
- global SUID_USER
- if SUID_USER is None:
- self.skipTest("test needs a valid user (set when run)")
- if os.getuid() != 0:
- self.skipTest("test must run as root (uid is not 0)")
- if os.geteuid() != 0:
- self.skipTest("test must run as root (euid is not 0)")
-
- init = subprocess.Popen(args=(BIND10_EXE, "-u", SUID_USER),
- stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- started_ok = self._waitForString(init, '[bind10] BIND 10 started')
- self.assertTrue(started_ok)
- ps = subprocess.Popen(args=("ps", "axo", "user,pid"),
- stdout=subprocess.PIPE)
- s = ps.stdout.readline()
- ps_user = None
- while True:
- s = ps.stdout.readline()
- if s == '': break
- (user, pid) = s.split()
- if int(pid) == init.pid:
- ps_user = user.decode()
- break
- self.assertTrue(ps_user is not None)
- self.assertTrue(ps_user == SUID_USER)
- time.sleep(0.1)
- init.terminate()
- x = init.wait()
- self.assertTrue(init.wait() == 0)
-
- def testPrettyName(self):
- """Try the --pretty-name option."""
- CMD_PRETTY_NAME = b'init-name-test'
- init = subprocess.Popen(args=(BIND10_EXE, '--pretty-name',
- CMD_PRETTY_NAME), stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- started_ok = self._waitForString(init, '[bind10] BIND 10 started')
- self.assertTrue(started_ok)
- ps = subprocess.Popen(args=("ps", "axo", "pid,comm"),
- stdout=subprocess.PIPE)
- s = ps.stdout.readline()
- command = None
- while True:
- s = ps.stdout.readline()
- if s == '': break
- (pid,comm) = s.split(None, 1)
- if int(pid) == init.pid:
- command = comm
- break
- self.assertEqual(command, CMD_PRETTY_NAME + b'\n')
- time.sleep(0.1)
- init.terminate()
- init.wait()
-
-if __name__ == '__main__':
- unittest.main()
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# Most of the time, we omit the "init" for brevity. Sometimes,
-# we want to be explicit about what we do, like when hijacking a library
-# call used by the b10-init.
-from init import Init, ProcessInfo, parse_args, dump_pid, unlink_pid_file, \
- _BASETIME
-import init
-
-# XXX: environment tests are currently disabled, due to the preprocessor
-# setup that we have now complicating the environment
-
-import unittest
-import sys
-import os
-import os.path
-import copy
-import signal
-import socket
-from isc.net.addr import IPAddr
-import time
-import isc.log
-import isc.config
-import isc.bind10.socket_cache
-import errno
-import random
-
-from isc.testutils.parse_args import TestOptParser, OptsError
-from isc.testutils.ccsession_mock import MockModuleCCSession
-
-class TestProcessInfo(unittest.TestCase):
- def setUp(self):
- # redirect stdout to a pipe so we can check that our
- # process spawning is doing the right thing with stdout
- self.old_stdout = os.dup(sys.stdout.fileno())
- self.pipes = os.pipe()
- os.dup2(self.pipes[1], sys.stdout.fileno())
- os.close(self.pipes[1])
- # note that we use dup2() to restore the original stdout
- # to the main program ASAP in each test... this prevents
- # hangs reading from the child process (as the pipe is only
- # open in the child), and also insures nice pretty output
-
- def tearDown(self):
- # clean up our stdout munging
- os.dup2(self.old_stdout, sys.stdout.fileno())
- os.close(self.pipes[0])
-
- def test_init(self):
- pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ])
- pi.spawn()
- os.dup2(self.old_stdout, sys.stdout.fileno())
- self.assertEqual(pi.name, 'Test Process')
- self.assertEqual(pi.args, [ '/bin/echo', 'foo' ])
-# self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
-# 'PYTHON_EXEC': os.environ['PYTHON_EXEC'] })
- self.assertEqual(pi.dev_null_stdout, False)
- self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
- self.assertNotEqual(pi.process, None)
- self.assertTrue(type(pi.pid) is int)
-
-# def test_setting_env(self):
-# pi = ProcessInfo('Test Process', [ '/bin/true' ], env={'FOO': 'BAR'})
-# os.dup2(self.old_stdout, sys.stdout.fileno())
-# self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
-# 'PYTHON_EXEC': os.environ['PYTHON_EXEC'],
-# 'FOO': 'BAR' })
-
- def test_setting_null_stdout(self):
- pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ],
- dev_null_stdout=True)
- pi.spawn()
- os.dup2(self.old_stdout, sys.stdout.fileno())
- self.assertEqual(pi.dev_null_stdout, True)
- self.assertEqual(os.read(self.pipes[0], 100), b"")
-
- def test_respawn(self):
- pi = ProcessInfo('Test Process', [ '/bin/echo', 'foo' ])
- pi.spawn()
- # wait for old process to work...
- self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
- # respawn it
- old_pid = pi.pid
- pi.respawn()
- os.dup2(self.old_stdout, sys.stdout.fileno())
- # make sure the new one started properly
- self.assertEqual(pi.name, 'Test Process')
- self.assertEqual(pi.args, [ '/bin/echo', 'foo' ])
-# self.assertEqual(pi.env, { 'PATH': os.environ['PATH'],
-# 'PYTHON_EXEC': os.environ['PYTHON_EXEC'] })
- self.assertEqual(pi.dev_null_stdout, False)
- self.assertEqual(os.read(self.pipes[0], 100), b"foo\n")
- self.assertNotEqual(pi.process, None)
- self.assertTrue(type(pi.pid) is int)
- self.assertNotEqual(pi.pid, old_pid)
-
-class TestCacheCommands(unittest.TestCase):
- """
- Test methods of b10-init related to the socket cache and socket handling.
- """
- def setUp(self):
- """
- Prepare b10-init for some tests.
-
- Also prepare some variables we need.
- """
- self.__b10_init = Init()
- # Fake the cache here so we can pretend it is us and hijack the
- # calls to its methods.
- self.__b10_init._socket_cache = self
- self.__b10_init._socket_path = '/socket/path'
- self.__raise_exception = None
- self.__socket_args = {
- "port": 53,
- "address": "::",
- "protocol": "UDP",
- "share_mode": "ANY",
- "share_name": "app"
- }
- # What was and wasn't called.
- self.__drop_app_called = None
- self.__get_socket_called = None
- self.__send_fd_called = None
- self.__get_token_called = None
- self.__drop_socket_called = None
- init.libutil_io_python.send_fd = self.__send_fd
-
- def __send_fd(self, to, socket):
- """
- A function to hook the send_fd in the b10-init.
- """
- self.__send_fd_called = (to, socket)
-
- class FalseSocket:
- """
- A socket where we can fake methods we need instead of having a real
- socket.
- """
- def __init__(self):
- self.send = b""
- def fileno(self):
- """
- The file number. Used for identifying the remote application.
- """
- return 42
-
- def sendall(self, data):
- """
- Adds data to the self.send.
- """
- self.send += data
-
- def drop_application(self, application):
- """
- Part of pretending to be the cache. Logs the parameter to
- self.__drop_app_called.
-
- In the case self.__raise_exception is set, the exception there
- is raised instead.
- """
- if self.__raise_exception is not None:
- raise self.__raise_exception
- self.__drop_app_called = application
-
- def test_consumer_dead(self):
- """
- Test that it calls the drop_application method of the cache.
- """
- self.__b10_init.socket_consumer_dead(self.FalseSocket())
- self.assertEqual(42, self.__drop_app_called)
-
- def test_consumer_dead_invalid(self):
- """
- Test that it doesn't crash in case the application is not known to
- the cache, the b10_init doesn't crash, as this actually can happen in
- practice.
- """
- self.__raise_exception = ValueError("This application is unknown")
- # This doesn't crash
- self.__b10_init.socket_consumer_dead(self.FalseSocket())
-
- def get_socket(self, token, application):
- """
- Part of pretending to be the cache. If there's anything in
- __raise_exception, it is raised. Otherwise, the call is logged
- into __get_socket_called and a number is returned.
- """
- if self.__raise_exception is not None:
- raise self.__raise_exception
- self.__get_socket_called = (token, application)
- return 13
-
- def test_request_handler(self):
- """
- Test that a request for socket is forwarded and the socket is sent
- back, if it returns a socket.
- """
- socket = self.FalseSocket()
- # An exception from the cache
- self.__raise_exception = ValueError("Test value error")
- self.__b10_init.socket_request_handler(b"token", socket)
- # It was called, but it threw, so it is not noted here
- self.assertIsNone(self.__get_socket_called)
- self.assertEqual(b"0\n", socket.send)
- # It should not have sent any socket.
- self.assertIsNone(self.__send_fd_called)
- # Now prepare a valid scenario
- self.__raise_exception = None
- socket.send = b""
- self.__b10_init.socket_request_handler(b"token", socket)
- self.assertEqual(b"1\n", socket.send)
- self.assertEqual((42, 13), self.__send_fd_called)
- self.assertEqual(("token", 42), self.__get_socket_called)
-
- def get_token(self, protocol, address, port, share_mode, share_name):
- """
- Part of pretending to be the cache. If there's anything in
- __raise_exception, it is raised. Otherwise, the parameters are
- logged into __get_token_called and a token is returned.
- """
- if self.__raise_exception is not None:
- raise self.__raise_exception
- self.__get_token_called = (protocol, address, port, share_mode,
- share_name)
- return "token"
-
- def test_get_socket_ok(self):
- """
- Test the successful scenario of getting a socket.
- """
- result = self.__b10_init._get_socket(self.__socket_args)
- [code, answer] = result['result']
- self.assertEqual(0, code)
- self.assertEqual({
- 'token': 'token',
- 'path': '/socket/path'
- }, answer)
- addr = self.__get_token_called[1]
- self.assertTrue(isinstance(addr, IPAddr))
- self.assertEqual("::", str(addr))
- self.assertEqual(("UDP", addr, 53, "ANY", "app"),
- self.__get_token_called)
-
- def test_get_socket_error(self):
- """
- Test that bad inputs are handled correctly, etc.
- """
- def check_code(code, args):
- """
- Pass the args there and check if it returns success or not.
-
- The rest is not tested, as it is already checked in the
- test_get_socket_ok.
- """
- [rcode, ranswer] = self.__b10_init._get_socket(args)['result']
- self.assertEqual(code, rcode)
- if code != 0:
- # This should be an error message. The exact formatting
- # is unknown, but we check it is string at least
- self.assertTrue(isinstance(ranswer, str))
-
- def mod_args(name, value):
- """
- Override a parameter in the args.
- """
- result = dict(self.__socket_args)
- result[name] = value
- return result
-
- # Port too large
- check_code(1, mod_args('port', 65536))
- # Not numeric address
- check_code(1, mod_args('address', 'example.org.'))
- # Some bad values of enum-like params
- check_code(1, mod_args('protocol', 'BAD PROTO'))
- check_code(1, mod_args('share_mode', 'BAD SHARE'))
- # Check missing parameters
- for param in self.__socket_args.keys():
- args = dict(self.__socket_args)
- del args[param]
- check_code(1, args)
- # These are OK values for the enum-like parameters
- # The ones from test_get_socket_ok are not tested here
- check_code(0, mod_args('protocol', 'TCP'))
- check_code(0, mod_args('share_mode', 'SAMEAPP'))
- check_code(0, mod_args('share_mode', 'NO'))
- # If an exception is raised from within the cache, it is converted
- # to an error, not propagated
- self.__raise_exception = Exception("Test exception")
- check_code(1, self.__socket_args)
- # The special "expected" exceptions
- self.__raise_exception = \
- isc.bind10.socket_cache.ShareError("Not shared")
- check_code(3, self.__socket_args)
- self.__raise_exception = \
- isc.bind10.socket_cache.SocketError("Not shared", 13)
- check_code(2, self.__socket_args)
-
- def drop_socket(self, token):
- """
- Part of pretending to be the cache. If there's anything in
- __raise_exception, it is raised. Otherwise, the parameter is stored
- in __drop_socket_called.
- """
- if self.__raise_exception is not None:
- raise self.__raise_exception
- self.__drop_socket_called = token
-
- def test_drop_socket(self):
- """
- Check the drop_socket command. It should directly call the method
- on the cache. Exceptions should be translated to error messages.
- """
- # This should be OK and just propagated to the call.
- self.assertEqual({"result": [0]},
- self.__b10_init.command_handler("drop_socket",
- {"token": "token"}))
- self.assertEqual("token", self.__drop_socket_called)
- self.__drop_socket_called = None
- # Missing parameter
- self.assertEqual({"result": [1, "Missing token parameter"]},
- self.__b10_init.command_handler("drop_socket", {}))
- self.assertIsNone(self.__drop_socket_called)
- # An exception is raised from within the cache
- self.__raise_exception = ValueError("Test error")
- self.assertEqual({"result": [1, "Test error"]},
- self.__b10_init.command_handler("drop_socket",
- {"token": "token"}))
-
-
-class TestInit(unittest.TestCase):
- def setUp(self):
- # Save original values that may be tweaked in some tests
- self.__orig_setgid = init.posix.setgid
- self.__orig_setuid = init.posix.setuid
- self.__orig_logger_class = isc.log.Logger
-
- def tearDown(self):
- # Restore original values saved in setUp()
- init.posix.setgid = self.__orig_setgid
- init.posix.setuid = self.__orig_setuid
- isc.log.Logger = self.__orig_logger_class
-
- def test_init(self):
- b10_init = Init()
- self.assertEqual(b10_init.verbose, False)
- self.assertEqual(b10_init.msgq_socket_file, None)
- self.assertEqual(b10_init.cc_session, None)
- self.assertEqual(b10_init.ccs, None)
- self.assertEqual(b10_init.components, {})
- self.assertEqual(b10_init.runnable, False)
- self.assertEqual(b10_init.username, None)
- self.assertIsNone(b10_init._socket_cache)
-
- def __setgid(self, gid):
- self.__gid_set = gid
-
- def __setuid(self, uid):
- self.__uid_set = uid
-
- def test_change_user(self):
- init.posix.setgid = self.__setgid
- init.posix.setuid = self.__setuid
-
- self.__gid_set = None
- self.__uid_set = None
- b10_init = Init()
- b10_init.change_user()
- # No gid/uid set in init, nothing called.
- self.assertIsNone(self.__gid_set)
- self.assertIsNone(self.__uid_set)
-
- Init(setuid=42, setgid=4200).change_user()
- # This time, it get's called
- self.assertEqual(4200, self.__gid_set)
- self.assertEqual(42, self.__uid_set)
-
- def raising_set_xid(gid_or_uid):
- ex = OSError()
- ex.errno, ex.strerror = errno.EPERM, 'Operation not permitted'
- raise ex
-
- # Let setgid raise an exception
- init.posix.setgid = raising_set_xid
- init.posix.setuid = self.__setuid
- self.assertRaises(init.ChangeUserError,
- Init(setuid=42, setgid=4200).change_user)
-
- # Let setuid raise an exception
- init.posix.setgid = self.__setgid
- init.posix.setuid = raising_set_xid
- self.assertRaises(init.ChangeUserError,
- Init(setuid=42, setgid=4200).change_user)
-
- # Let initial log output after setuid raise an exception
- init.posix.setgid = self.__setgid
- init.posix.setuid = self.__setuid
- isc.log.Logger = raising_set_xid
- self.assertRaises(init.ChangeUserError,
- Init(setuid=42, setgid=4200).change_user)
-
- def test_set_creator(self):
- """
- Test the call to set_creator. First time, the cache is created
- with the passed creator. The next time, it throws an exception.
- """
- init = Init()
- # The cache doesn't use it at start, so just create an empty class
- class Creator: pass
- creator = Creator()
- init.set_creator(creator)
- self.assertTrue(isinstance(init._socket_cache,
- isc.bind10.socket_cache.Cache))
- self.assertEqual(creator, init._socket_cache._creator)
- self.assertRaises(ValueError, init.set_creator, creator)
-
- def test_socket_srv(self):
- """Tests init_socket_srv() and remove_socket_srv() work as expected."""
- init = Init()
-
- self.assertIsNone(init._srv_socket)
- self.assertIsNone(init._tmpdir)
- self.assertIsNone(init._socket_path)
-
- init.init_socket_srv()
-
- self.assertIsNotNone(init._srv_socket)
- self.assertNotEqual(-1, init._srv_socket.fileno())
- self.assertEqual(os.path.join(init._tmpdir, 'sockcreator'),
- init._srv_socket.getsockname())
-
- self.assertIsNotNone(init._tmpdir)
- self.assertTrue(os.path.isdir(init._tmpdir))
- self.assertIsNotNone(init._socket_path)
- self.assertTrue(os.path.exists(init._socket_path))
-
- # Check that it's possible to connect to the socket file (this
- # only works if the socket file exists and the server listens on
- # it).
- s = socket.socket(socket.AF_UNIX)
- try:
- s.connect(init._socket_path)
- can_connect = True
- s.close()
- except socket.error as e:
- can_connect = False
-
- self.assertTrue(can_connect)
-
- init.remove_socket_srv()
-
- self.assertEqual(-1, init._srv_socket.fileno())
- self.assertFalse(os.path.exists(init._socket_path))
- self.assertFalse(os.path.isdir(init._tmpdir))
-
- # These should not fail either:
-
- # second call
- init.remove_socket_srv()
-
- init._srv_socket = None
- init.remove_socket_srv()
-
- def test_init_alternate_socket(self):
- init = Init("alt_socket_file")
- self.assertEqual(init.verbose, False)
- self.assertEqual(init.msgq_socket_file, "alt_socket_file")
- self.assertEqual(init.cc_session, None)
- self.assertEqual(init.ccs, None)
- self.assertEqual(init.components, {})
- self.assertEqual(init.runnable, False)
- self.assertEqual(init.username, None)
-
- def test_command_handler(self):
- class DummySession():
- def group_sendmsg(self, msg, group):
- (self.msg, self.group) = (msg, group)
- def group_recvmsg(self, nonblock, seq): pass
- class DummyModuleCCSession():
- module_spec = isc.config.module_spec.ModuleSpec({
- "module_name": "Init",
- "statistics": [
- {
- "item_name": "boot_time",
- "item_type": "string",
- "item_optional": False,
- "item_default": "1970-01-01T00:00:00Z",
- "item_title": "Boot time",
- "item_description": "A date time when bind10 process starts initially",
- "item_format": "date-time"
- }
- ]
- })
- def get_module_spec(self):
- return self.module_spec
- init = Init()
- init.verbose = True
- init.cc_session = DummySession()
- init.ccs = DummyModuleCCSession()
- # a bad command
- self.assertEqual(init.command_handler(-1, None),
- isc.config.ccsession.create_answer(1, "bad command"))
- # "shutdown" command
- self.assertEqual(init.command_handler("shutdown", None),
- isc.config.ccsession.create_answer(0))
- self.assertFalse(init.runnable)
- # "getstats" command
- self.assertEqual(init.command_handler("getstats", None),
- isc.config.ccsession.create_answer(0,
- { 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', _BASETIME) }))
- # "ping" command
- self.assertEqual(init.command_handler("ping", None),
- isc.config.ccsession.create_answer(0, "pong"))
- # "show_processes" command
- self.assertEqual(init.command_handler("show_processes", None),
- isc.config.ccsession.create_answer(0,
- init.get_processes()))
- # an unknown command
- self.assertEqual(init.command_handler("__UNKNOWN__", None),
- isc.config.ccsession.create_answer(1, "Unknown command"))
-
- # Fake the get_token of cache and test the command works
- init._socket_path = '/socket/path'
- class cache:
- def get_token(self, protocol, addr, port, share_mode, share_name):
- return str(addr) + ':' + str(port)
- init._socket_cache = cache()
- args = {
- "port": 53,
- "address": "0.0.0.0",
- "protocol": "UDP",
- "share_mode": "ANY",
- "share_name": "app"
- }
- # at all and this is the easiest way to check.
- self.assertEqual({'result': [0, {'token': '0.0.0.0:53',
- 'path': '/socket/path'}]},
- init.command_handler("get_socket", args))
- # The drop_socket is not tested here, but in TestCacheCommands.
- # It needs the cache mocks to be in place and they are there.
-
- def test_stop_process(self):
- """
- Test checking the stop_process method sends the right message over
- the message bus.
- """
- class DummySession():
- def group_sendmsg(self, msg, group, instance="*"):
- (self.msg, self.group, self.instance) = (msg, group, instance)
- init = Init()
- init.cc_session = DummySession()
- init.stop_process('process', 'address', 42)
- self.assertEqual('address', init.cc_session.group)
- self.assertEqual('address', init.cc_session.instance)
- self.assertEqual({'command': ['shutdown', {'pid': 42}]},
- init.cc_session.msg)
-
-# Mock class for testing Init's usage of ProcessInfo
-class MockProcessInfo:
- def __init__(self, name, args, env={}, dev_null_stdout=False,
- dev_null_stderr=False):
- self.name = name
- self.args = args
- self.env = env
- self.dev_null_stdout = dev_null_stdout
- self.dev_null_stderr = dev_null_stderr
- self.process = None
- self.pid = None
-
- def spawn(self):
- # set some pid (only used for testing that it is not None anymore)
- self.pid = 42147
-
-# Class for testing the Init without actually starting processes.
-# This is used for testing the start/stop components routines and
-# the Init commands.
-#
-# Testing that external processes start is outside the scope
-# of the unit test, by overriding the process start methods we can check
-# that the right processes are started depending on the configuration
-# options.
-class MockInit(Init):
- def __init__(self):
- Init.__init__(self)
-
- # Set flags as to which of the overridden methods has been run.
- self.msgq = False
- self.cfgmgr = False
- self.ccsession = False
- self.auth = False
- self.resolver = False
- self.xfrout = False
- self.xfrin = False
- self.zonemgr = False
- self.stats = False
- self.stats_httpd = False
- self.cmdctl = False
- self.dhcp6 = False
- self.dhcp4 = False
- self.c_channel_env = {}
- self.components = { }
- self.creator = False
- self.get_process_exit_status_called = False
-
- class MockSockCreator(isc.bind10.component.Component):
- def __init__(self, process, b10_init, kind, address=None,
- params=None):
- isc.bind10.component.Component.__init__(self, process,
- b10_init, kind,
- 'SockCreator')
- self._start_func = b10_init.start_creator
-
- specials = isc.bind10.special_component.get_specials()
- specials['sockcreator'] = MockSockCreator
- self._component_configurator = \
- isc.bind10.component.Configurator(self, specials)
-
- def start_creator(self):
- self.creator = True
- procinfo = ProcessInfo('b10-sockcreator', ['/bin/false'])
- procinfo.pid = 1
- return procinfo
-
- def _read_bind10_config(self):
- # Configuration options are set directly
- pass
-
- def start_msgq(self):
- self.msgq = True
- procinfo = ProcessInfo('b10-msgq', ['/bin/false'])
- procinfo.pid = 2
- return procinfo
-
- def start_ccsession(self, c_channel_env):
- # this is not a process, don't have to do anything with procinfo
- self.ccsession = True
-
- def start_cfgmgr(self):
- self.cfgmgr = True
- procinfo = ProcessInfo('b10-cfgmgr', ['/bin/false'])
- procinfo.pid = 3
- return procinfo
-
- def start_auth(self):
- self.auth = True
- procinfo = ProcessInfo('b10-auth', ['/bin/false'])
- procinfo.pid = 5
- return procinfo
-
- def start_resolver(self):
- self.resolver = True
- procinfo = ProcessInfo('b10-resolver', ['/bin/false'])
- procinfo.pid = 6
- return procinfo
-
- def start_simple(self, name):
- procmap = { 'b10-zonemgr': self.start_zonemgr,
- 'b10-stats': self.start_stats,
- 'b10-stats-httpd': self.start_stats_httpd,
- 'b10-cmdctl': self.start_cmdctl,
- 'b10-dhcp6': self.start_dhcp6,
- 'b10-dhcp4': self.start_dhcp4,
- 'b10-xfrin': self.start_xfrin,
- 'b10-xfrout': self.start_xfrout }
- return procmap[name]()
-
- def start_xfrout(self):
- self.xfrout = True
- procinfo = ProcessInfo('b10-xfrout', ['/bin/false'])
- procinfo.pid = 7
- return procinfo
-
- def start_xfrin(self):
- self.xfrin = True
- procinfo = ProcessInfo('b10-xfrin', ['/bin/false'])
- procinfo.pid = 8
- return procinfo
-
- def start_zonemgr(self):
- self.zonemgr = True
- procinfo = ProcessInfo('b10-zonemgr', ['/bin/false'])
- procinfo.pid = 9
- return procinfo
-
- def start_stats(self):
- self.stats = True
- procinfo = ProcessInfo('b10-stats', ['/bin/false'])
- procinfo.pid = 10
- return procinfo
-
- def start_stats_httpd(self):
- self.stats_httpd = True
- procinfo = ProcessInfo('b10-stats-httpd', ['/bin/false'])
- procinfo.pid = 11
- return procinfo
-
- def start_cmdctl(self):
- self.cmdctl = True
- procinfo = ProcessInfo('b10-cmdctl', ['/bin/false'])
- procinfo.pid = 12
- return procinfo
-
- def start_dhcp6(self):
- self.dhcp6 = True
- procinfo = ProcessInfo('b10-dhcp6', ['/bin/false'])
- procinfo.pid = 13
- return procinfo
-
- def start_dhcp4(self):
- self.dhcp4 = True
- procinfo = ProcessInfo('b10-dhcp4', ['/bin/false'])
- procinfo.pid = 14
- return procinfo
-
- def stop_process(self, process, recipient, pid):
- procmap = { 'b10-auth': self.stop_auth,
- 'b10-resolver': self.stop_resolver,
- 'b10-xfrout': self.stop_xfrout,
- 'b10-xfrin': self.stop_xfrin,
- 'b10-zonemgr': self.stop_zonemgr,
- 'b10-stats': self.stop_stats,
- 'b10-stats-httpd': self.stop_stats_httpd,
- 'b10-cmdctl': self.stop_cmdctl }
- procmap[process]()
-
- # Some functions to pretend we stop processes, use by stop_process
- def stop_msgq(self):
- if self.msgq:
- del self.components[2]
- self.msgq = False
-
- def stop_cfgmgr(self):
- if self.cfgmgr:
- del self.components[3]
- self.cfgmgr = False
-
- def stop_auth(self):
- if self.auth:
- del self.components[5]
- self.auth = False
-
- def stop_resolver(self):
- if self.resolver:
- del self.components[6]
- self.resolver = False
-
- def stop_xfrout(self):
- if self.xfrout:
- del self.components[7]
- self.xfrout = False
-
- def stop_xfrin(self):
- if self.xfrin:
- del self.components[8]
- self.xfrin = False
-
- def stop_zonemgr(self):
- if self.zonemgr:
- del self.components[9]
- self.zonemgr = False
-
- def stop_stats(self):
- if self.stats:
- del self.components[10]
- self.stats = False
-
- def stop_stats_httpd(self):
- if self.stats_httpd:
- del self.components[11]
- self.stats_httpd = False
-
- def stop_cmdctl(self):
- if self.cmdctl:
- del self.components[12]
- self.cmdctl = False
-
- def _get_process_exit_status(self):
- if self.get_process_exit_status_called:
- return (0, 0)
- self.get_process_exit_status_called = True
- return (53, 0)
-
- def _get_process_exit_status_unknown_pid(self):
- if self.get_process_exit_status_called:
- return (0, 0)
- self.get_process_exit_status_called = True
- return (42, 0)
-
- def _get_process_exit_status_raises_oserror_echild(self):
- raise OSError(errno.ECHILD, 'Mock error')
-
- def _get_process_exit_status_raises_oserror_other(self):
- raise OSError(0, 'Mock error')
-
- def _get_process_exit_status_raises_other(self):
- raise Exception('Mock error')
-
- def _make_mock_process_info(self, name, args, c_channel_env,
- dev_null_stdout=False, dev_null_stderr=False):
- return MockProcessInfo(name, args, c_channel_env,
- dev_null_stdout, dev_null_stderr)
-
-class MockInitSimple(Init):
- def __init__(self):
- Init.__init__(self)
- # Set which process has been started
- self.started_process_name = None
- self.started_process_args = None
- self.started_process_env = None
-
- def _make_mock_process_info(self, name, args, c_channel_env,
- dev_null_stdout=False, dev_null_stderr=False):
- return MockProcessInfo(name, args, c_channel_env,
- dev_null_stdout, dev_null_stderr)
-
- def start_process(self, name, args, c_channel_env, port=None,
- address=None):
- self.started_process_name = name
- self.started_process_args = args
- self.started_process_env = c_channel_env
- return None
-
-class TestStartStopProcessesInit(unittest.TestCase):
- """
- Check that the start_all_components method starts the right combination
- of components and that the right components are started and stopped
- according to changes in configuration.
- """
- def check_environment_unchanged(self):
- # Check whether the environment has not been changed
- self.assertEqual(original_os_environ, os.environ)
-
- def check_started(self, init, core, auth, resolver):
- """
- Check that the right sets of services are started. The ones that
- should be running are specified by the core, auth and resolver parameters
- (they are groups of processes, eg. auth means b10-auth, -xfrout, -xfrin
- and -zonemgr).
- """
- self.assertEqual(init.msgq, core)
- self.assertEqual(init.cfgmgr, core)
- self.assertEqual(init.ccsession, core)
- self.assertEqual(init.creator, core)
- self.assertEqual(init.auth, auth)
- self.assertEqual(init.resolver, resolver)
- self.assertEqual(init.xfrout, auth)
- self.assertEqual(init.xfrin, auth)
- self.assertEqual(init.zonemgr, auth)
- self.assertEqual(init.stats, core)
- self.assertEqual(init.stats_httpd, core)
- self.assertEqual(init.cmdctl, core)
- self.check_environment_unchanged()
-
- def check_preconditions(self, init):
- self.check_started(init, False, False, False)
-
- def check_started_none(self, init):
- """
- Check that the situation is according to configuration where no servers
- should be started. Some components still need to be running.
- """
- self.check_started(init, True, False, False)
- self.check_environment_unchanged()
-
- def check_started_both(self, init):
- """
- Check the situation is according to configuration where both servers
- (auth and resolver) are enabled.
- """
- self.check_started(init, True, True, True)
- self.check_environment_unchanged()
-
- def check_started_auth(self, init):
- """
- Check the set of components needed to run auth only is started.
- """
- self.check_started(init, True, True, False)
- self.check_environment_unchanged()
-
- def check_started_resolver(self, init):
- """
- Check the set of components needed to run resolver only is started.
- """
- self.check_started(init, True, False, True)
- self.check_environment_unchanged()
-
- def check_started_dhcp(self, init, v4, v6):
- """
- Check if proper combinations of DHCPv4 and DHCpv6 can be started
- """
- self.assertEqual(v4, init.dhcp4)
- self.assertEqual(v6, init.dhcp6)
- self.check_environment_unchanged()
-
- def construct_config(self, start_auth, start_resolver):
- # The things that are common, not turned on an off
- config = {}
- config['b10-stats'] = { 'kind': 'dispensable', 'address': 'Stats' }
- config['b10-stats-httpd'] = { 'kind': 'dispensable',
- 'address': 'StatsHttpd' }
- config['b10-cmdctl'] = { 'kind': 'needed', 'special': 'cmdctl' }
- if start_auth:
- config['b10-auth'] = { 'kind': 'needed', 'special': 'auth' }
- config['b10-xfrout'] = { 'kind': 'dispensable',
- 'address': 'Xfrout' }
- config['b10-xfrin'] = { 'kind': 'dispensable',
- 'address': 'Xfrin' }
- config['b10-zonemgr'] = { 'kind': 'dispensable',
- 'address': 'Zonemgr' }
- if start_resolver:
- config['b10-resolver'] = { 'kind': 'needed',
- 'special': 'resolver' }
- return {'components': config}
-
- def config_start_init(self, start_auth, start_resolver):
- """
- Test the configuration is loaded at the startup.
- """
- init = MockInit()
- config = self.construct_config(start_auth, start_resolver)
- class CC:
- def get_full_config(self):
- return config
- # Provide the fake CC with data
- init.ccs = CC()
- # And make sure it's not overwritten
- def start_ccsession():
- init.ccsession = True
- init.start_ccsession = lambda _: start_ccsession()
- # We need to return the original _read_bind10_config
- init._read_bind10_config = lambda: Init._read_bind10_config(init)
- init.wait_msgq = lambda: None
- init.start_all_components()
- self.check_started(init, True, start_auth, start_resolver)
- self.check_environment_unchanged()
-
- def test_start_none(self):
- self.config_start_init(False, False)
-
- def test_start_resolver(self):
- self.config_start_init(False, True)
-
- def test_start_auth(self):
- self.config_start_init(True, False)
-
- def test_start_both(self):
- self.config_start_init(True, True)
-
- def test_config_start(self):
- """
- Test that the configuration starts and stops components according
- to configuration changes.
- """
-
- # Create Init and ensure correct initialization
- init = MockInit()
- self.check_preconditions(init)
-
- init.wait_msgq = lambda: None
- init.start_all_components()
- init.runnable = True
- init.config_handler(self.construct_config(False, False))
- self.check_started_none(init)
-
- # Enable both at once
- init.config_handler(self.construct_config(True, True))
- self.check_started_both(init)
-
- # Not touched by empty change
- init.config_handler({})
- self.check_started_both(init)
-
- # Not touched by change to the same configuration
- init.config_handler(self.construct_config(True, True))
- self.check_started_both(init)
-
- # Turn them both off again
- init.config_handler(self.construct_config(False, False))
- self.check_started_none(init)
-
- # Not touched by empty change
- init.config_handler({})
- self.check_started_none(init)
-
- # Not touched by change to the same configuration
- init.config_handler(self.construct_config(False, False))
- self.check_started_none(init)
-
- # Start and stop auth separately
- init.config_handler(self.construct_config(True, False))
- self.check_started_auth(init)
-
- init.config_handler(self.construct_config(False, False))
- self.check_started_none(init)
-
- # Start and stop resolver separately
- init.config_handler(self.construct_config(False, True))
- self.check_started_resolver(init)
-
- init.config_handler(self.construct_config(False, False))
- self.check_started_none(init)
-
- # Alternate
- init.config_handler(self.construct_config(True, False))
- self.check_started_auth(init)
-
- init.config_handler(self.construct_config(False, True))
- self.check_started_resolver(init)
-
- init.config_handler(self.construct_config(True, False))
- self.check_started_auth(init)
-
- def test_config_start_once(self):
- """
- Tests that a component is started only once.
- """
- # Create Init and ensure correct initialization
- init = MockInit()
- self.check_preconditions(init)
-
- init.wait_msgq = lambda: None
- init.start_all_components()
-
- init.runnable = True
- init.config_handler(self.construct_config(True, True))
- self.check_started_both(init)
-
- init.start_auth = lambda: self.fail("Started auth again")
- init.start_xfrout = lambda: self.fail("Started xfrout again")
- init.start_xfrin = lambda: self.fail("Started xfrin again")
- init.start_zonemgr = lambda: self.fail("Started zonemgr again")
- init.start_resolver = lambda: self.fail("Started resolver again")
-
- # Send again we want to start them. Should not do it, as they are.
- init.config_handler(self.construct_config(True, True))
-
- def test_config_not_started_early(self):
- """
- Test that components are not started by the config handler before
- startup.
- """
- init = MockInit()
- self.check_preconditions(init)
-
- init.start_auth = lambda: self.fail("Started auth again")
- init.start_xfrout = lambda: self.fail("Started xfrout again")
- init.start_xfrin = lambda: self.fail("Started xfrin again")
- init.start_zonemgr = lambda: self.fail("Started zonemgr again")
- init.start_resolver = lambda: self.fail("Started resolver again")
-
- init.config_handler({'start_auth': True, 'start_resolver': True})
-
- # Checks that DHCP (v4 and v6) components are started when expected
- def test_start_dhcp(self):
-
- # Create Init and ensure correct initialization
- init = MockInit()
- self.check_preconditions(init)
-
- init.wait_msgq = lambda: None
- init.start_all_components()
- init.config_handler(self.construct_config(False, False))
- self.check_started_dhcp(init, False, False)
-
- def test_start_dhcp_v6only(self):
- # Create Init and ensure correct initialization
- init = MockInit()
- self.check_preconditions(init)
- # v6 only enabled
- init.wait_msgq = lambda: None
- init.start_all_components()
- init.runnable = True
- init._Init_started = True
- config = self.construct_config(False, False)
- config['components']['b10-dhcp6'] = { 'kind': 'needed',
- 'address': 'Dhcp6' }
- init.config_handler(config)
- self.check_started_dhcp(init, False, True)
-
- # uncomment when dhcpv4 becomes implemented
- # v4 only enabled
- #init.cfg_start_dhcp6 = False
- #init.cfg_start_dhcp4 = True
- #self.check_started_dhcp(init, True, False)
-
- # both v4 and v6 enabled
- #init.cfg_start_dhcp6 = True
- #init.cfg_start_dhcp4 = True
- #self.check_started_dhcp(init, True, True)
-
-class MockComponent:
- def __init__(self, name, pid, address=None):
- self.name = lambda: name
- self.pid = lambda: pid
- self.address = lambda: address
- self.restarted = False
- self.forceful = False
- self.running = True
- self.has_failed = False
-
- def get_restart_time(self):
- return 0 # arbitrary dummy value
-
- def restart(self, now):
- self.restarted = True
- return True
-
- def is_running(self):
- return self.running
-
- def failed(self, status):
- return self.has_failed
-
- def kill(self, forceful):
- self.forceful = forceful
-
-class TestInitCmd(unittest.TestCase):
- def test_ping(self):
- """
- Confirm simple ping command works.
- """
- init = MockInit()
- answer = init.command_handler("ping", None)
- self.assertEqual(answer, {'result': [0, 'pong']})
-
- def test_show_processes_empty(self):
- """
- Confirm getting a list of processes works.
- """
- init = MockInit()
- answer = init.command_handler("show_processes", None)
- self.assertEqual(answer, {'result': [0, []]})
-
- def test_show_processes(self):
- """
- Confirm getting a list of processes works.
- """
- init = MockInit()
- init.register_process(1, MockComponent('first', 1))
- init.register_process(2, MockComponent('second', 2, 'Second'))
- answer = init.command_handler("show_processes", None)
- processes = [[1, 'first', None],
- [2, 'second', 'Second']]
- self.assertEqual(answer, {'result': [0, processes]})
-
-class TestParseArgs(unittest.TestCase):
- """
- This tests parsing of arguments of the bind10 master process.
- """
- #TODO: Write tests for the original parsing, bad options, etc.
- def test_no_opts(self):
- """
- Test correct default values when no options are passed.
- """
- options = parse_args([], TestOptParser)
- self.assertEqual(None, options.data_path)
- self.assertEqual(None, options.config_file)
- self.assertEqual(None, options.cmdctl_port)
-
- def test_data_path(self):
- """
- Test it can parse the data path.
- """
- self.assertRaises(OptsError, parse_args, ['-p'], TestOptParser)
- self.assertRaises(OptsError, parse_args, ['--data-path'],
- TestOptParser)
- options = parse_args(['-p', '/data/path'], TestOptParser)
- self.assertEqual('/data/path', options.data_path)
- options = parse_args(['--data-path=/data/path'], TestOptParser)
- self.assertEqual('/data/path', options.data_path)
-
- def test_config_filename(self):
- """
- Test it can parse the config switch.
- """
- self.assertRaises(OptsError, parse_args, ['-c'], TestOptParser)
- self.assertRaises(OptsError, parse_args, ['--config-file'],
- TestOptParser)
- options = parse_args(['-c', 'config-file'], TestOptParser)
- self.assertEqual('config-file', options.config_file)
- options = parse_args(['--config-file=config-file'], TestOptParser)
- self.assertEqual('config-file', options.config_file)
-
- def test_clear_config(self):
- options = parse_args([], TestOptParser)
- self.assertEqual(False, options.clear_config)
- options = parse_args(['--clear-config'], TestOptParser)
- self.assertEqual(True, options.clear_config)
-
- def test_nokill(self):
- options = parse_args([], TestOptParser)
- self.assertEqual(False, options.nokill)
- options = parse_args(['--no-kill'], TestOptParser)
- self.assertEqual(True, options.nokill)
- options = parse_args([], TestOptParser)
- self.assertEqual(False, options.nokill)
- options = parse_args(['-i'], TestOptParser)
- self.assertEqual(True, options.nokill)
-
- def test_cmdctl_port(self):
- """
- Test it can parse the command control port.
- """
- self.assertRaises(OptsError, parse_args, ['--cmdctl-port=abc'],
- TestOptParser)
- self.assertRaises(OptsError, parse_args, ['--cmdctl-port=100000000'],
- TestOptParser)
- self.assertRaises(OptsError, parse_args, ['--cmdctl-port'],
- TestOptParser)
- options = parse_args(['--cmdctl-port=1234'], TestOptParser)
- self.assertEqual(1234, options.cmdctl_port)
-
-class TestPIDFile(unittest.TestCase):
- def setUp(self):
- self.pid_file = '@builddir@' + os.sep + 'bind10.pid'
- if os.path.exists(self.pid_file):
- os.unlink(self.pid_file)
-
- def tearDown(self):
- if os.path.exists(self.pid_file):
- os.unlink(self.pid_file)
-
- def check_pid_file(self):
- # dump PID to the file, and confirm the content is correct
- dump_pid(self.pid_file)
- my_pid = os.getpid()
- with open(self.pid_file, "r") as f:
- self.assertEqual(my_pid, int(f.read()))
-
- def test_dump_pid(self):
- self.check_pid_file()
-
- # make sure any existing content will be removed
- with open(self.pid_file, "w") as f:
- f.write('dummy data\n')
- self.check_pid_file()
-
- def test_unlink_pid_file_notexist(self):
- dummy_data = 'dummy_data\n'
-
- with open(self.pid_file, "w") as f:
- f.write(dummy_data)
-
- unlink_pid_file("no_such_pid_file")
-
- # the file specified for unlink_pid_file doesn't exist,
- # and the original content of the file should be intact.
- with open(self.pid_file, "r") as f:
- self.assertEqual(dummy_data, f.read())
-
- def test_dump_pid_with_none(self):
- # Check the behavior of dump_pid() and unlink_pid_file() with None.
- # This should be no-op.
- dump_pid(None)
- self.assertFalse(os.path.exists(self.pid_file))
-
- dummy_data = 'dummy_data\n'
-
- with open(self.pid_file, "w") as f:
- f.write(dummy_data)
-
- unlink_pid_file(None)
-
- with open(self.pid_file, "r") as f:
- self.assertEqual(dummy_data, f.read())
-
- def test_dump_pid_failure(self):
- # the attempt to open file will fail, which should result in exception.
- self.assertRaises(IOError, dump_pid,
- 'nonexistent_dir' + os.sep + 'bind10.pid')
-
-class TestInitComponents(unittest.TestCase):
- """
- Test b10-init propagates component configuration properly to the
- component configurator and acts sane.
- """
- def setUp(self):
- self.__param = None
- self.__called = False
- self.__compconfig = {
- 'comp': {
- 'kind': 'needed',
- 'process': 'cat'
- }
- }
- self._tmp_time = None
- self._tmp_sleep = None
- self._tmp_module_cc_session = None
- self._tmp_cc_session = None
-
- def tearDown(self):
- if self._tmp_time is not None:
- time.time = self._tmp_time
- if self._tmp_sleep is not None:
- time.sleep = self._tmp_sleep
- if self._tmp_module_cc_session is not None:
- isc.config.ModuleCCSession = self._tmp_module_cc_session
- if self._tmp_cc_session is not None:
- isc.cc.Session = self._tmp_cc_session
-
- def __unary_hook(self, param):
- """
- A hook function that stores the parameter for later examination.
- """
- self.__param = param
-
- def __nullary_hook(self):
- """
- A hook function that notes down it was called.
- """
- self.__called = True
-
- def __check_core(self, config):
- """
- A function checking that the config contains parts for the valid
- core component configuration.
- """
- self.assertIsNotNone(config)
- for component in ['sockcreator', 'msgq', 'cfgmgr']:
- self.assertTrue(component in config)
- self.assertEqual(component, config[component]['special'])
- self.assertEqual('core', config[component]['kind'])
-
- def __check_extended(self, config):
- """
- This checks that the config contains the core and one more component.
- """
- self.__check_core(config)
- self.assertTrue('comp' in config)
- self.assertEqual('cat', config['comp']['process'])
- self.assertEqual('needed', config['comp']['kind'])
- self.assertEqual(4, len(config))
-
- def test_correct_run(self):
- """
- Test the situation when we run in usual scenario, nothing fails,
- we just start, reconfigure and then stop peacefully.
- """
- init = MockInit()
- # Start it
- orig = init._component_configurator.startup
- init._component_configurator.startup = self.__unary_hook
- init.wait_msgq = lambda: None
- init.start_all_components()
- init._component_configurator.startup = orig
- self.__check_core(self.__param)
- self.assertEqual(3, len(self.__param))
-
- # Reconfigure it
- self.__param = None
- orig = init._component_configurator.reconfigure
- init._component_configurator.reconfigure = self.__unary_hook
- # Otherwise it does not work
- init.runnable = True
- init.config_handler({'components': self.__compconfig})
- self.__check_extended(self.__param)
- currconfig = self.__param
- # If we reconfigure it, but it does not contain the components part,
- # nothing is called
- init.config_handler({})
- self.assertEqual(self.__param, currconfig)
- self.__param = None
- init._component_configurator.reconfigure = orig
- # Check a configuration that messes up the core components is rejected.
- compconf = dict(self.__compconfig)
- compconf['msgq'] = { 'process': 'echo' }
- result = init.config_handler({'components': compconf})
- # Check it rejected it
- self.assertEqual(1, result['result'][0])
-
- # We can't call shutdown, that one relies on the stuff in main
- # We check somewhere else that the shutdown is actually called
- # from there (the test_kills).
-
- def __real_test_kill(self, nokill=False, ex_on_kill=None):
- """
- Helper function that does the actual kill functionality testing.
- """
- init = MockInit()
- init.nokill = nokill
-
- killed = []
- class ImmortalComponent:
- """
- An immortal component. It does not stop when it is told so
- (anyway it is not told so). It does not die if it is killed
- the first time. It dies only when killed forcefully.
- """
- def __init__(self):
- # number of kill() calls, preventing infinite loop.
- self.__call_count = 0
-
- def kill(self, forceful=False):
- self.__call_count += 1
- if self.__call_count > 2:
- raise Exception('Too many calls to ImmortalComponent.kill')
-
- killed.append(forceful)
- if ex_on_kill is not None:
- # If exception is given by the test, raise it here.
- # In the case of ESRCH, the process should have gone
- # somehow, so we clear the components.
- if ex_on_kill.errno == errno.ESRCH:
- init.components = {}
- raise ex_on_kill
- if forceful:
- init.components = {}
- def pid(self):
- return 1
- def name(self):
- return "Immortal"
- init.components = {}
- init.register_process(1, ImmortalComponent())
-
- # While at it, we check the configurator shutdown is actually called
- orig = init._component_configurator.shutdown
- init._component_configurator.shutdown = self.__nullary_hook
- self.__called = False
-
- init.ccs = MockModuleCCSession()
- self.assertFalse(init.ccs.stopped)
-
- init.shutdown()
-
- self.assertTrue(init.ccs.stopped)
-
- # Here, killed is an array where False is added if SIGTERM
- # should be sent, or True if SIGKILL should be sent, in order in
- # which they're sent.
- if nokill:
- self.assertEqual([], killed)
- else:
- if ex_on_kill is not None:
- self.assertEqual([False], killed)
- else:
- self.assertEqual([False, True], killed)
-
- self.assertTrue(self.__called)
-
- init._component_configurator.shutdown = orig
-
- def test_kills(self):
- """
- Test that b10-init kills components which don't want to stop.
- """
- self.__real_test_kill()
-
- def test_kill_fail(self):
- """Test cases where kill() results in an exception due to OS error.
-
- The behavior should be different for EPERM, so we test two cases.
-
- """
-
- ex = OSError()
- ex.errno, ex.strerror = errno.ESRCH, 'No such process'
- self.__real_test_kill(ex_on_kill=ex)
-
- ex.errno, ex.strerror = errno.EPERM, 'Operation not permitted'
- self.__real_test_kill(ex_on_kill=ex)
-
- def test_nokill(self):
- """
- Test that b10-init *doesn't* kill components which don't want to
- stop, when asked not to (by passing the --no-kill option which
- sets init.nokill to True).
- """
- self.__real_test_kill(True)
-
- def test_component_shutdown(self):
- """
- Test the component_shutdown sets all variables accordingly.
- """
- init = MockInit()
- self.assertRaises(Exception, init.component_shutdown, 1)
- self.assertEqual(1, init.exitcode)
- init._Init__started = True
- init.component_shutdown(2)
- self.assertEqual(2, init.exitcode)
- self.assertFalse(init.runnable)
-
- def test_init_config(self):
- """
- Test initial configuration is loaded.
- """
- init = MockInit()
- # Start it
- init._component_configurator.reconfigure = self.__unary_hook
- # We need to return the original read_bind10_config
- init._read_bind10_config = lambda: Init._read_bind10_config(init)
- # And provide a session to read the data from
- class CC:
- pass
- init.ccs = CC()
- init.ccs.get_full_config = lambda: {'components': self.__compconfig}
- init.wait_msgq = lambda: None
- init.start_all_components()
- self.__check_extended(self.__param)
-
- def __setup_restart(self, init, component):
- '''Common procedure for restarting a component used below.'''
- init.components_to_restart = { component }
- component.restarted = False
- init.restart_processes()
-
- def test_restart_processes(self):
- '''Check some behavior on restarting processes.'''
- init = MockInit()
- init.runnable = True
- component = MockComponent('test', 53)
-
- # A component to be restarted will actually be restarted iff it's
- # in the configurator's configuration.
- # We bruteforce the configurator internal below; ugly, but the easiest
- # way for the test.
- init._component_configurator._components['test'] = (None, component)
- self.__setup_restart(init, component)
- self.assertTrue(component.restarted)
- self.assertNotIn(component, init.components_to_restart)
-
- # Remove the component from the configuration. It won't be restarted
- # even if scheduled, nor will remain in the to-be-restarted list.
- del init._component_configurator._components['test']
- self.__setup_restart(init, component)
- self.assertFalse(component.restarted)
- self.assertNotIn(component, init.components_to_restart)
-
- def test_get_processes(self):
- '''Test that procsses are returned correctly, sorted by pid.'''
- init = MockInit()
-
- pids = list(range(0, 20))
- random.shuffle(pids)
-
- for i in range(0, 20):
- pid = pids[i]
- component = MockComponent('test' + str(pid), pid,
- 'Test' + str(pid))
- init.components[pid] = component
-
- process_list = init.get_processes()
- self.assertEqual(20, len(process_list))
-
- last_pid = -1
- for process in process_list:
- pid = process[0]
- self.assertLessEqual(last_pid, pid)
- last_pid = pid
- self.assertEqual([pid, 'test' + str(pid), 'Test' + str(pid)],
- process)
-
- def _test_reap_children_helper(self, runnable, is_running, failed):
- '''Construct a Init instance, set various data in it according to
- passed args and check if the component was added to the list of
- components to restart.'''
- init = MockInit()
- init.runnable = runnable
-
- component = MockComponent('test', 53)
- component.running = is_running
- component.has_failed = failed
- init.components[53] = component
-
- self.assertNotIn(component, init.components_to_restart)
-
- init.reap_children()
-
- if runnable and is_running and not failed:
- self.assertIn(component, init.components_to_restart)
- else:
- self.assertEqual([], init.components_to_restart)
-
- def test_reap_children(self):
- '''Test that children are queued to be restarted when they ask for it.'''
- # test various combinations of 3 booleans
- # (Init.runnable, component.is_running(), component.failed())
- self._test_reap_children_helper(False, False, False)
- self._test_reap_children_helper(False, False, True)
- self._test_reap_children_helper(False, True, False)
- self._test_reap_children_helper(False, True, True)
- self._test_reap_children_helper(True, False, False)
- self._test_reap_children_helper(True, False, True)
- self._test_reap_children_helper(True, True, False)
- self._test_reap_children_helper(True, True, True)
-
- # setup for more tests below
- init = MockInit()
- init.runnable = True
- component = MockComponent('test', 53)
- init.components[53] = component
-
- # case where the returned pid is unknown to us. nothing should
- # happen then.
- init.get_process_exit_status_called = False
- init._get_process_exit_status = init._get_process_exit_status_unknown_pid
- init.components_to_restart = []
- # this should do nothing as the pid is unknown
- init.reap_children()
- self.assertEqual([], init.components_to_restart)
-
- # case where init._get_process_exit_status() raises OSError with
- # errno.ECHILD
- init._get_process_exit_status = \
- init._get_process_exit_status_raises_oserror_echild
- init.components_to_restart = []
- # this should catch and handle the OSError
- init.reap_children()
- self.assertEqual([], init.components_to_restart)
-
- # case where init._get_process_exit_status() raises OSError with
- # errno other than ECHILD
- init._get_process_exit_status = \
- init._get_process_exit_status_raises_oserror_other
- with self.assertRaises(OSError):
- init.reap_children()
-
- # case where init._get_process_exit_status() raises something
- # other than OSError
- init._get_process_exit_status = \
- init._get_process_exit_status_raises_other
- with self.assertRaises(Exception):
- init.reap_children()
-
- def test_kill_started_components(self):
- '''Test that started components are killed.'''
- init = MockInit()
-
- component = MockComponent('test', 53, 'Test')
- init.components[53] = component
-
- self.assertEqual([[53, 'test', 'Test']], init.get_processes())
- init.kill_started_components()
- self.assertEqual([], init.get_processes())
- self.assertTrue(component.forceful)
-
- def _start_msgq_helper(self, init, verbose):
- init.verbose = verbose
- pi = init.start_msgq()
- self.assertEqual('b10-msgq', pi.name)
- self.assertEqual(['b10-msgq'], pi.args)
- self.assertEqual(pi.dev_null_stdout, not verbose)
- self.assertEqual(pi.dev_null_stderr, not verbose)
- self.assertEqual({'FOO': 'an env string'}, pi.env)
-
- # this is set by ProcessInfo.spawn()
- self.assertEqual(42147, pi.pid)
-
- def test_start_msgq(self):
- '''Test that b10-msgq is started.'''
- init = MockInitSimple()
- init.c_channel_env = {'FOO': 'an env string'}
- init._run_under_unittests = True
-
- # use the MockProcessInfo creator
- init._make_process_info = init._make_mock_process_info
-
- # non-verbose case
- self._start_msgq_helper(init, False)
-
- # verbose case
- self._start_msgq_helper(init, True)
-
- def test_start_msgq_timeout(self):
- '''Test that b10-msgq startup attempts connections several times
- and times out eventually.'''
- b10_init = MockInitSimple()
- b10_init.c_channel_env = {}
- # set the timeout to an arbitrary pre-determined value (which
- # code below depends on)
- b10_init.msgq_timeout = 1
- b10_init._run_under_unittests = False
-
- # use the MockProcessInfo creator
- b10_init._make_process_info = b10_init._make_mock_process_info
-
- global attempts
- global tsec
- attempts = 0
- tsec = 0
- self._tmp_time = time.time
- self._tmp_sleep = time.sleep
- def _my_time():
- global attempts
- global tsec
- attempts += 1
- return tsec
- def _my_sleep(nsec):
- global tsec
- tsec += nsec
- time.time = _my_time
- time.sleep = _my_sleep
-
- global cc_sub
- cc_sub = None
- class DummySessionAlwaysFails():
- def __init__(self, socket_file):
- raise isc.cc.session.SessionError('Connection fails')
- def group_subscribe(self, s):
- global cc_sub
- cc_sub = s
-
- isc.cc.Session = DummySessionAlwaysFails
-
- with self.assertRaises(init.CChannelConnectError):
- # An exception will be thrown here when it eventually times
- # out.
- pi = b10_init.start_msgq()
-
- # time.time() should be called 12 times within the while loop:
- # starting from 0, and 11 more times from 0.1 to 1.1. There's
- # another call to time.time() outside the loop, which makes it
- # 13.
- self.assertEqual(attempts, 13)
-
- # group_subscribe() should not have been called here.
- self.assertIsNone(cc_sub)
-
- global cc_socket_file
- cc_socket_file = None
- cc_sub = None
- class DummySession():
- def __init__(self, socket_file):
- global cc_socket_file
- cc_socket_file = socket_file
- def group_subscribe(self, s):
- global cc_sub
- cc_sub = s
-
- isc.cc.Session = DummySession
-
- # reset values
- attempts = 0
- tsec = 0
-
- pi = b10_init.start_msgq()
-
- # just one attempt, but 2 calls to time.time()
- self.assertEqual(attempts, 2)
-
- self.assertEqual(cc_socket_file, b10_init.msgq_socket_file)
- self.assertEqual(cc_sub, 'Init')
-
- # isc.cc.Session, time.time() and time.sleep() are restored
- # during tearDown().
-
- def _start_cfgmgr_helper(self, init, data_path, filename, clear_config):
- expect_args = ['b10-cfgmgr']
- if data_path is not None:
- init.data_path = data_path
- expect_args.append('--data-path=' + data_path)
- if filename is not None:
- init.config_filename = filename
- expect_args.append('--config-filename=' + filename)
- if clear_config:
- init.clear_config = clear_config
- expect_args.append('--clear-config')
-
- pi = init.start_cfgmgr()
- self.assertEqual('b10-cfgmgr', pi.name)
- self.assertEqual(expect_args, pi.args)
- self.assertEqual({'TESTENV': 'A test string'}, pi.env)
-
- # this is set by ProcessInfo.spawn()
- self.assertEqual(42147, pi.pid)
-
- def test_wait_msgq(self):
- """
- Test we can wait for msgq to provide its own alias.
-
- It is not available the first time, the second it is.
- """
- class RpcSession:
- def __init__(self):
- # Not yet called
- self.called = 0
-
- def rpc_call(self, command, recipient):
- self.called += 1
- if self.called == 1:
- raise isc.config.RPCRecipientMissing("Not yet")
- elif self.called == 2:
- raise isc.config.RPCError(1, "What?")
- else:
- raise Exception("Called too many times")
-
- init = MockInitSimple()
- init.wait_time = 1
- init.ccs = RpcSession()
- init.wait_msgq()
- self.assertEqual(2, init.ccs.called)
-
- def test_wait_msgq_fail(self):
- """
- Test the wait_msgq fails in case the msgq does not appear
- after so many attempts.
- """
- class RpcSession:
- def __init__(self):
- self.called = 0
-
- def rpc_call(self, command, recipient):
- self.called += 1
- raise isc.config.RPCRecipientMissing("Not yet")
-
- b10init = MockInitSimple()
- b10init.wait_time = 1
- b10init.ccs = RpcSession()
- self.assertRaises(init.ProcessStartError, b10init.wait_msgq)
- self.assertEqual(10, b10init.ccs.called)
-
- def test_start_cfgmgr(self):
- '''Test that b10-cfgmgr is started.'''
- class DummySession():
- def __init__(self):
- self._tries = 0
- def group_recvmsg(self):
- self._tries += 1
- # return running on the 3rd try onwards
- if self._tries >= 3:
- return ({'running': 'ConfigManager'}, None)
- else:
- return ({}, None)
-
- init = MockInitSimple()
- init.c_channel_env = {'TESTENV': 'A test string'}
- init.cc_session = DummySession()
- init.wait_time = 5
-
- # use the MockProcessInfo creator
- init._make_process_info = init._make_mock_process_info
-
- global attempts
- attempts = 0
- self._tmp_sleep = time.sleep
- def _my_sleep(nsec):
- global attempts
- attempts += 1
- time.sleep = _my_sleep
-
- # defaults
- self._start_cfgmgr_helper(init, None, None, False)
-
- # check that 2 attempts were made. on the 3rd attempt,
- # process_running() returns that ConfigManager is running.
- self.assertEqual(attempts, 2)
-
- # data_path is specified
- self._start_cfgmgr_helper(init, '/var/lib/test', None, False)
-
- # config_filename is specified. Because `init` is not
- # reconstructed, data_path is retained from the last call to
- # _start_cfgmgr_helper().
- self._start_cfgmgr_helper(init, '/var/lib/test', 'foo.cfg', False)
-
- # clear_config is specified. Because `init` is not reconstructed,
- # data_path and config_filename are retained from the last call
- # to _start_cfgmgr_helper().
- self._start_cfgmgr_helper(init, '/var/lib/test', 'foo.cfg', True)
-
- def test_start_cfgmgr_timeout(self):
- '''Test that b10-cfgmgr startup attempts connections several times
- and times out eventually.'''
- class DummySession():
- def group_recvmsg(self):
- return (None, None)
- b10_init = MockInitSimple()
- b10_init.c_channel_env = {}
- b10_init.cc_session = DummySession()
- # set wait_time to an arbitrary pre-determined value (which code
- # below depends on)
- b10_init.wait_time = 2
-
- # use the MockProcessInfo creator
- b10_init._make_process_info = b10_init._make_mock_process_info
-
- global attempts
- attempts = 0
- self._tmp_sleep = time.sleep
- def _my_sleep(nsec):
- global attempts
- attempts += 1
- time.sleep = _my_sleep
-
- # We just check that an exception was thrown, and that several
- # attempts were made to connect.
- with self.assertRaises(init.ProcessStartError):
- pi = b10_init.start_cfgmgr()
-
- # 2 seconds of attempts every 1 second should result in 2 attempts
- self.assertEqual(attempts, 2)
-
- # time.sleep() is restored during tearDown().
-
- def test_start_ccsession(self):
- '''Test that CC session is started.'''
- class DummySession():
- def __init__(self, specfile, config_handler, command_handler,
- socket_file):
- self.specfile = specfile
- self.config_handler = config_handler
- self.command_handler = command_handler
- self.socket_file = socket_file
- self.started = False
- def start(self):
- self.started = True
- b10_init = MockInitSimple()
- self._tmp_module_cc_session = isc.config.ModuleCCSession
- isc.config.ModuleCCSession = DummySession
-
- b10_init.start_ccsession({})
- self.assertEqual(init.SPECFILE_LOCATION, b10_init.ccs.specfile)
- self.assertEqual(b10_init.config_handler, b10_init.ccs.config_handler)
- self.assertEqual(b10_init.command_handler,
- b10_init.ccs.command_handler)
- self.assertEqual(b10_init.msgq_socket_file, b10_init.ccs.socket_file)
- self.assertTrue(b10_init.ccs.started)
-
- # isc.config.ModuleCCSession is restored during tearDown().
-
- def test_start_process(self):
- '''Test that processes can be started.'''
- init = MockInit()
-
- # use the MockProcessInfo creator
- init._make_process_info = init._make_mock_process_info
-
- pi = init.start_process('Test Process', ['/bin/true'], {})
- self.assertEqual('Test Process', pi.name)
- self.assertEqual(['/bin/true'], pi.args)
- self.assertEqual({}, pi.env)
-
- # this is set by ProcessInfo.spawn()
- self.assertEqual(42147, pi.pid)
-
- def test_register_process(self):
- '''Test that processes can be registered with Init.'''
- init = MockInit()
- component = MockComponent('test', 53, 'Test')
-
- self.assertFalse(53 in init.components)
- init.register_process(53, component)
- self.assertTrue(53 in init.components)
- self.assertEqual(init.components[53].name(), 'test')
- self.assertEqual(init.components[53].pid(), 53)
- self.assertEqual(init.components[53].address(), 'Test')
-
- def _start_simple_helper(self, init, verbose):
- init.verbose = verbose
-
- args = ['/bin/true']
- if verbose:
- args.append('-v')
-
- init.start_simple('/bin/true')
- self.assertEqual('/bin/true', init.started_process_name)
- self.assertEqual(args, init.started_process_args)
- self.assertEqual({'TESTENV': 'A test string'}, init.started_process_env)
-
- def test_start_simple(self):
- '''Test simple process startup.'''
- init = MockInitSimple()
- init.c_channel_env = {'TESTENV': 'A test string'}
-
- # non-verbose case
- self._start_simple_helper(init, False)
-
- # verbose case
- self._start_simple_helper(init, True)
-
- def _start_auth_helper(self, init, verbose):
- init.verbose = verbose
-
- args = ['b10-auth']
- if verbose:
- args.append('-v')
-
- init.start_auth()
- self.assertEqual('b10-auth', init.started_process_name)
- self.assertEqual(args, init.started_process_args)
- self.assertEqual({'FOO': 'an env string'}, init.started_process_env)
-
- def test_start_auth(self):
- '''Test that b10-auth is started.'''
- init = MockInitSimple()
- init.c_channel_env = {'FOO': 'an env string'}
-
- # non-verbose case
- self._start_auth_helper(init, False)
-
- # verbose case
- self._start_auth_helper(init, True)
-
- def _start_resolver_helper(self, init, verbose):
- init.verbose = verbose
-
- args = ['b10-resolver']
- if verbose:
- args.append('-v')
-
- init.start_resolver()
- self.assertEqual('b10-resolver', init.started_process_name)
- self.assertEqual(args, init.started_process_args)
- self.assertEqual({'BAR': 'an env string'}, init.started_process_env)
-
- def test_start_resolver(self):
- '''Test that b10-resolver is started.'''
- init = MockInitSimple()
- init.c_channel_env = {'BAR': 'an env string'}
-
- # non-verbose case
- self._start_resolver_helper(init, False)
-
- # verbose case
- self._start_resolver_helper(init, True)
-
- def _start_cmdctl_helper(self, init, verbose, port = None):
- init.verbose = verbose
-
- args = ['b10-cmdctl']
-
- if port is not None:
- init.cmdctl_port = port
- args.append('--port=9353')
-
- if verbose:
- args.append('-v')
-
- init.start_cmdctl()
- self.assertEqual('b10-cmdctl', init.started_process_name)
- self.assertEqual(args, init.started_process_args)
- self.assertEqual({'BAZ': 'an env string'}, init.started_process_env)
-
- def test_start_cmdctl(self):
- '''Test that b10-cmdctl is started.'''
- init = MockInitSimple()
- init.c_channel_env = {'BAZ': 'an env string'}
-
- # non-verbose case
- self._start_cmdctl_helper(init, False)
-
- # verbose case
- self._start_cmdctl_helper(init, True)
-
- # with port, non-verbose case
- self._start_cmdctl_helper(init, False, 9353)
-
- # with port, verbose case
- self._start_cmdctl_helper(init, True, 9353)
-
- def test_socket_data(self):
- '''Test that Init._socket_data works as expected.'''
- class MockSock:
- def __init__(self, fd, throw):
- self.fd = fd
- self.throw = throw
- self.buf = b'Hello World.\nYou are so nice today.\nXX'
- self.i = 0
-
- def recv(self, bufsize, flags = 0):
- if bufsize != 1:
- raise Exception('bufsize != 1')
- if flags != socket.MSG_DONTWAIT:
- raise Exception('flags != socket.MSG_DONTWAIT')
- # after 15 recv()s, throw a socket.error with EAGAIN to
- # get _socket_data() to save back what's been read. The
- # number 15 is arbitrarily chosen, but the checks then
- # depend on this being 15, i.e., if you adjust this
- # number, you may have to adjust the checks below too.
- if self.throw and self.i > 15:
- raise socket.error(errno.EAGAIN, 'Try again')
- if self.i >= len(self.buf):
- return b'';
- t = self.i
- self.i += 1
- return self.buf[t:t+1]
-
- def close(self):
- return
-
- class MockInitSocketData(Init):
- def __init__(self, throw):
- self._unix_sockets = {42: (MockSock(42, throw), b'')}
- self.requests = []
- self.dead = []
-
- def socket_request_handler(self, previous, sock):
- self.requests.append({sock.fd: previous})
-
- def socket_consumer_dead(self, sock):
- self.dead.append(sock.fd)
-
- # Case where we get data every time we call recv()
- init = MockInitSocketData(False)
- init._socket_data(42)
- self.assertEqual(init.requests,
- [{42: b'Hello World.'},
- {42: b'You are so nice today.'}])
- self.assertEqual(init.dead, [42])
- self.assertEqual({}, init._unix_sockets)
-
- # Case where socket.recv() raises EAGAIN. In this case, the
- # routine is supposed to save what it has back to
- # Init._unix_sockets.
- init = MockInitSocketData(True)
- init._socket_data(42)
- self.assertEqual(init.requests, [{42: b'Hello World.'}])
- self.assertFalse(init.dead)
- self.assertEqual(len(init._unix_sockets), 1)
- self.assertEqual(init._unix_sockets[42][1], b'You')
-
- def test_startup(self):
- '''Test that Init.startup() handles failures properly.'''
- class MockInitStartup(Init):
- def __init__(self, throw):
- self.throw = throw
- self.started = False
- self.killed = False
- self.msgq_socket_file = None
- self.curproc = 'myproc'
- self.runnable = False
-
- def start_all_components(self):
- self.started = True
- if self.throw is True:
- raise Exception('Assume starting components has failed.')
- elif self.throw:
- raise self.throw
-
- def kill_started_components(self):
- self.killed = True
-
- class DummySession():
- def __init__(self, socket_file):
- raise isc.cc.session.SessionError('This is the expected case.')
-
- class DummySessionSocketExists():
- def __init__(self, socket_file):
- # simulate that connect passes
- return
-
- isc.cc.Session = DummySession
-
- # All is well case, where all components are started
- # successfully. We check that the actual call to
- # start_all_components() is made, and Init.runnable is true.
- b10_init = MockInitStartup(False)
- r = b10_init.startup()
- self.assertIsNone(r)
- self.assertTrue(b10_init.started)
- self.assertFalse(b10_init.killed)
- self.assertTrue(b10_init.runnable)
- self.assertEqual({}, b10_init.c_channel_env)
-
- # Case where starting components fails. We check that
- # kill_started_components() is called right after, and
- # Init.runnable is not modified.
- b10_init = MockInitStartup(True)
- r = b10_init.startup()
- # r contains an error message
- self.assertEqual(r, 'Unable to start myproc: Assume starting components has failed.')
- self.assertTrue(b10_init.started)
- self.assertTrue(b10_init.killed)
- self.assertFalse(b10_init.runnable)
- self.assertEqual({}, b10_init.c_channel_env)
-
- # Check if msgq_socket_file is carried over
- b10_init = MockInitStartup(False)
- b10_init.msgq_socket_file = 'foo'
- r = b10_init.startup()
- self.assertEqual({'BIND10_MSGQ_SOCKET_FILE': 'foo'},
- b10_init.c_channel_env)
-
- # Check failure of changing user results in a different message
- b10_init = MockInitStartup(init.ChangeUserError('failed to chusr'))
- r = b10_init.startup()
- self.assertIn('failed to chusr', r)
- self.assertTrue(b10_init.killed)
-
- # Check the case when socket file already exists
- isc.cc.Session = DummySessionSocketExists
- b10_init = MockInitStartup(False)
- r = b10_init.startup()
- self.assertIn('already running', r)
-
- # isc.cc.Session is restored during tearDown().
-
-class SocketSrvTest(unittest.TestCase):
- """
- This tests some methods of b10-init related to the unix domain sockets
- used to transfer other sockets to applications.
- """
- def setUp(self):
- """
- Create the b10-init to test, testdata and backup some functions.
- """
- self.__b10_init = Init()
- self.__select_backup = init.select.select
- self.__select_called = None
- self.__socket_data_called = None
- self.__consumer_dead_called = None
- self.__socket_request_handler_called = None
-
- def tearDown(self):
- """
- Restore functions.
- """
- init.select.select = self.__select_backup
-
- class __FalseSocket:
- """
- A mock socket for the select and accept and stuff like that.
- """
- def __init__(self, owner, fileno=42):
- self.__owner = owner
- self.__fileno = fileno
- self.data = None
- self.closed = False
-
- def fileno(self):
- return self.__fileno
-
- def accept(self):
- return (self.__class__(self.__owner, 13), "/path/to/socket")
-
- def recv(self, bufsize, flags=0):
- self.__owner.assertEqual(1, bufsize)
- self.__owner.assertEqual(socket.MSG_DONTWAIT, flags)
- if isinstance(self.data, socket.error):
- raise self.data
- elif self.data is not None:
- if len(self.data):
- result = self.data[0:1]
- self.data = self.data[1:]
- return result
- else:
- raise socket.error(errno.EAGAIN, "Would block")
- else:
- return b''
-
- def close(self):
- self.closed = True
-
- class __CCS:
- """
- A mock CCS, just to provide the socket file number.
- """
- class __Socket:
- def fileno(self):
- return 1
- def get_socket(self):
- return self.__Socket()
-
- def __select_accept(self, r, w, x, t):
- self.__select_called = (r, w, x, t)
- return ([42], [], [])
-
- def __select_data(self, r, w, x, t):
- self.__select_called = (r, w, x, t)
- return ([13], [], [])
-
- def __accept(self):
- """
- Hijack the accept method of the b10-init.
-
- Notes down it was called and stops b10-init.
- """
- self.__accept_called = True
- self.__b10_init.runnable = False
-
- def test_srv_accept_called(self):
- """
- Test that the _srv_accept method of b10-init is called when the
- listening socket is readable.
- """
- self.__b10_init.runnable = True
- self.__b10_init._srv_socket = self.__FalseSocket(self)
- self.__b10_init._srv_accept = self.__accept
- self.__b10_init.ccs = self.__CCS()
- init.select.select = self.__select_accept
- self.__b10_init.run(2)
- # It called the accept
- self.assertTrue(self.__accept_called)
- # And the select had the right parameters
- self.assertEqual(([2, 1, 42], [], [], None), self.__select_called)
-
- def test_srv_accept(self):
- """
- Test how the _srv_accept method works.
- """
- self.__b10_init._srv_socket = self.__FalseSocket(self)
- self.__b10_init._srv_accept()
- # After we accepted, a new socket is added there
- socket = self.__b10_init._unix_sockets[13][0]
- # The socket is properly stored there
- self.assertTrue(isinstance(socket, self.__FalseSocket))
- # And the buffer (yet empty) is there
- self.assertEqual({13: (socket, b'')}, self.__b10_init._unix_sockets)
-
- def __socket_data(self, socket):
- self.__b10_init.runnable = False
- self.__socket_data_called = socket
-
- def test_socket_data(self):
- """
- Test that a socket that wants attention gets it.
- """
- self.__b10_init._srv_socket = self.__FalseSocket(self)
- self.__b10_init._socket_data = self.__socket_data
- self.__b10_init.ccs = self.__CCS()
- self.__b10_init._unix_sockets = {13: (self.__FalseSocket(self, 13), b'')}
- self.__b10_init.runnable = True
- init.select.select = self.__select_data
- self.__b10_init.run(2)
- self.assertEqual(13, self.__socket_data_called)
- self.assertEqual(([2, 1, 42, 13], [], [], None), self.__select_called)
-
- def __prepare_data(self, data):
- socket = self.__FalseSocket(self, 13)
- self.__b10_init._unix_sockets = {13: (socket, b'')}
- socket.data = data
- self.__b10_init.socket_consumer_dead = self.__consumer_dead
- self.__b10_init.socket_request_handler = self.__socket_request_handler
- return socket
-
- def __consumer_dead(self, socket):
- self.__consumer_dead_called = socket
-
- def __socket_request_handler(self, token, socket):
- self.__socket_request_handler_called = (token, socket)
-
- def test_socket_closed(self):
- """
- Test that a socket is removed and the socket_consumer_dead is called
- when it is closed.
- """
- socket = self.__prepare_data(None)
- self.__b10_init._socket_data(13)
- self.assertEqual(socket, self.__consumer_dead_called)
- self.assertEqual({}, self.__b10_init._unix_sockets)
- self.assertTrue(socket.closed)
-
- def test_socket_short(self):
- """
- Test that if there's not enough data to get the whole socket, it is
- kept there, but nothing is called.
- """
- socket = self.__prepare_data(b'tok')
- self.__b10_init._socket_data(13)
- self.assertEqual({13: (socket, b'tok')}, self.__b10_init._unix_sockets)
- self.assertFalse(socket.closed)
- self.assertIsNone(self.__consumer_dead_called)
- self.assertIsNone(self.__socket_request_handler_called)
-
- def test_socket_continue(self):
- """
- Test that we call the token handling function when the whole token
- comes. This test pretends to continue reading where the previous one
- stopped.
- """
- socket = self.__prepare_data(b"en\nanothe")
- # The data to finish
- self.__b10_init._unix_sockets[13] = (socket, b'tok')
- self.__b10_init._socket_data(13)
- self.assertEqual({13: (socket, b'anothe')}, self.__b10_init._unix_sockets)
- self.assertFalse(socket.closed)
- self.assertIsNone(self.__consumer_dead_called)
- self.assertEqual((b'token', socket),
- self.__socket_request_handler_called)
-
- def test_broken_socket(self):
- """
- If the socket raises an exception during the read other than EAGAIN,
- it is broken and we remove it.
- """
- sock = self.__prepare_data(socket.error(errno.ENOMEM,
- "There's more memory available, but not for you"))
- self.__b10_init._socket_data(13)
- self.assertEqual(sock, self.__consumer_dead_called)
- self.assertEqual({}, self.__b10_init._unix_sockets)
- self.assertTrue(sock.closed)
-
- def test_log_starting(self):
- """
- Checks the log_starting call doesn't raise any errors
- (does not check actual log output)
- """
- self.__b10_init.log_starting("foo")
- self.__b10_init.log_starting("foo", 1)
- self.__b10_init.log_starting("foo", 1, "192.0.2.1")
-
-
-class TestFunctions(unittest.TestCase):
- def setUp(self):
- self.lockfile_testpath = \
- "@abs_top_builddir@/src/bin/bind10/tests/lockfile_test"
- self.assertFalse(os.path.exists(self.lockfile_testpath))
- os.mkdir(self.lockfile_testpath)
- self.assertTrue(os.path.isdir(self.lockfile_testpath))
- self.__isfile_orig = init.os.path.isfile
- self.__unlink_orig = init.os.unlink
-
- def tearDown(self):
- os.rmdir(self.lockfile_testpath)
- self.assertFalse(os.path.isdir(self.lockfile_testpath))
- os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] = "@abs_top_builddir@"
- init.os.path.isfile = self.__isfile_orig
- init.os.unlink = self.__unlink_orig
-
- def test_remove_lock_files(self):
- os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] = self.lockfile_testpath
-
- # create lockfiles for the testcase
- lockfiles = ["logger_lockfile"]
- for f in lockfiles:
- fname = os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] + '/' + f
- self.assertFalse(os.path.exists(fname))
- open(fname, "w").close()
- self.assertTrue(os.path.isfile(fname))
-
- # first call should clear up all the lockfiles
- init.remove_lock_files()
-
- # check if the lockfiles exist
- for f in lockfiles:
- fname = os.environ["B10_LOCKFILE_DIR_FROM_BUILD"] + '/' + f
- self.assertFalse(os.path.isfile(fname))
-
- # second call should not assert anyway
- init.remove_lock_files()
-
- def test_remove_lock_files_fail(self):
- # Permission error on unlink is ignored; other exceptions are really
- # unexpected and propagated.
- def __raising_unlink(unused, ex):
- raise ex
-
- init.os.path.isfile = lambda _: True
- os_error = OSError()
- init.os.unlink = lambda f: __raising_unlink(f, os_error)
-
- os_error.errno = errno.EPERM
- init.remove_lock_files() # no disruption
-
- os_error.errno = errno.EACCES
- init.remove_lock_files() # no disruption
-
- os_error.errno = errno.ENOENT
- self.assertRaises(OSError, init.remove_lock_files)
-
- init.os.unlink = lambda f: __raising_unlink(f, Exception('bad'))
- self.assertRaises(Exception, init.remove_lock_files)
-
- def test_get_signame(self):
- # just test with some samples
- signame = init.get_signame(signal.SIGTERM)
- self.assertEqual('SIGTERM', signame)
- signame = init.get_signame(signal.SIGKILL)
- self.assertEqual('SIGKILL', signame)
- # 59426 is hopefully an unused signal on most platforms
- signame = init.get_signame(59426)
- self.assertEqual('Unknown signal 59426', signame)
-
- def test_fatal_signal(self):
- self.assertIsNone(init.b10_init)
- init.b10_init = Init()
- init.b10_init.runnable = True
- init.fatal_signal(signal.SIGTERM, None)
- # Now, runnable must be False
- self.assertFalse(init.b10_init.runnable)
- init.b10_init = None
-
-if __name__ == '__main__':
- # store os.environ for test_unchanged_environment
- original_os_environ = copy.deepcopy(os.environ)
- isc.log.resetUnitTestRootLogger()
- unittest.main()
+++ /dev/null
-/bindctl
-/bindctl_main.py
-/run_bindctl.sh
-/bindctl.1
+++ /dev/null
-SUBDIRS = . tests
-
-bin_SCRIPTS = bindctl
-man_MANS = bindctl.1
-
-EXTRA_DIST = $(man_MANS) bindctl.xml
-
-noinst_SCRIPTS = run_bindctl.sh
-
-python_PYTHON = __init__.py bindcmd.py cmdparse.py exception.py moduleinfo.py \
- mycollections.py command_sets.py
-pythondir = $(pyexecdir)/bindctl
-
-bindctldir = $(pkgdatadir)
-
-CLEANFILES = bindctl bindctl_main.pyc
-DISTCLEANFILES = $(man_MANS)
-
-if GENERATE_DOCS
-
-bindctl.1: bindctl.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/bindctl.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 BIND 10 > $@
-
-endif
-
-bindctl: bindctl_main.py
- $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" \
- -e "s|@@SYSCONFDIR@@|@sysconfdir@|" \
- -e "s|@@LIBEXECDIR@@|$(pkglibexecdir)|" bindctl_main.py >$@
- chmod a+x $@
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-1. Start bindctl by run command "sh bindctl".
-2. Login to b10-cmdctl with the username and password. The username and password will be saved in file default_user.csv automatically after logining successfully, so next time bindctl will login with the username and password saved in default_user.csv. For more usage information, please turn to "man bindctl".
+++ /dev/null
-1. Refactor the code for bindctl.
-2. Update man page for bindctl provided by jreed.
-3. Add more unit tests.
-4. Need Review(When command line syntax is changed later, the following
-functions should be updated first.):
- bindcmd.py:
- apply_config_cmd()
- _validate_cmd()
- complete()
-
- cmdparse.py:
- _parse_params
-
- moduleinfo.py:
- get_param_name_by_position
-5. Understandable error message when cert_file is missing.
-6. Pick location to save default user. Or use the cfgmgr for this.
- If the default user is saved in file, its password shouldn't be saved in plaintext.
-7. Need to think of what exactly to do with responses received to commands
- (currently it simply print the map)
-8. Remove bindctl-source.py.in(replace with bindctl-source.py) when merging to trunk.
+++ /dev/null
-__all__ = ['mycollections', 'exception', 'moduleinfo', 'command', 'bindctl']
+++ /dev/null
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""This module holds the BindCmdInterpreter class. This provides the
- core functionality for bindctl. It maintains a session with
- b10-cmdctl, holds local configuration and module information, and
- handles command line interface commands"""
-
-import sys
-from cmd import Cmd
-from bindctl.exception import *
-from bindctl.moduleinfo import *
-from bindctl.cmdparse import BindCmdParser
-from bindctl import command_sets
-from xml.dom import minidom
-import isc.config
-import isc.cc.data
-import http.client
-import json
-import inspect
-import pprint
-import ssl, socket
-import os, time, random, re
-import os.path
-import getpass
-from hashlib import sha1
-import csv
-import pwd
-import getpass
-import copy
-import errno
-
-try:
- from collections import OrderedDict
-except ImportError:
- from bindctl.mycollections import OrderedDict
-
-# if we have readline support, use that, otherwise use normal stdio
-try:
- import readline
- # Only consider spaces as word boundaries; identifiers can contain
- # '/' and '[]', and configuration item names can in theory use any
- # printable character. See the discussion in tickets #1345 and
- # #2254 for more information.
- readline.set_completer_delims(' ')
-
- my_readline = readline.get_line_buffer
-except ImportError:
- my_readline = sys.stdin.readline
-
-# Used for tab-completion of 'identifiers' (i.e. config values)
-# If a command parameter has this name, the tab completion hints
-# are derived from config data
-CFGITEM_IDENTIFIER_PARAM = 'identifier'
-
-CSV_FILE_NAME = 'default_user.csv'
-CONFIG_MODULE_NAME = 'config'
-CONST_BINDCTL_HELP = """
-usage: <module name> <command name> [param1 = value1 [, param2 = value2]]
-Type Tab character to get the hint of module/command/parameters.
-Type \"help(? h)\" for help on bindctl.
-Type \"<module_name> help\" for help on the specific module.
-Type \"<module_name> <command_name> help\" for help on the specific command.
-\nAvailable module names: """
-
-class ValidatedHTTPSConnection(http.client.HTTPSConnection):
- '''Overrides HTTPSConnection to support certification
- validation. '''
- def __init__(self, host, ca_certs):
- http.client.HTTPSConnection.__init__(self, host)
- self.ca_certs = ca_certs
-
- def connect(self):
- ''' Overrides the connect() so that we do
- certificate validation. '''
- sock = socket.create_connection((self.host, self.port),
- self.timeout)
- if self._tunnel_host:
- self.sock = sock
- self._tunnel()
-
- req_cert = ssl.CERT_NONE
- if self.ca_certs:
- req_cert = ssl.CERT_REQUIRED
- self.sock = ssl.wrap_socket(sock, self.key_file,
- self.cert_file,
- cert_reqs=req_cert,
- ca_certs=self.ca_certs)
-
-class BindCmdInterpreter(Cmd):
- """simple bindctl example."""
-
- def __init__(self, server_port='localhost:8080', pem_file=None,
- csv_file_dir=None):
- Cmd.__init__(self)
- self.location = ""
- self.prompt_end = '> '
- if sys.stdin.isatty():
- self.prompt = self.prompt_end
- else:
- self.prompt = ""
- self.ruler = '-'
- self.modules = OrderedDict()
- self.add_module_info(ModuleInfo("help", desc = "Get help for bindctl."))
- self.server_port = server_port
- self.conn = ValidatedHTTPSConnection(self.server_port,
- ca_certs=pem_file)
- self.session_id = self._get_session_id()
- self.config_data = None
- if csv_file_dir is not None:
- self.csv_file_dir = csv_file_dir
- else:
- self.csv_file_dir = pwd.getpwnam(getpass.getuser()).pw_dir + \
- os.sep + '.bind10' + os.sep
-
- def _print(self, *args):
- '''Simple wrapper around calls to print that can be overridden in
- unit tests.'''
- print(*args)
-
- def _get_session_id(self):
- '''Generate one session id for the connection. '''
- rand = os.urandom(16)
- now = time.time()
- session_id = sha1(("%s%s%s" %(rand, now,
- socket.gethostname())).encode())
- digest = session_id.hexdigest()
- return digest
-
- def run(self):
- '''Parse commands from user and send them to cmdctl.'''
-
- # Show helper warning about a well known issue. We only do this
- # when stdin is attached to a terminal, because otherwise it doesn't
- # matter and is just noisy, and could even be harmful if the output
- # is processed by a script that expects a specific format.
- if my_readline == sys.stdin.readline and sys.stdin.isatty():
- sys.stdout.write("""\
-WARNING: The Python readline module isn't available, so some command line
- editing features (including command history management) will not
- work. See the BIND 10 guide for more details.\n\n""")
-
- try:
- if not self.login_to_cmdctl():
- return 1
-
- self.cmdloop()
- self._print('\nExit from bindctl')
- return 0
- except FailToLogin as err:
- # error already printed when this was raised, ignoring
- return 1
- except KeyboardInterrupt:
- self._print('\nExit from bindctl')
- return 0
- except socket.error as err:
- self._print('Failed to send request, the connection is closed')
- return 1
- except http.client.CannotSendRequest:
- self._print('Can not send request, the connection is busy')
- return 1
-
- def _get_saved_user_info(self, dir, file_name):
- ''' Read all the available username and password pairs saved in
- file(path is "dir + file_name"), Return value is one list of elements
- ['name', 'password'], If get information failed, empty list will be
- returned.'''
- if (not dir) or (not os.path.exists(dir)):
- return []
-
- try:
- csvfile = None
- users = []
- csvfile = open(dir + file_name)
- users_info = csv.reader(csvfile)
- for row in users_info:
- users.append([row[0], row[1]])
- except (IOError, IndexError) as err:
- self._print("Error reading saved username and password "
- "from %s%s: %s" % (dir, file_name, err))
- finally:
- if csvfile:
- csvfile.close()
- return users
-
- def _save_user_info(self, username, passwd, dir, file_name):
- ''' Save username and password in file "dir + file_name"
- If it's saved properly, return True, or else return False. '''
- try:
- if not os.path.exists(dir):
- os.mkdir(dir, 0o700)
-
- csvfilepath = dir + file_name
- csvfile = open(csvfilepath, 'w')
- os.chmod(csvfilepath, 0o600)
- writer = csv.writer(csvfile)
- writer.writerow([username, passwd])
- csvfile.close()
- except IOError as err:
- self._print("Error saving user information:", err)
- self._print("user info file name: %s%s" % (dir, file_name))
- return False
-
- return True
-
- def _try_login(self, username, password):
- '''
- Attempts to log into cmdctl by sending a POST with the given
- username and password. On success of the POST (not the login,
- but the network operation), it returns a tuple (response, data).
- We check for some failures such as SSL errors and socket errors
- which could happen due to the environment in which BIND 10 runs.
- On failure, it raises a FailToLogin exception and prints some
- information on the failure. This call is essentially 'private',
- but made 'protected' for easier testing.
- '''
- param = {'username': username, 'password' : password}
- try:
- response = self.send_POST('/login', param)
- data = response.read().decode()
- # return here (will raise error after try block)
- return (response, data)
- except (ssl.SSLError, socket.error) as err:
- self._print('Error while sending login information:', err)
- pass
- raise FailToLogin()
-
- def login_to_cmdctl(self):
- '''Login to cmdctl with the username and password given by
- the user. After the login is sucessful, the username and
- password will be saved in 'default_user.csv', when run the next
- time, username and password saved in 'default_user.csv' will be
- used first.
- '''
- # Look at existing username/password combinations and try to log in
- users = self._get_saved_user_info(self.csv_file_dir, CSV_FILE_NAME)
- for row in users:
- response, data = self._try_login(row[0], row[1])
-
- if response.status == http.client.OK:
- # Is interactive?
- if sys.stdin.isatty():
- self._print(data + ' login as ' + row[0])
- return True
-
- # No valid logins were found, prompt the user for a username/password
- count = 0
- if not os.path.exists(self.csv_file_dir + CSV_FILE_NAME):
- self._print('\nNo stored password file found.\n\n'
- 'When the system is first set up you need to create '
- 'at least one user account.\n'
- 'For information on how to set up a BIND 10 system, '
- 'please check see the\n'
- 'BIND 10 Guide: \n\n'
- 'http://bind10.isc.org/docs/bind10-guide.html#quick-start-auth-dns\n\n'
-
- 'If a user account has been set up, please check the '
- 'b10-cmdctl log for other\n'
- 'information.\n')
- else:
- self._print('Login failed: either the user name or password is '
- 'invalid.\n')
-
- # If this was not an interactive session do not prompt for login info.
- if not sys.stdin.isatty():
- return False
-
- while True:
- count = count + 1
- if count > 3:
- self._print("Too many authentication failures")
- return False
-
- username = input("Username: ")
- passwd = getpass.getpass()
-
- response, data = self._try_login(username, passwd)
- self._print(data)
-
- if response.status == http.client.OK:
- self._save_user_info(username, passwd, self.csv_file_dir,
- CSV_FILE_NAME)
- return True
-
- def _update_commands(self):
- '''Update the commands of all modules. '''
- for module_name in self.config_data.get_config_item_list():
- self._prepare_module_commands(self.config_data.get_module_spec(module_name))
-
- def _send_message(self, url, body):
- headers = {"cookie" : self.session_id}
- self.conn.request('GET', url, body, headers)
- res = self.conn.getresponse()
- return res.status, res.read()
-
- def send_GET(self, url, body = None):
- '''Send GET request to cmdctl, session id is send with the name
- 'cookie' in header.
- '''
- status, reply_msg = self._send_message(url, body)
- if status == http.client.UNAUTHORIZED:
- if self.login_to_cmdctl():
- # successful, so try send again
- status, reply_msg = self._send_message(url, body)
-
- if reply_msg:
- return json.loads(reply_msg.decode())
- else:
- return {}
-
-
- def send_POST(self, url, post_param=None):
- '''Send POST request to cmdctl, session id is send with the name
- 'cookie' in header.
- Format: /module_name/command_name
- parameters of command is encoded as a map
- '''
- param = None
- if post_param is not None and len(post_param) != 0:
- param = json.dumps(post_param)
-
- headers = {"cookie" : self.session_id}
- self.conn.request('POST', url, param, headers)
- return self.conn.getresponse()
-
- def _update_all_modules_info(self):
- ''' Get all modules' information from cmdctl, including
- specification file and configuration data. This function
- should be called before interpreting command line or complete-key
- is entered. This may not be the best way to keep bindctl
- and cmdctl share same modules information, but it works.'''
- if self.config_data is not None:
- self.config_data.update_specs_and_config()
- else:
- self.config_data = isc.config.UIModuleCCSession(self)
- self._update_commands()
-
- def precmd(self, line):
- if line != 'EOF':
- self._update_all_modules_info()
- return line
-
- def postcmd(self, stop, line):
- '''Update the prompt after every command, but only if we
- have a tty as output'''
- if sys.stdin.isatty():
- self.prompt = self.location + self.prompt_end
- return stop
-
- def _prepare_module_commands(self, module_spec):
- '''Prepare the module commands'''
- module = ModuleInfo(name = module_spec.get_module_name(),
- desc = module_spec.get_module_description())
- for command in module_spec.get_commands_spec():
- cmd = CommandInfo(name = command["command_name"],
- desc = command["command_description"])
- for arg in command["command_args"]:
- param = ParamInfo(name = arg["item_name"],
- type = arg["item_type"],
- optional = bool(arg["item_optional"]),
- param_spec = arg)
- if ("item_default" in arg):
- param.default = arg["item_default"]
- if ("item_description" in arg):
- param.desc = arg["item_description"]
- cmd.add_param(param)
- module.add_command(cmd)
- self.add_module_info(module)
-
- def _validate_cmd(self, cmd):
- '''validate the parameters and merge some parameters together,
- merge algorithm is based on the command line syntax, later, if
- a better command line syntax come out, this function should be
- updated first.
- '''
- if not cmd.module in self.modules:
- raise CmdUnknownModuleSyntaxError(cmd.module)
-
- module_info = self.modules[cmd.module]
- if not module_info.has_command_with_name(cmd.command):
- raise CmdUnknownCmdSyntaxError(cmd.module, cmd.command)
-
- command_info = module_info.get_command_with_name(cmd.command)
- manda_params = command_info.get_mandatory_param_names()
- all_params = command_info.get_param_names()
-
- # If help is entered, don't do further parameter validation.
- for val in cmd.params.keys():
- if val == "help":
- return
-
- params = cmd.params.copy()
- if not params and manda_params:
- raise CmdMissParamSyntaxError(cmd.module, cmd.command, manda_params[0])
- elif params and not all_params:
- raise CmdUnknownParamSyntaxError(cmd.module, cmd.command,
- list(params.keys())[0])
- elif params:
- param_name = None
- param_count = len(params)
- for name in params:
- # either the name of the parameter must be known, or
- # the 'name' must be an integer (ie. the position of
- # an unnamed argument
- if type(name) == int:
- # lump all extraneous arguments together as one big final one
- # todo: check if last param type is a string?
- while (param_count > 2 and
- param_count > len(command_info.params) - 1):
- params[param_count - 2] += " " + params[param_count - 1]
- del(params[param_count - 1])
- param_count = len(params)
- cmd.params = params.copy()
-
- # (-1, help is always in the all_params list)
- if name >= len(all_params) - 1:
- # add to last known param
- if param_name:
- cmd.params[param_name] += cmd.params[name]
- else:
- raise CmdUnknownParamSyntaxError(cmd.module, cmd.command, cmd.params[name])
- else:
- # replace the numbered items by named items
- param_name = command_info.get_param_name_by_position(name, param_count)
- cmd.params[param_name] = cmd.params[name]
- del cmd.params[name]
-
- elif not name in all_params:
- raise CmdUnknownParamSyntaxError(cmd.module, cmd.command, name)
-
- param_nr = 0
- for name in manda_params:
- if not name in params and not param_nr in params:
- raise CmdMissParamSyntaxError(cmd.module, cmd.command, name)
- param_nr += 1
-
- # Convert parameter value according to parameter spec
- # file. Ignore check for commands belonging to module 'config'
- # or 'execute'.
- if cmd.module != CONFIG_MODULE_NAME and\
- cmd.module != command_sets.EXECUTE_MODULE_NAME:
- for param_name in cmd.params:
- param_spec = command_info.get_param_with_name(param_name).param_spec
- try:
- cmd.params[param_name] = isc.config.config_data.convert_type(param_spec, cmd.params[param_name])
- except isc.cc.data.DataTypeError as e:
- raise isc.cc.data.DataTypeError('Invalid parameter value for \"%s\", the type should be \"%s\" \n'
- % (param_name, param_spec['item_type']) + str(e))
-
- def _handle_cmd(self, cmd):
- '''Handle a command entered by the user'''
- if cmd.command == "help" or ("help" in cmd.params.keys()):
- self._handle_help(cmd)
- elif cmd.module == CONFIG_MODULE_NAME:
- self.apply_config_cmd(cmd)
- elif cmd.module == command_sets.EXECUTE_MODULE_NAME:
- self.apply_execute_cmd(cmd)
- else:
- self.apply_cmd(cmd)
-
- def add_module_info(self, module_info):
- '''Add the information about one module'''
- self.modules[module_info.name] = module_info
-
- def get_module_names(self):
- '''Return the names of all known modules'''
- return list(self.modules.keys())
-
- #override methods in cmd
- def default(self, line):
- self._parse_cmd(line)
-
- def emptyline(self):
- pass
-
- def do_help(self, name):
- self._print(CONST_BINDCTL_HELP)
- for k in self.modules.values():
- n = k.get_name()
- if len(n) >= CONST_BINDCTL_HELP_INDENT_WIDTH:
- self._print(" %s" % n)
- self._print(textwrap.fill(k.get_desc(),
- initial_indent=" ",
- subsequent_indent=" " +
- " " * CONST_BINDCTL_HELP_INDENT_WIDTH,
- width=70))
- else:
- self._print(textwrap.fill("%s%s%s" %
- (k.get_name(),
- " "*(CONST_BINDCTL_HELP_INDENT_WIDTH -
- len(k.get_name())),
- k.get_desc()),
- initial_indent=" ",
- subsequent_indent=" " +
- " " * CONST_BINDCTL_HELP_INDENT_WIDTH,
- width=70))
-
- def onecmd(self, line):
- if line == 'EOF' or line.lower() == "quit":
- self.conn.close()
- return True
-
- if line == 'h':
- line = 'help'
-
- Cmd.onecmd(self, line)
-
- def _get_identifier_startswith(self, id_text):
- """Return the tab-completion hints for identifiers starting with
- id_text.
-
- Parameters:
- id_text (string): the currently entered identifier part, which
- is to be completed.
- """
- # Strip starting "/" from id_text
- if id_text.startswith('/'):
- id_text = id_text[1:]
- # Get all items from the given module (up to the first /)
- list = self.config_data.get_config_item_list(
- id_text.rpartition("/")[0], recurse=True)
- # filter out all possibilities that don't match currently entered
- # text part
- hints = [val for val in list if val.startswith(id_text)]
- return hints
-
- def _cmd_has_identifier_param(self, cmd):
- """
- Returns True if the given (parsed) command is known and has a
- parameter which points to a config data identifier
-
- Parameters:
- cmd (cmdparse.BindCmdParser): command context, including given params
-
- """
- if cmd.module not in self.modules:
- return False
- command = self.modules[cmd.module].get_command_with_name(cmd.command)
- return command.has_param_with_name(CFGITEM_IDENTIFIER_PARAM)
-
- def complete(self, text, state):
- """
- Returns tab-completion hints. See the python documentation of the
- readline and Cmd modules for more information.
-
- The first time this is called (within one 'completer' action), it
- has state 0, and a list of possible completions is made. This list
- is stored; complete() will then be called with increasing values of
- state, until it returns None. For each call it returns the state'th
- element of the hints it collected in the first call.
-
- The hints list contents depend on which part of the full command
- line; if no module is given yet, it will list all modules. If a
- module is given, but no command, it will complete with module
- commands. If both have been given, it will create the hints based on
- the command parameters.
-
- If module and command have already been specified, and the command
- has a parameter 'identifier', the configuration data is used to
- create the hints list.
-
- Parameters:
- text (string): The text entered so far in the 'current' part of
- the command (module, command, parameters)
- state (int): state used in the readline tab-completion logic;
- 0 on first call, increasing by one until there are
- no (more) hints to return.
-
- Returns the string value of the hints list with index 'state',
- or None if no (more) hints are available.
- """
- if state == 0:
- self._update_all_modules_info()
- text = text.strip()
- hints = []
- cur_line = my_readline()
- try:
- cmd = BindCmdParser(cur_line)
- if not cmd.params and text:
- hints = self._get_command_startswith(cmd.module, text)
- elif self._cmd_has_identifier_param(cmd):
- # If the command has an argument that is a configuration
- # identifier (currently, this is only a subset of
- # the config commands), then don't tab-complete with
- # hints derived from command parameters, but from
- # possible configuration identifiers.
- #
- # This solves the issue reported in #2254, where
- # there were hints such as 'argument' and 'identifier'.
- #
- # Since they are replaced, the tab-completion no longer
- # adds 'help' as an option (but it still works)
- #
- # Also, currently, tab-completion does not work
- # together with 'config go' (it does not take 'current
- # position' into account). But config go currently has
- # problems by itself, unrelated to completion.
- hints = self._get_identifier_startswith(text)
- else:
- hints = self._get_param_startswith(cmd.module, cmd.command,
- text)
-
- except CmdModuleNameFormatError:
- if not text:
- hints = self.get_module_names()
-
- except CmdMissCommandNameFormatError as e:
- if not text.strip(): # command name is empty
- hints = self.modules[e.module].get_command_names()
- else:
- hints = self._get_module_startswith(text)
-
- except CmdCommandNameFormatError as e:
- if e.module in self.modules:
- hints = self._get_command_startswith(e.module, text)
-
- except CmdParamFormatError as e:
- hints = self._get_param_startswith(e.module, e.command, text)
-
- except BindCtlException:
- hints = []
-
- self.hint = hints
-
- if state < len(self.hint):
- return self.hint[state]
- else:
- return None
-
-
- def _get_module_startswith(self, text):
- return [module
- for module in self.modules
- if module.startswith(text)]
-
-
- def _get_command_startswith(self, module, text):
- if module in self.modules:
- return [command
- for command in self.modules[module].get_command_names()
- if command.startswith(text)]
-
- return []
-
-
- def _get_param_startswith(self, module, command, text):
- if module in self.modules:
- module_info = self.modules[module]
- if command in module_info.get_command_names():
- cmd_info = module_info.get_command_with_name(command)
- params = cmd_info.get_param_names()
- hint = []
- if text:
- hint = [val for val in params if val.startswith(text)]
- else:
- hint = list(params)
-
- if len(hint) == 1 and hint[0] != "help":
- hint[0] = hint[0] + " ="
-
- return hint
-
- return []
-
- def _parse_cmd(self, line):
- try:
- cmd = BindCmdParser(line)
- self._validate_cmd(cmd)
- self._handle_cmd(cmd)
- except (IOError, http.client.HTTPException) as err:
- self._print('Error: ', err)
- except BindCtlException as err:
- self._print("Error! ", err)
- self._print_correct_usage(err)
- except isc.cc.data.DataTypeError as err:
- self._print("Error! ", err)
- except isc.cc.data.DataTypeError as dte:
- self._print("Error: " + str(dte))
- except isc.cc.data.DataNotFoundError as dnfe:
- self._print("Error: " + str(dnfe))
- except isc.cc.data.DataAlreadyPresentError as dape:
- self._print("Error: " + str(dape))
- except KeyError as ke:
- self._print("Error: missing " + str(ke))
-
- def _print_correct_usage(self, ept):
- if isinstance(ept, CmdUnknownModuleSyntaxError):
- self.do_help(None)
-
- elif isinstance(ept, CmdUnknownCmdSyntaxError):
- self.modules[ept.module].module_help()
-
- elif isinstance(ept, CmdMissParamSyntaxError) or \
- isinstance(ept, CmdUnknownParamSyntaxError):
- self.modules[ept.module].command_help(ept.command)
-
-
- def _append_space_to_hint(self):
- """Append one space at the end of complete hint."""
- self.hint = [(val + " ") for val in self.hint]
-
-
- def _handle_help(self, cmd):
- if cmd.command == "help":
- self.modules[cmd.module].module_help()
- else:
- self.modules[cmd.module].command_help(cmd.command)
-
-
- def apply_config_cmd(self, cmd):
- '''Handles a configuration command.
- Raises a DataTypeError if a wrong value is set.
- Raises a DataNotFoundError if a wrong identifier is used.
- Raises a KeyError if the command was not complete
- '''
- identifier = self.location
- if 'identifier' in cmd.params:
- if not identifier.endswith("/"):
- identifier += "/"
- if cmd.params['identifier'].startswith("/"):
- identifier = cmd.params['identifier']
- else:
- if cmd.params['identifier'].startswith('['):
- identifier = identifier[:-1]
- identifier += cmd.params['identifier']
-
- # Check if the module is known; for unknown modules
- # we currently deny setting preferences, as we have
- # no way yet to determine if they are ok.
- module_name = identifier.split('/')[1]
- if module_name != "" and (self.config_data is None or \
- not self.config_data.have_specification(module_name)):
- self._print("Error: Module '" + module_name +
- "' unknown or not running")
- return
-
- if cmd.command == "show":
- # check if we have the 'all' argument
- show_all = False
- if 'argument' in cmd.params:
- if cmd.params['argument'] == 'all':
- show_all = True
- elif 'identifier' not in cmd.params:
- # no 'all', no identifier, assume this is the
- #identifier
- identifier += cmd.params['argument']
- else:
- self._print("Error: unknown argument " +
- cmd.params['argument'] +
- ", or multiple identifiers given")
- return
- values = self.config_data.get_value_maps(identifier, show_all)
- for value_map in values:
- line = value_map['name']
- if value_map['type'] in [ 'module', 'map' ]:
- line += "/"
- elif value_map['type'] == 'list' \
- and value_map['value'] != []:
- # do not print content of non-empty lists if
- # we have more data to show
- line += "/"
- else:
- # if type is named_set, don't print value if None
- # (it is either {} meaning empty, or None, meaning
- # there actually is data, but not to be shown with
- # the current command
- if value_map['type'] == 'named_set' and\
- value_map['value'] is None:
- line += "/\t"
- else:
- line += "\t" + json.dumps(value_map['value'])
- line += "\t" + value_map['type']
- line += "\t"
- if value_map['default']:
- line += "(default)"
- if value_map['modified']:
- line += "(modified)"
- self._print(line)
- elif cmd.command == "show_json":
- if identifier == "":
- self._print("Need at least the module to show the "
- "configuration in JSON format")
- else:
- data, default = self.config_data.get_value(identifier)
- self._print(json.dumps(data))
- elif cmd.command == "add":
- self.config_data.add_value(identifier,
- cmd.params.get('value_or_name'),
- cmd.params.get('value_for_set'))
- elif cmd.command == "remove":
- if 'value' in cmd.params:
- self.config_data.remove_value(identifier, cmd.params['value'])
- else:
- self.config_data.remove_value(identifier, None)
- elif cmd.command == "set":
- if 'identifier' not in cmd.params:
- self._print("Error: missing identifier or value")
- else:
- parsed_value = None
- try:
- parsed_value = json.loads(cmd.params['value'])
- except Exception as exc:
- # ok could be an unquoted string, interpret as such
- parsed_value = cmd.params['value']
- self.config_data.set_value(identifier, parsed_value)
- elif cmd.command == "unset":
- self.config_data.unset(identifier)
- elif cmd.command == "revert":
- self.config_data.clear_local_changes()
- elif cmd.command == "commit":
- try:
- self.config_data.commit()
- except isc.config.ModuleCCSessionError as mcse:
- self._print(str(mcse))
- elif cmd.command == "diff":
- self._print(self.config_data.get_local_changes())
- elif cmd.command == "go":
- self.go(identifier)
-
- def go(self, identifier):
- '''Handles the config go command, change the 'current' location
- within the configuration tree. '..' will be interpreted as
- 'up one level'.'''
- id_parts = isc.cc.data.split_identifier(identifier)
-
- new_location = ""
- for id_part in id_parts:
- if (id_part == ".."):
- # go 'up' one level
- new_location, a, b = new_location.rpartition("/")
- else:
- new_location += "/" + id_part
- # check if exists, if not, revert and error
- v,d = self.config_data.get_value(new_location)
- if v is None:
- self._print("Error: " + identifier + " not found")
- return
-
- self.location = new_location
-
- def apply_execute_cmd(self, command):
- '''Handles the 'execute' command, which executes a number of
- (preset) statements. The command set to execute is either
- read from a file (e.g. 'execute file <file>'.) or one
- of the sets as defined in command_sets.py'''
- if command.command == 'file':
- try:
- with open(command.params['filename']) as command_file:
- commands = command_file.readlines()
- except IOError as ioe:
- self._print("Error: " + str(ioe))
- return
- elif command_sets.has_command_set(command.command):
- commands = command_sets.get_commands(command.command)
- else:
- # Should not be reachable; parser should've caught this
- raise Exception("Unknown execute command type " + command.command)
-
- # We have our set of commands now, depending on whether 'show' was
- # specified, show or execute them
- if 'show' in command.params and command.params['show'] == 'show':
- self.__show_execute_commands(commands)
- else:
- self.__apply_execute_commands(commands)
-
- def __show_execute_commands(self, commands):
- '''Prints the command list without executing them'''
- for line in commands:
- self._print(line.strip())
-
- def __apply_execute_commands(self, commands):
- '''Applies the configuration commands from the given iterator.
- This is the method that catches, comments, echo statements, and
- other directives. All commands not filtered by this method are
- interpreted as if they are directly entered in an active session.
- Lines starting with any of the following characters are not
- passed directly:
- # - These are comments
- ! - These are directives
- !echo: print the rest of the line
- !verbose on/off: print the commands themselves too
- Unknown directives are ignored (with a warning)
- The execution is stopped if there are any errors.
- '''
- verbose = False
- try:
- for line in commands:
- line = line.strip()
- if verbose:
- self._print(line)
- if line.startswith('#') or len(line) == 0:
- continue
- elif line.startswith('!'):
- if re.match('^!echo ', line, re.I) and len(line) > 6:
- self._print(line[6:])
- elif re.match('^!verbose\s+on\s*$', line, re.I):
- verbose = True
- elif re.match('^!verbose\s+off$', line, re.I):
- verbose = False
- else:
- self._print("Warning: ignoring unknown directive: " +
- line)
- else:
- cmd = BindCmdParser(line)
- self._validate_cmd(cmd)
- self._handle_cmd(cmd)
- except (isc.config.ModuleCCSessionError,
- IOError, http.client.HTTPException,
- BindCtlException, isc.cc.data.DataTypeError,
- isc.cc.data.DataNotFoundError,
- isc.cc.data.DataAlreadyPresentError,
- KeyError) as err:
- self._print('Error: ', err)
- self._print()
- self._print('Depending on the contents of the script, and which')
- self._print('commands it has called, there can be committed and')
- self._print('local changes. It is advised to check your settings')
- self._print(', and revert local changes with "config revert".')
-
- def apply_cmd(self, cmd):
- '''Handles a general module command'''
- url = '/' + cmd.module + '/' + cmd.command
- cmd_params = None
- if (len(cmd.params) != 0):
- cmd_params = json.dumps(cmd.params)
-
- reply = self.send_POST(url, cmd.params)
- data = reply.read().decode()
- # The reply is a string containing JSON data,
- # parse it, then prettyprint
- if data != "" and data != "{}":
- self._print(json.dumps(json.loads(data), sort_keys=True,
- indent=4))
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2010-2012 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>June 20, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>bindctl</refentrytitle>
- <manvolnum>1</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>bindctl</refname>
- <refpurpose>control and configure BIND 10</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2010</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>bindctl</command>
- <arg><option>-a <replaceable>address</replaceable></option></arg>
- <arg><option>-h</option></arg>
- <arg><option>-c <replaceable>file</replaceable></option></arg>
- <arg><option>-p <replaceable>number</replaceable></option></arg>
- <arg><option>--address <replaceable>address</replaceable></option></arg>
- <arg><option>--help</option></arg>
- <arg><option>--certificate-chain <replaceable>file</replaceable></option></arg>
- <arg><option>--csv-file-dir<replaceable>file</replaceable></option></arg>
- <arg><option>--port <replaceable>number</replaceable></option></arg>
- <arg><option>--version</option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>
- The <command>bindctl</command> tool is a user interface to
- the BIND 10 services. The program can be used to control
- the components and configure the BIND 10 options. The options
- may be specified
-<!-- TODO: as command-line arguments, or --> via its
- interactive command interpreter.
- </para>
-
- <para>
- <command>bindctl</command> communicates over a HTTPS REST-ful
- interface provided by
- <citerefentry><refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
- The
-<citerefentry><refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum></citerefentry>
- daemon stores the configurations.
- </para>
-
- </refsect1>
-
- <refsect1>
- <title>ARGUMENTS</title>
-
- <para>The arguments are as follows:</para>
-
- <variablelist>
-
- <varlistentry>
- <term><option>-a</option> <replaceable>address</replaceable>, <option>--address</option> <replaceable>address</replaceable></term>
-
- <listitem>
- <para>The IPv4 or IPv6 address to use to connect to the running
- <citerefentry><refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum></citerefentry>
- daemon.
- The default is 127.0.0.1.
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-c</option> <replaceable>file</replaceable>,
- <option>--certificate-chain</option> <replaceable>file</replaceable></term>
-
- <listitem>
- <para>The PEM formatted server certificate validation chain file.
- </para>
-<!-- TODO: any default? -->
-<!-- TODO: any way to choose this for cmdctl? -->
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>--csv-file-dir</option><replaceable>file</replaceable>
- </term>
-
- <listitem>
- <para>
- The directory name in which the user/password CSV file
- is stored (see AUTHENTICATION).
- By default this option doesn't have any value,
- in which case the "<filename>.bind10</filename>" directory
- under the user's home directory will be used.
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-h</option>,
- <option>--help</option></term>
- <listitem><para>
- Display command usage.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-p</option> <replaceable>number</replaceable>, <option>--port</option> <replaceable>number</replaceable></term>
-
- <listitem>
- <para>The port number to use to connect to the running
- <citerefentry><refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum></citerefentry>
- daemon.
- The default is 8080.</para>
-<!-- TODO: -->
- <note><simpara>This default port number may change.</simpara></note>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>--version</option></term>
- <listitem><para>
- Display the version number and exit.</para>
- </listitem>
- </varlistentry>
-
- </variablelist>
-
- </refsect1>
-
- <refsect1>
- <title>AUTHENTICATION</title>
-
- <para>
- The tool will authenticate using a username and password.
- On the first successful login, it will save the details to
- a comma-separated-value (CSV) file
- which will be used for later uses of <command>bindctl</command>.
- The file name is "<filename>default_user.csv</filename>"
- located under the directory specified by the
- <option>--csv-file-dir</option> option.
- </para>
-
-<!-- TODO: mention HTTPS? -->
-
- </refsect1>
-
- <refsect1>
- <title>USAGE</title>
-
- <para>
- The <command>bindctl</command> prompt shows
- <quote>> </quote>.
- The prompt will also display the location if changed.
- The options are based on the module in use.
- The usage is:
- <command>module</command> <command>command</command> <replaceable>param1 = value1 , <replaceable>param2 = value2</replaceable></replaceable>
- </para>
-
- <para>
- <command>bindctl</command>'s
- interactive interface provides command-line completion and
- hints. Press the Tab key to get a hint for the module,
- command, and/or parameters.
-<!-- TODO: no hints at a blank line though ? -->
-
- The arrow keys and Emacs-style editing keys may be used to edit
- and recall previous lines.
-<!-- TODO: all Emacs-style editing keys? -->
- </para>
-
- <para>
- You can use the <command>help</command>
- keyword to receive usage assistance for a module or a module's
- command.
- </para>
-
- <para>
- The <command>quit</command>
- command is used to exit
- <command>bindctl</command>.
- (It doesn't stop the BIND 10 services.)
- </para>
-
- <para>
- The following module is available by default:
- <command>config</command> for Configuration commands.
-<!-- TODO: or is config from the cfgmgr module? -->
- Additional modules may be available, such as
- <command>Init</command>, <command>Xfrin</command>, and
- <command>Auth</command>.
- </para>
-
-<!--
-.Sh CONFIGURATION COMMANDS
-The following configuration commands are recognized by
-.Nm :
-.Bl -tag -width Ic
-.It Ic add Ar identifier Ar value
-Add a value to a list.
-.\" TODO: explain better
-.\" TODO: define list, map, others ...
-.\" TODO: why the value?
-.It Ic commit
-Commit all local changes.
-.\" TODO: what does this do?
-.It Ic go Ar identifier
-Go to the specified location within the configuration.
-.\" TODO: explain this
-.It Ic help
-Get help for the
-.Em config
-module.
-.It Ic remove Ar identifier Ar value
-Remove a value from a list.
-.\" TODO: why the value?
-.It Ic revert
-Revert all local changes (that have not been committed).
-.\" TODO: what does this do?
-.It Ic set Ar identifier Ar value
-Set a configuration value.
-.\" TODO
-.It Ic show Op Ar identifier.
-.\" TODO
-Show the currently set configuration values.
-If no
-.Ar identifier
-is given, the current location is used (as set by the
-.Ic go
-command).
-If a config option is a list or a map, the value is not
-shown directly, but must be requested separately.
-.\" TODO: example of this list or map
-.It Ic unset Ar identifier
-Remove a configuration value.
-If the option is mandatory, it will reset to the default value.
-.\" TODO: how to know default values before?
-.El
-.\" This manual page only covers builtin commands.
-.\" .Sh PARKINGLOT COMMANDS
-.\" .Bl -tag -width Ic
-.\" .It Ic help
-.\" Get help for the
-.\" .Em ParkingLot
-.\" module.
-.\"
-.\" TODO: Command arguments which have embedded spaces may be quoted with
-.\" quote
-.\" .Sq \&"
-.\" marks.
-.Pp
-.Pp command-line editing?
-.Pp
-.Sh EXAMPLES
-The following are some
-.Nm
-examples:
-.Bl -tag -width abcde
-.It Ic "config help"
-Show the commands for the
-.Em config
-module.
-.It Ic "config go ParkingLot/"
-Go to the
-.Dq ParkingLot/
-configuration location.
--->
-
- </refsect1>
-
-<!-- TODO: any options? -->
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-init</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
-<!-- TODO: point to developer docs -->
- </refsect1>
-
- <refsect1>
- <title>AUTHORS</title>
- <para>
- The <command>bindctl</command>
- tool and library were initially coded by Zhang Likun of CNNIC
- for the BIND 10 project.
- The initial manual page was written by Jeremy C. Reed of ISC.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- The initial version (with internal name of
- <command>BigTool</command>)
- was started in October 2009.
- </para>
- </refsect1>
-
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
-
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""This is the main calling class for the bindctl configuration and
- command tool. It sets up a command interpreter and runs that."""
-
-import sys; sys.path.append ('@@PYTHONPATH@@')
-
-from bindctl.moduleinfo import *
-from bindctl.bindcmd import *
-from bindctl import command_sets
-import pprint
-from optparse import OptionParser, OptionValueError
-import isc.util.process
-import isc.util.traceback_handler
-
-isc.util.process.rename()
-
-# This is the version that gets displayed to the user.
-# The VERSION string consists of the module name, the module version
-# number, and the overall BIND 10 version number (set in configure.ac).
-VERSION = "bindctl 20110217 (BIND 10 @PACKAGE_VERSION@)"
-
-DEFAULT_IDENTIFIER_DESC = "The identifier specifies the config item. Child elements are separated with the '/' character. List indices can be specified with '[i]', where i is an integer specifying the index, starting with 0. Examples: 'Init/start_auth', 'Auth/listen_on[0]/address'. If no identifier is given, shows the item at the current location."
-
-def prepare_config_commands(tool):
- '''Prepare fixed commands for local configuration editing'''
- module = ModuleInfo(name = CONFIG_MODULE_NAME, desc = "Configuration commands.")
- cmd = CommandInfo(name = "show", desc = "Show configuration.")
- param = ParamInfo(name = "argument", type = "string", optional=True, desc = "If you specify the argument 'all' (before the identifier), recursively show all child elements for the given identifier.")
- cmd.add_param(param)
- param = ParamInfo(name=CFGITEM_IDENTIFIER_PARAM, type="string",
- optional=True, desc=DEFAULT_IDENTIFIER_DESC)
- cmd.add_param(param)
- module.add_command(cmd)
-
- cmd = CommandInfo(name="show_json",
- desc="Show full configuration in JSON format.")
- param = ParamInfo(name=CFGITEM_IDENTIFIER_PARAM, type="string",
- optional=True, desc=DEFAULT_IDENTIFIER_DESC)
- cmd.add_param(param)
- module.add_command(cmd)
-
- cmd = CommandInfo(name="add", desc=
- "Add an entry to configuration list or a named set. "
- "When adding to a list, the command has one optional argument, "
- "a value to add to the list. The value must be in correct JSON "
- "and complete. When adding to a named set, it has one "
- "mandatory parameter (the name to add), and an optional "
- "parameter value, similar to when adding to a list. "
- "In either case, when no value is given, an entry will be "
- "constructed with default values.")
- param = ParamInfo(name=CFGITEM_IDENTIFIER_PARAM, type="string",
- optional=True, desc=DEFAULT_IDENTIFIER_DESC)
- cmd.add_param(param)
- param = ParamInfo(name="value_or_name", type="string", optional=True,
- desc="Specifies a value to add to the list, or the name when adding to a named set. It must be in correct JSON format and complete.")
- cmd.add_param(param)
- module.add_command(cmd)
- param = ParamInfo(name="value_for_set", type="string", optional=True,
- desc="Specifies an optional value to add to the named map. It must be in correct JSON format and complete.")
- cmd.add_param(param)
- module.add_command(cmd)
-
- cmd = CommandInfo(name="remove", desc="Remove entry from configuration list or named set.")
- param = ParamInfo(name=CFGITEM_IDENTIFIER_PARAM, type="string",
- optional=True, desc=DEFAULT_IDENTIFIER_DESC)
- cmd.add_param(param)
- param = ParamInfo(name = "value", type = "string", optional=True, desc = "When identifier is a list, specifies a value to remove from the list. It must be in correct JSON format and complete. When it is a named set, specifies the name to remove.")
- cmd.add_param(param)
- module.add_command(cmd)
-
- cmd = CommandInfo(name="set", desc="Set a configuration value.")
- param = ParamInfo(name=CFGITEM_IDENTIFIER_PARAM, type="string",
- optional=True, desc=DEFAULT_IDENTIFIER_DESC)
- cmd.add_param(param)
- param = ParamInfo(name="value", type="string", optional=False,
- desc="Specifies a value to set. It must be in correct JSON format and complete.")
- cmd.add_param(param)
- module.add_command(cmd)
-
- cmd = CommandInfo(name="unset", desc="Unset a configuration value (i.e. revert to the default, if any).")
- param = ParamInfo(name=CFGITEM_IDENTIFIER_PARAM, type="string",
- optional=False, desc=DEFAULT_IDENTIFIER_DESC)
- cmd.add_param(param)
- module.add_command(cmd)
-
- cmd = CommandInfo(name="diff", desc="Show all local changes that have not been committed.")
- module.add_command(cmd)
-
- cmd = CommandInfo(name="revert", desc="Revert all local changes.")
- module.add_command(cmd)
-
- cmd = CommandInfo(name="commit", desc="Commit all local changes.")
- module.add_command(cmd)
-
- cmd = CommandInfo(name="go", desc="Go to a specific configuration part.")
- param = ParamInfo(name=CFGITEM_IDENTIFIER_PARAM, type="string",
- optional=False, desc=DEFAULT_IDENTIFIER_DESC)
- cmd.add_param(param)
- module.add_command(cmd)
-
- tool.add_module_info(module)
-
-def check_port(option, opt_str, value, parser):
- if (value < 0) or (value > 65535):
- raise OptionValueError('%s requires a port number (0-65535)' % opt_str)
- parser.values.port = value
-
-def check_addr(option, opt_str, value, parser):
- ipstr = value
- ip_family = socket.AF_INET
- if (ipstr.find(':') != -1):
- ip_family = socket.AF_INET6
-
- try:
- socket.inet_pton(ip_family, ipstr)
- except:
- raise OptionValueError("%s invalid ip address" % ipstr)
-
- parser.values.addr = value
-
-def set_bindctl_options(parser):
- parser.add_option('-p', '--port', dest='port', type='int',
- action='callback', callback=check_port,
- default='8080', help='port for cmdctl of bind10')
-
- parser.add_option('-a', '--address', dest='addr', type='string',
- action='callback', callback=check_addr,
- default='127.0.0.1', help='IP address for cmdctl of bind10')
-
- parser.add_option('-c', '--certificate-chain', dest='cert_chain',
- type='string', action='store',
- help='PEM formatted server certificate validation chain file')
-
- parser.add_option('--csv-file-dir', dest='csv_file_dir', type='string',
- default=None, action='store',
- help='Directory to store the password CSV file')
-
-def main():
- parser = OptionParser(version = VERSION)
- set_bindctl_options(parser)
- (options, args) = parser.parse_args()
- server_addr = options.addr + ':' + str(options.port)
- tool = BindCmdInterpreter(server_addr, pem_file=options.cert_chain,
- csv_file_dir=options.csv_file_dir)
- prepare_config_commands(tool)
- command_sets.prepare_execute_commands(tool)
- result = tool.run()
- sys.exit(result)
-
-if __name__ == '__main__':
- isc.util.traceback_handler.traceback_handler(main)
+++ /dev/null
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-
-"""This module holds the command parser class for bindctl"""
-
-import re
-from bindctl.exception import *
-try:
- from collections import OrderedDict
-except ImportError:
- from bindctl.mycollections import OrderedDict
-
-param_name_str = "^\s*(?P<param_name>[\w]+)\s*=\s*"
-
-# The value string can be a sequence without space or comma
-# characters, or a string surroundedby quotation marks(such marks
-# can be part of string in an escaped form)
-#param_value_str = "(?P<param_value>[\"\'].+?(?<!\\\)[\"\']|[^\'\"][^, ]+)"
-param_value_str = "(?P<param_value>[^\'\" ][^, ]+)"
-param_value_with_quota_str = "[\"\'](?P<param_value>.+?)(?<!\\\)[\"\']"
-next_params_str = "(?P<blank>\s*)(?P<comma>,?)(?P<next_params>.*)$"
-
-
-PARAM_WITH_QUOTA_PATTERN = re.compile(param_name_str +
- param_value_with_quota_str +
- next_params_str)
-PARAM_PATTERN = re.compile(param_name_str + param_value_str + next_params_str)
-# Used for module and command name
-NAME_PATTERN = re.compile("^\s*(?P<name>[\w]+)(?P<blank>\s*)(?P<others>.*)$")
-
-# this removes all whitespace in the given string, except when
-# between " quotes
-_remove_unquoted_whitespace = \
- lambda text:'"'.join( it if i%2 else ''.join(it.split())
- for i,it in enumerate(text.split('"')) )
-
-
-def _remove_list_and_map_whitespace(text):
- """Returns a string where the whitespace between matching [ and ]
- is removed, unless quoted"""
- # regular expression aren't really the right tool, since we may have
- # nested structures
- result = []
- start_pos = 0
- pos = 0
- list_count = 0
- map_count = 0
- cur_start_list_pos = None
- cur_start_map_pos = None
- for i in text:
- if i == '[' and map_count == 0:
- if list_count == 0:
- result.append(text[start_pos:pos + 1])
- cur_start_list_pos = pos + 1
- list_count = list_count + 1
- elif i == ']' and map_count == 0:
- if list_count > 0:
- list_count = list_count - 1
- if list_count == 0:
- result.append(_remove_unquoted_whitespace(text[cur_start_list_pos:pos + 1]))
- start_pos = pos + 1
- if i == '{' and list_count == 0:
- if map_count == 0:
- result.append(text[start_pos:pos + 1])
- cur_start_map_pos = pos + 1
- map_count = map_count + 1
- elif i == '}' and list_count == 0:
- if map_count > 0:
- map_count = map_count - 1
- if map_count == 0:
- result.append(_remove_unquoted_whitespace(text[cur_start_map_pos:pos + 1]))
- start_pos = pos + 1
-
-
- pos = pos + 1
- if start_pos <= len(text):
- result.append(text[start_pos:len(text)])
- return "".join(result)
-
-
-class BindCmdParser:
- """ This class will parse the command line user input into three parts:
- module name, command, parameters
- the first two parts are strings and parameter is one hash,
- parameters part is optional
-
- Example: zone reload, zone_name=example.com
- module == zone
- command == reload
- params == [zone_name = 'example.com']
- """
-
- def __init__(self, cmd):
- self.params = OrderedDict()
- self.module = ''
- self.command = ''
- self._parse_cmd(cmd)
-
- def _parse_cmd(self, text_str):
- '''Parse command line. '''
- # Get module name
- groups = NAME_PATTERN.match(text_str)
- if not groups:
- raise CmdModuleNameFormatError
-
- self.module = groups.group('name')
- cmd_str = groups.group('others')
- if cmd_str:
- if not groups.group('blank'):
- raise CmdModuleNameFormatError
- else:
- raise CmdMissCommandNameFormatError(self.module)
-
- # Get command name
- groups = NAME_PATTERN.match(cmd_str)
- if (not groups):
- raise CmdCommandNameFormatError(self.module)
-
- self.command = groups.group('name')
- param_str = groups.group('others')
- if param_str:
- if not groups.group('blank'):
- raise CmdCommandNameFormatError(self.module)
-
- self._parse_params(param_str)
-
- def _remove_list_whitespace(self, text):
- return ""
-
- def _parse_params(self, param_text):
- """convert a=b,c=d into one hash """
- param_text = _remove_list_and_map_whitespace(param_text)
-
- # Check parameter name "help"
- param = NAME_PATTERN.match(param_text)
- if param and param.group('name') == "help":
- self.params["help"] = "help"
- return
-
- while True:
- if not param_text.strip():
- break
-
- groups = PARAM_PATTERN.match(param_text) or \
- PARAM_WITH_QUOTA_PATTERN.match(param_text)
- if not groups:
- # ok, fill in the params in the order entered
- params = re.findall("([^\" ]+|\".*\")", param_text)
- i = 0
- for p in params:
- self.params[i] = p
- i += 1
- break
- else:
- self.params[groups.group('param_name')] = groups.group('param_value')
- param_text = groups.group('next_params')
- if not param_text or (not param_text.strip()):
- break
-
- if not groups.group('blank') and \
- not groups.group('comma'):
- raise CmdParamFormatError(self.module, self.command)
+++ /dev/null
-# Copyright (C) 2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# This file provides a built-in set of 'execute' commands, for common
-# functions, such as adding an initial auth server.
-# By calling the function prepare_execute_commands, the
-# commands in the command_sets map are added to the virtual
-# component called 'execute'. This is done in bindctl_main.
-
-from bindctl.moduleinfo import *
-# The name of the 'virtual' command set execution module in bindctl
-EXECUTE_MODULE_NAME = 'execute'
-
-# This is a map of command names to lists
-# Each element in the set should itself be a dict containing:
-# 'description': A string with a description of the command set
-# 'commands': A list of bindctl commands
-command_sets = {
- 'init_authoritative_server': {
- 'description':
- 'Configure and run a basic Authoritative server, with default '+
- 'SQLite3 backend, and xfrin and xfrout functionality',
- 'commands':
- [
- '!echo adding Authoritative server component',
- 'config add /Init/components b10-auth',
- 'config set /Init/components/b10-auth/kind needed',
- 'config set /Init/components/b10-auth/special auth',
- '!echo adding Xfrin component',
- 'config add /Init/components b10-xfrin',
- 'config set /Init/components/b10-xfrin/address Xfrin',
- 'config set /Init/components/b10-xfrin/kind dispensable',
- '!echo adding Xfrout component',
- 'config add /Init/components b10-xfrout',
- 'config set /Init/components/b10-xfrout/address Xfrout',
- 'config set /Init/components/b10-xfrout/kind dispensable',
- '!echo adding Zone Manager component',
- 'config add /Init/components b10-zonemgr',
- 'config set /Init/components/b10-zonemgr/address Zonemgr',
- 'config set /Init/components/b10-zonemgr/kind dispensable',
- '!echo Components added. Please enter "config commit" to',
- '!echo finalize initial setup and run the components.'
- ]
- }
-}
-
-def has_command_set(name):
- return name in command_sets
-
-def get_commands(name):
- return command_sets[name]['commands']
-
-def get_description(name):
- return command_sets[name]['description']
-
-# For each
-def prepare_execute_commands(tool):
- """This function is called by bindctl_main, and sets up the commands
- defined here for use in bindctl."""
- # common parameter
- param_show = ParamInfo(name="show", type="string", optional=True,
- desc="Show the list of commands without executing them")
-
- # The command module
- module = ModuleInfo(name=EXECUTE_MODULE_NAME,
- desc="Execute a given set of commands")
-
- # Command to execute a file
- cmd = CommandInfo(name="file", desc="Read commands from file")
- param = ParamInfo(name="filename", type="string", optional=False,
- desc="File to read the set of commands from.")
- cmd.add_param(param)
- cmd.add_param(param_show)
- module.add_command(cmd)
-
- # and loop through all command sets defined above
- for name in command_sets:
- cmd = CommandInfo(name=name, desc=get_description(name))
- cmd.add_param(param_show)
- module.add_command(cmd)
-
- tool.add_module_info(module)
+++ /dev/null
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""This module holds exception classes specific for bindctl"""
-class BindCtlException(Exception):
- """Abstract base class shared by all bindctl exceptions"""
- def __str__(self):
- return "Big tool has problem"
-
-# Begin define Format exception
-
-class CmdFormatError(BindCtlException):
- """Command is malformed"""
- def __str__(self):
- return "Command is malformed"
-
-
-class CmdModuleNameFormatError(CmdFormatError):
- """module name format error"""
-
- def __str__(self):
- return "Module name format error: the character of name can only be '0-9a-zA-Z_'"
-
-
-class CmdCommandNameFormatError(CmdFormatError):
- """command name format error"""
-
- def __init__(self, module):
- self.module = module
-
- def __str__(self):
- return "Command name format error: the character of name can only be '0-9a-zA-Z_'"
-
-
-class CmdMissCommandNameFormatError(CmdFormatError):
- """Module name isn't finished"""
-
- def __init__(self, module):
- self.module = module
-
- def __str__(self):
- return "command name is missed"
-
-
-class CmdParamFormatError(CmdFormatError):
- """Command is malformed which parameter isn't key value pair"""
-
- def __init__(self, module, command):
- self.module = module
- self.command = command
-
- def __str__(self):
- return "Parameter format error, it should be 'key = value'"
-
-# Begin define the exception for syntax
-
-class CmdSyntaxError(BindCtlException):
- """Command line has syntax error"""
-
- def __str__(self):
- return "Command line has syntax error"
-
-
-class CmdUnknownModuleSyntaxError(CmdSyntaxError):
- """Command is unknown"""
- def __init__(self, module):
- self.module = module
-
- def __str__(self):
- return str("Unknown module '%s'" % self.module)
-
-
-class CmdUnknownCmdSyntaxError(CmdSyntaxError):
- """Command is unknown"""
- def __init__(self, module, command):
- self.module = module
- self.command = command
-
- def __str__(self):
- return str("Unknown command '%s' to module '%s'" %
- (self.command, self.module))
-
-
-class CmdUnknownParamSyntaxError(CmdSyntaxError):
- """The parameter of command is unknown"""
- def __init__(self, module, command, param):
- self.module = module
- self.command = command
- self.param = param
-
- def __str__(self):
- return str("Unknown parameter '%s' to command '%s' of module '%s'" %
- (self.param, self.command, self.module))
-
-
-class CmdMissParamSyntaxError(CmdSyntaxError):
- """The parameter of one command is missed"""
- def __init__(self, module, command, param):
- self.module = module
- self.command = command
- self.param = param
-
- def __str__(self):
- return str("Parameter '%s' is missed for command '%s' of module '%s'" %
- (self.param, self.command, self.module))
-
-
-class FailToLogin(BindCtlException):
- def __str__(self):
- return "Fail to login to cmdctl"
-
-
+++ /dev/null
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""This module holds classes representing modules, commands and
- parameters for use in bindctl"""
-
-import textwrap
-
-try:
- from collections import OrderedDict
-except ImportError:
- from bindctl.mycollections import OrderedDict
-
-# Define value type
-STRING_TYPE = "string"
-LIST_TYPE = "list"
-INT_TYPE = "int"
-
-MODULE_NODE_NAME = 'module'
-COMMAND_NODE_NAME = 'command'
-PARAM_NODE_NAME = 'param'
-
-# this is used to align the descriptions in help output
-CONST_BINDCTL_HELP_INDENT_WIDTH=12
-
-
-class ParamInfo:
- """One parameter of one command.
- Each command parameter has five attributes:
- parameter name, parameter type, parameter value,
- parameter description and paramter's spec(got from
- module spec file).
- """
- def __init__(self, name, desc = '', type = STRING_TYPE,
- optional = False, value = '', default_value = '',
- param_spec = None):
- self.name = name
- self.type = type
- self.value = value
- self.default_value = default_value
- self.desc = desc
- self.is_optional = optional
- self.param_spec = param_spec
-
- def __str__(self):
- return str("\t%s <type: %s> \t(%s)" % (self.name, self.type, self.desc))
-
- def get_basic_info(self):
- if self.is_optional:
- opt_str = "optional"
- else:
- opt_str = "mandatory"
- return "%s (%s, %s)" % (self.name, self.type, opt_str)
-
- def get_desc(self):
- return self.desc
-
-class CommandInfo:
- """One command which is provided by one bind10 module, it has zero
- or more parameters
- """
-
- def __init__(self, name, desc = ""):
- self.name = name
- self.desc = desc
- self.params = OrderedDict()
- # Set default parameter "help"
- self.add_param(ParamInfo("help",
- desc = "Get help for command.",
- optional = True))
-
- def __str__(self):
- return str("%s \t(%s)" % (self.name, self.desc))
-
- def get_name(self):
- return self.name
-
- def get_desc(self):
- return self.desc;
-
- def add_param(self, paraminfo):
- """Add a ParamInfo object to this CommandInfo"""
- self.params[paraminfo.name] = paraminfo
-
-
- def has_param_with_name(self, param_name):
- """Returns true if the parameter with param_name exists"""
- return param_name in self.params
-
-
- def get_param_with_name(self, param_name):
- """Returns the ParamInfo with the given name. Raises a
- KeyError if it doesn't exist"""
- return self.params[param_name]
-
-
- def get_params(self):
- """Returns a list of all ParamInfo objects for this CommandInfo"""
- return list(self.params.values())
-
-
- def get_param_names(self):
- """Returns a list of the names of all parameters for this command"""
- return list(self.params.keys())
-
-
- def get_mandatory_param_names(self):
- """Returns a list of the names of all mandatory parameters for
- this command"""
- all_names = self.params.keys()
- return [name for name in all_names
- if not self.params[name].is_optional]
-
- def get_param_name_by_position(self, pos, param_count):
- """
- Find a proper parameter name for the position 'pos':
- If param_count is equal to the count of mandatory parameters of command,
- and there is some optional parameter, find the first mandatory parameter
- from the position 'pos' to the end. Else, return the name on position pos.
- (This function will be changed if bindctl command line syntax is changed
- in the future. )
- """
- if type(pos) != int:
- raise KeyError(str(pos) + " is not an integer")
-
- else:
- params = self.params.copy()
- del params['help']
- count = len(params)
- if (pos >= count):
- raise KeyError(str(pos) + " out of range")
-
- mandatory_count = len(self.get_mandatory_param_names())
- param_names = list(params.keys())
- if (param_count == mandatory_count) and (param_count < count):
- while pos < count:
- if not params[param_names[pos]].is_optional:
- return param_names[pos]
- pos += 1
-
- raise KeyError(str(pos) + "parameters have error")
- else:
- return param_names[pos]
-
-
- def command_help(self):
- """Prints the help info for this command to stdout"""
- print("Command ", self)
- print("\t\thelp (Get help for command)")
-
- params = self.params.copy()
- del params["help"]
-
- if len(params) == 0:
- print("This command has no parameters")
- return
-
- print("Parameters:")
- for info in params.values():
- print(" %s" % info.get_basic_info())
- description = info.get_desc()
- if description != "":
- print(textwrap.fill(description,
- initial_indent=" ",
- subsequent_indent=" ",
- width=70))
-
-class ModuleInfo:
- """Define the information of one module, include module name,
- module supporting commands.
- """
-
- def __init__(self, name, desc = ""):
- self.name = name
- self.desc = desc
- self.commands = OrderedDict()
- self.add_command(CommandInfo(name = "help",
- desc = "Get help for module."))
-
- def __str__(self):
- return str("%s \t%s" % (self.name, self.desc))
-
- def get_name(self):
- return self.name
-
- def get_desc(self):
- return self.desc
-
- def add_command(self, command_info):
- """Add a CommandInfo to this ModuleInfo."""
- self.commands[command_info.name] = command_info
-
- def has_command_with_name(self, command_name):
- """Returns true if this module has a command with the given name."""
- return command_name in self.commands
-
- def get_command_with_name(self, command_name):
- """Returns the CommandInfo for the command with the given name.
- Raises a KeyError if not found"""
- return self.commands[command_name]
-
- def get_commands(self):
- """Returns a list of all CommandInfo objects for this module."""
- return list(self.commands.values())
-
- def get_command_names(self):
- """Returns a list of the names of all commands for this module."""
- return list(self.commands.keys())
-
- def module_help(self):
- """Prints the help info for this module to stdout"""
- print("Module " + str(self))
- print("Available commands:")
- for k in self.commands.values():
- n = k.get_name()
- if len(n) >= CONST_BINDCTL_HELP_INDENT_WIDTH:
- print(" %s" % n)
- print(textwrap.fill(k.get_desc(),
- initial_indent=" ",
- subsequent_indent=" " +
- " " * CONST_BINDCTL_HELP_INDENT_WIDTH,
- width=70))
- else:
- print(textwrap.fill("%s%s%s" %
- (k.get_name(),
- " "*(CONST_BINDCTL_HELP_INDENT_WIDTH - len(k.get_name())),
- k.get_desc()),
- initial_indent=" ",
- subsequent_indent=" " +
- " " * CONST_BINDCTL_HELP_INDENT_WIDTH,
- width=70))
-
- def command_help(self, command):
- """Prints the help info for the command with the given name.
- Raises KeyError if not found"""
- self.commands[command].command_help()
-
+++ /dev/null
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from collections import MutableMapping
-
-class OrderedDict(dict, MutableMapping):
-
- # Methods with direct access to underlying attributes
-
- def __init__(self, *args, **kwds):
- if len(args) > 1:
- raise TypeError('expected at 1 argument, got %d', len(args))
- if not hasattr(self, '_keys'):
- self._keys = []
- self.update(*args, **kwds)
-
- def clear(self):
- del self._keys[:]
- dict.clear(self)
-
- def __setitem__(self, key, value):
- if key not in self:
- self._keys.append(key)
- dict.__setitem__(self, key, value)
-
- def __delitem__(self, key):
- dict.__delitem__(self, key)
- self._keys.remove(key)
-
- def __iter__(self):
- return iter(self._keys)
-
- def __reversed__(self):
- return reversed(self._keys)
-
- def popitem(self):
- if not self:
- raise KeyError
- key = self._keys.pop()
- value = dict.pop(self, key)
- return key, value
-
- def __reduce__(self):
- items = [[k, self[k]] for k in self]
- inst_dict = vars(self).copy()
- inst_dict.pop('_keys', None)
- return (self.__class__, (items,), inst_dict)
-
- # Methods with indirect access via the above methods
-
- setdefault = MutableMapping.setdefault
- update = MutableMapping.update
- pop = MutableMapping.pop
- keys = MutableMapping.keys
- values = MutableMapping.values
- items = MutableMapping.items
-
- def __repr__(self):
- pairs = ', '.join(map('%r: %r'.__mod__, self.items()))
- return '%s({%s})' % (self.__class__.__name__, pairs)
-
- def copy(self):
- return self.__class__(self)
-
- @classmethod
- def fromkeys(cls, iterable, value=None):
- d = cls()
- for key in iterable:
- d[key] = value
- return d
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-BINDCTL_PATH=@abs_top_builddir@/src/bin/bindctl
-
-PYTHONPATH=@abs_top_srcdir@/src/bin:@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python/isc/cc:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/bin:@abs_top_srcdir@/src/lib/python
-export PYTHONPATH
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-SET_ENV_LIBRARY_PATH=@SET_ENV_LIBRARY_PATH@
-if test $SET_ENV_LIBRARY_PATH = yes; then
- @ENV_LIBRARY_PATH@=@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/cryptolink/.libs:@abs_top_builddir@/src/lib/cc/.libs:@abs_top_builddir@/src/lib/config/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/.libs:@abs_top_builddir@/src/lib/util/threads/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/exceptions/.libs:@abs_top_builddir@/src/lib/datasrc/.libs:$@ENV_LIBRARY_PATH@
- export @ENV_LIBRARY_PATH@
-fi
-
-B10_FROM_SOURCE=@abs_top_srcdir@
-export B10_FROM_SOURCE
-
-BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
-export BIND10_MSGQ_SOCKET_FILE
-
-cd ${BINDCTL_PATH}
-exec ${PYTHON_EXEC} -O bindctl "$@"
+++ /dev/null
-/bindctl_test
+++ /dev/null
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = bindctl_test.py cmdparse_test.py
-EXTRA_DIST = $(PYTESTS)
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/bindctl:$(abs_top_srcdir)/src/bin \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-BINDCTL_TEST_PATH=@abs_top_srcdir@/src/bin/bindctl/tests
-PYTHONPATH=@abs_top_srcdir@/src/lib/python:@abs_top_srcdir@/src/bin
-export PYTHONPATH
-
-cd ${BINDCTL_TEST_PATH}
-exec ${PYTHON_EXEC} -O bindctl_test.py $*
-
+++ /dev/null
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-
-import unittest
-import isc.cc.data
-import os
-import io
-import errno
-import sys
-import socket
-import ssl
-import http.client
-import pwd
-import getpass
-import re
-import json
-from optparse import OptionParser
-from isc.config.config_data import ConfigData, MultiConfigData
-from isc.config.module_spec import ModuleSpec
-from isc.testutils.parse_args import TestOptParser, OptsError
-from bindctl_main import set_bindctl_options
-from bindctl import cmdparse
-from bindctl import bindcmd
-from bindctl.moduleinfo import *
-from bindctl.exception import *
-try:
- from collections import OrderedDict
-except ImportError:
- from mycollections import OrderedDict
-
-class TestCmdLex(unittest.TestCase):
-
- def my_assert_raise(self, exception_type, cmd_line):
- self.assertRaises(exception_type, cmdparse.BindCmdParser, cmd_line)
-
-
- def testCommandWithoutParameter(self):
- cmd_parser = cmdparse.BindCmdParser("zone add")
- assert cmd_parser.module == "zone"
- assert cmd_parser.command == "add"
- self.assertEqual(len(cmd_parser.params), 0)
-
-
- def testCommandWithParameters(self):
- lines = {"zone add zone_name = cnnic.cn, file = cnnic.cn.file master=1.1.1.1",
- "zone add zone_name = \"cnnic.cn\", file ='cnnic.cn.file' master=1.1.1.1 ",
- "zone add zone_name = 'cnnic.cn\", file ='cnnic.cn.file' master=1.1.1.1, " }
-
- for cmd_line in lines:
- cmd_parser = cmdparse.BindCmdParser(cmd_line)
- assert cmd_parser.module == "zone"
- assert cmd_parser.command == "add"
- assert cmd_parser.params["zone_name"] == "cnnic.cn"
- assert cmd_parser.params["file"] == "cnnic.cn.file"
- assert cmd_parser.params["master"] == '1.1.1.1'
-
- def testCommandWithParamters_2(self):
- '''Test whether the parameters in key=value can be parsed properly.'''
- cmd_parser = cmdparse.BindCmdParser('zone cmd name = 1:34::2')
- self.assertEqual(cmd_parser.params['name'], '1:34::2')
-
- cmd_parser = cmdparse.BindCmdParser('zone cmd name = 1\"\'34**&2'
- ' value=44\"\'\"')
- self.assertEqual(cmd_parser.params['name'], '1\"\'34**&2')
- self.assertEqual(cmd_parser.params['value'], '44\"\'\"')
-
- cmd_parser = cmdparse.BindCmdParser('zone cmd name = 1\"\'34**&2'
- ',value= 44\"\'\"')
- self.assertEqual(cmd_parser.params['name'], '1\"\'34**&2')
- self.assertEqual(cmd_parser.params['value'], '44\"\'\"')
-
- cmd_parser = cmdparse.BindCmdParser('zone cmd name = 1\'34**&2'
- 'value=44\"\'\" value = '
- '\"==============\'')
- self.assertEqual(cmd_parser.params['name'], '1\'34**&2value=44\"\'\"')
- self.assertEqual(cmd_parser.params['value'], '==============')
-
- cmd_parser = cmdparse.BindCmdParser('zone cmd name = \"1234, '
- '567890 \" value ==&*/')
- self.assertEqual(cmd_parser.params['name'], '1234, 567890 ')
- self.assertEqual(cmd_parser.params['value'], '=&*/')
-
- def testCommandWithListParam(self):
- cmd_parser = cmdparse.BindCmdParser("zone set zone_name='cnnic.cn', "
- "master='1.1.1.1, 2.2.2.2'")
- assert cmd_parser.params["master"] == '1.1.1.1, 2.2.2.2'
-
- def testCommandWithHelpParam(self):
- cmd_parser = cmdparse.BindCmdParser("zone add help")
- assert cmd_parser.params["help"] == "help"
-
- cmd_parser = cmdparse.BindCmdParser("zone add help *&)&)*&&$#$^%")
- assert cmd_parser.params["help"] == "help"
- self.assertEqual(len(cmd_parser.params), 1)
-
-
- def testCmdModuleNameFormatError(self):
- self.my_assert_raise(CmdModuleNameFormatError, "zone=good")
- self.my_assert_raise(CmdModuleNameFormatError, "zo/ne")
- self.my_assert_raise(CmdModuleNameFormatError, "")
- self.my_assert_raise(CmdModuleNameFormatError, "=zone")
- self.my_assert_raise(CmdModuleNameFormatError, "zone,")
-
-
- def testCmdMissCommandNameFormatError(self):
- self.my_assert_raise(CmdMissCommandNameFormatError, "zone")
- self.my_assert_raise(CmdMissCommandNameFormatError, "zone ")
- self.my_assert_raise(CmdMissCommandNameFormatError, "help ")
-
-
- def testCmdCommandNameFormatError(self):
- self.my_assert_raise(CmdCommandNameFormatError, "zone =d")
- self.my_assert_raise(CmdCommandNameFormatError, "zone z=d")
- self.my_assert_raise(CmdCommandNameFormatError, "zone z-d ")
- self.my_assert_raise(CmdCommandNameFormatError, "zone zdd/")
- self.my_assert_raise(CmdCommandNameFormatError, "zone zdd/ \"")
-
-class TestCmdSyntax(unittest.TestCase):
-
- def _create_bindcmd(self):
- """Create one bindcmd"""
-
- tool = bindcmd.BindCmdInterpreter()
- string_spec = { 'item_type' : 'string',
- 'item_optional' : False,
- 'item_default' : ''}
- int_spec = { 'item_type' : 'integer',
- 'item_optional' : False,
- 'item_default' : 10}
- zone_file_param = ParamInfo(name = "zone_file",
- param_spec = string_spec)
- zone_name = ParamInfo(name = 'zone_name', param_spec = string_spec)
- load_cmd = CommandInfo(name = "load")
- load_cmd.add_param(zone_file_param)
- load_cmd.add_param(zone_name)
-
- param_master = ParamInfo(name = "master", optional = True,
- param_spec = string_spec)
- param_master = ParamInfo(name = "port", optional = True,
- param_spec = int_spec)
- param_allow_update = ParamInfo(name = "allow_update",
- optional = True,
- param_spec = string_spec)
- set_cmd = CommandInfo(name = "set")
- set_cmd.add_param(param_master)
- set_cmd.add_param(param_allow_update)
- set_cmd.add_param(zone_name)
-
- reload_all_cmd = CommandInfo(name = "reload_all")
-
- zone_module = ModuleInfo(name = "zone")
- zone_module.add_command(load_cmd)
- zone_module.add_command(set_cmd)
- zone_module.add_command(reload_all_cmd)
-
- tool.add_module_info(zone_module)
- return tool
-
-
- def setUp(self):
- self.bindcmd = self._create_bindcmd()
-
-
- def no_assert_raise(self, cmd_line):
- cmd_parser = cmdparse.BindCmdParser(cmd_line)
- self.bindcmd._validate_cmd(cmd_parser)
-
-
- def my_assert_raise(self, exception_type, cmd_line):
- cmd_parser = cmdparse.BindCmdParser(cmd_line)
- self.assertRaises(exception_type, self.bindcmd._validate_cmd,
- cmd_parser)
-
-
- def testValidateSuccess(self):
- self.no_assert_raise("zone load zone_file='cn' zone_name='cn'")
- self.no_assert_raise("zone load zone_file='cn', zone_name='cn', ")
- self.no_assert_raise("zone help ")
- self.no_assert_raise("zone load help ")
- self.no_assert_raise("zone help help='dd' ")
- self.no_assert_raise("zone set allow_update='1.1.1.1' zone_name='cn'")
- self.no_assert_raise("zone set zone_name='cn'")
- self.my_assert_raise(isc.cc.data.DataTypeError,
- "zone set zone_name ='cn', port='cn'")
- self.no_assert_raise("zone reload_all")
-
- def testCmdUnknownModuleSyntaxError(self):
- self.my_assert_raise(CmdUnknownModuleSyntaxError, "zoned d")
- self.my_assert_raise(CmdUnknownModuleSyntaxError, "dd dd ")
-
- def testCmdUnknownCmdSyntaxError(self):
- self.my_assert_raise(CmdUnknownCmdSyntaxError, "zone dd")
-
- def testCmdMissParamSyntaxError(self):
- self.my_assert_raise(CmdMissParamSyntaxError,
- "zone load zone_file='cn'")
- self.my_assert_raise(CmdMissParamSyntaxError,
- "zone load zone_name='cn'")
- self.my_assert_raise(CmdMissParamSyntaxError,
- "zone set allow_update='1.1.1.1'")
- self.my_assert_raise(CmdMissParamSyntaxError,
- "zone set ")
-
- def testCmdUnknownParamSyntaxError(self):
- self.my_assert_raise(CmdUnknownParamSyntaxError,
- "zone load zone_d='cn'")
- self.my_assert_raise(CmdUnknownParamSyntaxError,
- "zone reload_all zone_name = 'cn'")
- self.my_assert_raise(CmdUnknownParamSyntaxError,
- "zone help a b c")
-
-class TestModuleInfo(unittest.TestCase):
-
- def test_get_param_name_by_position(self):
- cmd = CommandInfo('command')
- cmd.add_param(ParamInfo('name'))
- cmd.add_param(ParamInfo('age'))
- cmd.add_param(ParamInfo('data', optional = True))
- cmd.add_param(ParamInfo('sex'))
- self.assertEqual('name', cmd.get_param_name_by_position(0, 2))
- self.assertEqual('age', cmd.get_param_name_by_position(1, 2))
- self.assertEqual('sex', cmd.get_param_name_by_position(2, 3))
- self.assertEqual('data', cmd.get_param_name_by_position(2, 4))
- self.assertEqual('data', cmd.get_param_name_by_position(2, 4))
-
- self.assertRaises(KeyError, cmd.get_param_name_by_position, 4, 4)
-
-
-
-class TestNameSequence(unittest.TestCase):
- """
- Test if the module/command/parameters is saved in the order creation
- """
-
- def _create_bindcmd(self):
- """Create one bindcmd"""
-
- self._cmd = CommandInfo(name = "load")
- self.module = ModuleInfo(name = "zone")
- self.tool = bindcmd.BindCmdInterpreter()
- for random_str in self.random_names:
- self._cmd.add_param(ParamInfo(name = random_str))
- self.module.add_command(CommandInfo(name = random_str))
- self.tool.add_module_info(ModuleInfo(name = random_str))
-
- def setUp(self):
- self.random_names = ['1erdfeDDWsd', '3fe', '2009erd',
- 'Fe231', 'tere142', 'rei8WD']
- self._create_bindcmd()
-
- def testSequence(self):
- param_names = self._cmd.get_param_names()
- cmd_names = self.module.get_command_names()
- module_names = self.tool.get_module_names()
-
- i = 0
- while i < len(self.random_names):
- assert self.random_names[i] == param_names[i+1]
- assert self.random_names[i] == cmd_names[i+1]
- assert self.random_names[i] == module_names[i+1]
- i = i + 1
-
-# tine class to fake a UIModuleCCSession, but only the config data
-# parts for the next set of tests
-class FakeCCSession(MultiConfigData):
- def __init__(self):
- self._local_changes = {}
- self._current_config = {}
- self._specifications = {}
- self.add_foo_spec()
-
- def add_foo_spec(self):
- spec = { "module_name": "foo",
- "config_data": [
- { "item_name": "an_int",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 1
- },
- { "item_name": "a_list",
- "item_type": "list",
- "item_optional": False,
- "item_default": [],
- "list_item_spec":
- { "item_name": "a_string",
- "item_type": "string",
- "item_optional": False,
- "item_default": "bar"
- }
- }
- ]
- }
- self.set_specification(ModuleSpec(spec))
-
-
-# fake socket
-class FakeSocket():
- def __init__(self):
- self.run = True
-
- def connect(self, to):
- if not self.run:
- raise socket.error
-
- def close(self):
- self.run = False
-
- def send(self, data):
- if not self.run:
- raise socket.error
- return len(data)
-
- def makefile(self, type):
- return self
-
- def sendall(self, data):
- if not self.run:
- raise socket.error
- return len(data)
-
-
-class TestConfigCommands(unittest.TestCase):
- def setUp(self):
- self.tool = bindcmd.BindCmdInterpreter()
- mod_info = ModuleInfo(name = "foo")
- self.tool.add_module_info(mod_info)
- self.tool.config_data = FakeCCSession()
- self.stdout_backup = sys.stdout
- self.printed_messages = []
- self.tool._print = self.store_print
-
- def test_precmd(self):
- def update_all_modules_info():
- raise socket.error
- def precmd(line):
- self.tool.precmd(line)
- self.tool._update_all_modules_info = update_all_modules_info
- # If line is equals to 'EOF', _update_all_modules_info()
- # shouldn't be called
- precmd('EOF')
- self.assertRaises(socket.error, precmd, 'continue')
-
- def store_print(self, *args):
- '''Method to override _print in BindCmdInterpreter.
- Instead of printing the values, appends the argument tuple
- to the list in self.printed_messages'''
- self.printed_messages.append(" ".join(map(str, args)))
-
- def __check_printed_message(self, expected_message, printed_message):
- self.assertIsNotNone(re.match(expected_message, printed_message),
- "Printed message '" + printed_message +
- "' does not match '" + expected_message + "'")
-
- def __check_printed_messages(self, expected_messages):
- '''Helper test function to check the printed messages against a list
- of regexps'''
- self.assertEqual(len(expected_messages), len(self.printed_messages))
- for _ in map(self.__check_printed_message,
- expected_messages,
- self.printed_messages):
- pass
-
- def test_try_login(self):
- # Make sure __try_login raises the correct exception
- # upon failure of either send_POST or the read() on the
- # response
-
- orig_send_POST = self.tool.send_POST
- expected_printed_messages = []
- try:
- def send_POST_raiseImmediately(self, params):
- raise socket.error("test error")
-
- self.tool.send_POST = send_POST_raiseImmediately
- self.assertRaises(FailToLogin, self.tool._try_login, "foo", "bar")
- expected_printed_messages.append(
- 'Error while sending login information: test error')
- self.__check_printed_messages(expected_printed_messages)
-
- def create_send_POST_raiseOnRead(exception):
- '''Create a replacement send_POST() method that raises
- the given exception when read() is called on the value
- returned from send_POST()'''
- def send_POST_raiseOnRead(self, params):
- class MyResponse:
- def read(self):
- raise exception
- return MyResponse()
- return send_POST_raiseOnRead
-
- # basic socket error
- self.tool.send_POST =\
- create_send_POST_raiseOnRead(socket.error("read error"))
- self.assertRaises(FailToLogin, self.tool._try_login, "foo", "bar")
- expected_printed_messages.append(
- 'Error while sending login information: read error')
- self.__check_printed_messages(expected_printed_messages)
-
- # connection reset
- exc = socket.error("connection reset")
- exc.errno = errno.ECONNRESET
- self.tool.send_POST =\
- create_send_POST_raiseOnRead(exc)
- self.assertRaises(FailToLogin, self.tool._try_login, "foo", "bar")
- expected_printed_messages.append(
- 'Error while sending login information: connection reset')
- self.__check_printed_messages(expected_printed_messages)
-
- # 'normal' SSL error
- exc = ssl.SSLError()
- self.tool.send_POST =\
- create_send_POST_raiseOnRead(exc)
- self.assertRaises(FailToLogin, self.tool._try_login, "foo", "bar")
- expected_printed_messages.append(
- 'Error while sending login information: .*')
- self.__check_printed_messages(expected_printed_messages)
-
- # 'EOF' SSL error
- exc = ssl.SSLError()
- exc.errno = ssl.SSL_ERROR_EOF
- self.tool.send_POST =\
- create_send_POST_raiseOnRead(exc)
- self.assertRaises(FailToLogin, self.tool._try_login, "foo", "bar")
- expected_printed_messages.append(
- 'Error while sending login information: .*')
- self.__check_printed_messages(expected_printed_messages)
-
- # any other exception should be passed through
- self.tool.send_POST =\
- create_send_POST_raiseOnRead(ImportError())
- self.assertRaises(ImportError, self.tool._try_login, "foo", "bar")
- self.__check_printed_messages(expected_printed_messages)
-
- finally:
- self.tool.send_POST = orig_send_POST
-
- def test_try_login_calls_cmdctl(self):
- # Make sure _try_login() makes the right API call to cmdctl.
- orig_conn = self.tool.conn
- try:
- class MyConn:
- def __init__(self):
- self.method = None
- self.url = None
- self.param = None
- self.headers = None
-
- def request(self, method, url, param, headers):
- self.method = method
- self.url = url
- self.param = param
- self.headers = headers
-
- def getresponse(self):
- class MyResponse:
- def __init__(self):
- self.status = http.client.OK
- def read(self):
- class MyData:
- def decode(self):
- return json.dumps(True)
- return MyData()
- return MyResponse()
-
- self.tool.conn = MyConn()
- self.assertTrue(self.tool._try_login('user32', 'pass64'))
- self.assertEqual(self.tool.conn.method, 'POST')
- self.assertEqual(self.tool.conn.url, '/login')
- self.assertEqual(json.loads(self.tool.conn.param),
- {"password": "pass64", "username": "user32"})
- self.assertIn('cookie', self.tool.conn.headers)
- finally:
- self.tool.conn = orig_conn
-
- def test_run(self):
- def login_to_cmdctl():
- return True
- def cmd_loop():
- self.tool._send_message("/module_spec", None)
-
- self.tool.login_to_cmdctl = login_to_cmdctl
- # rewrite cmdloop() to avoid interactive mode
- self.tool.cmdloop = cmd_loop
-
- self.tool.conn.sock = FakeSocket()
- self.tool.conn.sock.close()
-
- self.assertEqual(1, self.tool.run())
-
- # First few lines may be some kind of heading, or a warning that
- # Python readline is unavailable, so we do a sub-string check.
- self.assertIn("Failed to send request, the connection is closed",
- self.printed_messages)
- self.assertEqual(1, len(self.printed_messages))
-
- # validate log message for http.client.CannotSendRequest
- self.assertEqual(1, self.tool.run())
-
- # First few lines may be some kind of heading, or a warning that
- # Python readline is unavailable, so we do a sub-string check.
- self.assertIn("Can not send request, the connection is busy",
- self.printed_messages)
- self.assertEqual(2, len(self.printed_messages))
-
- def test_apply_cfg_command_int(self):
- self.tool.location = '/'
-
- self.assertEqual((1, MultiConfigData.DEFAULT),
- self.tool.config_data.get_value("/foo/an_int"))
-
- cmd_parser = cmdparse.BindCmdParser('config set identifier='
- '"foo/an_int" value="5"')
- self.tool.apply_config_cmd(cmd_parser)
- self.assertEqual((5, MultiConfigData.LOCAL),
- self.tool.config_data.get_value("/foo/an_int"))
-
- cmd_parser = cmdparse.BindCmdParser('config unset identifier='
- '"foo/an_int"')
- self.tool.apply_config_cmd(cmd_parser)
-
- self.assertEqual((1, MultiConfigData.DEFAULT),
- self.tool.config_data.get_value("/foo/an_int"))
-
- # this should raise a NotFoundError
- cmd_parser = cmdparse.BindCmdParser('config set identifier='
- '"foo/bar" value="[]"')
- self.assertRaises(isc.cc.data.DataNotFoundError,
- self.tool.apply_config_cmd, cmd_parser)
-
- cmd_parser = cmdparse.BindCmdParser('config unset identifier='
- '"foo/bar"')
- self.assertRaises(isc.cc.data.DataNotFoundError,
- self.tool.apply_config_cmd, cmd_parser)
-
- # this should raise a TypeError
- cmd_parser = cmdparse.BindCmdParser('config set identifier='
- '"foo/an_int" value="[]"')
- self.assertRaises(isc.cc.data.DataTypeError,
- self.tool.apply_config_cmd, cmd_parser)
-
- # this is a very specific one for use with a set of list tests
- # to try out the flexibility of the parser (only in the next test)
- def clt(self, full_cmd_string, item_value):
- cmd_parser = cmdparse.BindCmdParser(full_cmd_string)
- self.tool.apply_config_cmd(cmd_parser)
- self.assertEqual(([item_value], MultiConfigData.LOCAL),
- self.tool.config_data.get_value("/foo/a_list"))
-
- def test_apply_cfg_command_list(self):
- self.tool.location = '/'
-
- self.assertEqual(([], MultiConfigData.DEFAULT),
- self.tool.config_data.get_value("/foo/a_list"))
-
- self.clt("config set identifier=\"foo/a_list\" value=[\"a\"]", "a")
- self.clt("config set identifier=\"foo/a_list\" value =[\"b\"]", "b")
- self.clt("config set identifier=\"foo/a_list\" value= [\"c\"]", "c")
- self.clt("config set identifier=\"foo/a_list\" value = [\"d\"]", "d")
- self.clt("config set identifier =\"foo/a_list\" value=[\"e\"]", "e")
- self.clt("config set identifier= \"foo/a_list\" value=[\"f\"]", "f")
- self.clt("config set identifier = \"foo/a_list\" value=[\"g\"]", "g")
- self.clt("config set identifier = \"foo/a_list\" value = [\"h\"]", "h")
- self.clt("config set identifier = \"foo/a_list\" value=[\"i\" ]", "i")
- self.clt("config set identifier = \"foo/a_list\" value=[ \"j\"]", "j")
- self.clt("config set identifier = \"foo/a_list\" value=[ \"k\" ]", "k")
-
- # this should raise a TypeError
- cmd_parser = cmdparse.BindCmdParser('config set identifier='
- '"foo/a_list" value="a"')
- self.assertRaises(isc.cc.data.DataTypeError,
- self.tool.apply_config_cmd, cmd_parser)
-
- cmd_parser = cmdparse.BindCmdParser('config set identifier='
- '"foo/a_list" value=[1]')
- self.assertRaises(isc.cc.data.DataTypeError,
- self.tool.apply_config_cmd, cmd_parser)
-
- def tearDown(self):
- sys.stdout = self.stdout_backup
-
- def test_cmd_has_identifier_param(self):
- module = ModuleInfo(name="test_module")
-
- cmd = CommandInfo(name="command_with_identifier")
- param = ParamInfo(name=bindcmd.CFGITEM_IDENTIFIER_PARAM)
- cmd.add_param(param)
- module.add_command(cmd)
-
- cmd = CommandInfo(name="command_without_identifier")
- param = ParamInfo(name="some_argument")
- cmd.add_param(param)
- module.add_command(cmd)
-
- self.tool.add_module_info(module)
-
- cmd_parser = cmdparse.BindCmdParser('test_module '
- 'command_with_identifier')
- self.assertTrue(self.tool._cmd_has_identifier_param(cmd_parser))
-
- cmd_parser = cmdparse.BindCmdParser('test_module '
- 'command_without_identifier')
- self.assertFalse(self.tool._cmd_has_identifier_param(cmd_parser))
-
- cmd_parser = cmdparse.BindCmdParser('badmodule '
- 'command_without_identifier')
- self.assertFalse(self.tool._cmd_has_identifier_param(cmd_parser))
-
- def test_get_identifier_startswith(self):
- hints = self.tool._get_identifier_startswith("/")
- self.assertEqual(['foo/an_int', 'foo/a_list'], hints)
-
- hints = self.tool._get_identifier_startswith("/foo/an")
- self.assertEqual(['foo/an_int'], hints)
-
- hints = self.tool._get_identifier_startswith("/bar")
- self.assertEqual([], hints)
-
-class FakeBindCmdInterpreter(bindcmd.BindCmdInterpreter):
- def __init__(self):
- pass
-
-class TestBindCmdInterpreter(unittest.TestCase):
-
- def setUp(self):
- self.old_stdout = sys.stdout
-
- def tearDown(self):
- sys.stdout = self.old_stdout
-
- def _create_invalid_csv_file(self, csvfilename):
- import csv
- csvfile = open(csvfilename, 'w')
- writer = csv.writer(csvfile)
- writer.writerow(['name1'])
- writer.writerow(['name2'])
- csvfile.close()
-
- def test_csv_file_dir(self):
- # Checking default value
- home_dir = pwd.getpwnam(getpass.getuser()).pw_dir
- self.assertEqual(home_dir + os.sep + '.bind10' + os.sep,
- bindcmd.BindCmdInterpreter().csv_file_dir)
-
- new_csv_dir = '/something/different/'
- custom_cmd = bindcmd.BindCmdInterpreter(csv_file_dir=new_csv_dir)
- self.assertEqual(new_csv_dir, custom_cmd.csv_file_dir)
-
- def test_get_saved_user_info(self):
- with open(os.devnull, 'w') as f:
- sys.stdout = f
- cmd = bindcmd.BindCmdInterpreter()
- users = cmd._get_saved_user_info('/notexist', 'csv_file.csv')
- self.assertEqual([], users)
-
- csvfilename = 'csv_file.csv'
- self._create_invalid_csv_file(csvfilename)
- users = cmd._get_saved_user_info('./', csvfilename)
- self.assertEqual([], users)
- os.remove(csvfilename)
-
-class TestCommandLineOptions(unittest.TestCase):
- def setUp(self):
- self.parser = TestOptParser()
- set_bindctl_options(self.parser)
-
- def test_csv_file_dir(self):
- # by default the option is "undefined"
- (options, _) = self.parser.parse_args([])
- self.assertEqual(None, options.csv_file_dir)
-
- # specify the option, valid case.
- (options, _) = self.parser.parse_args(['--csv-file-dir', 'some_dir'])
- self.assertEqual('some_dir', options.csv_file_dir)
-
- # missing option arg; should trigger parser error.
- self.assertRaises(OptsError, self.parser.parse_args,
- ['--csv-file-dir'])
-
-if __name__== "__main__":
- unittest.main()
-
+++ /dev/null
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-
-import unittest
-from bindctl import cmdparse
-
-class TestCmdParse(unittest.TestCase):
-
- def test_remove_unquoted_whitespace(self):
- self.assertEqual(cmdparse._remove_unquoted_whitespace("a"), "a")
- self.assertEqual(cmdparse._remove_unquoted_whitespace(" a"), "a")
- self.assertEqual(cmdparse._remove_unquoted_whitespace("a "), "a")
- self.assertEqual(cmdparse._remove_unquoted_whitespace(" a "), "a")
- self.assertNotEqual(cmdparse._remove_unquoted_whitespace("a"), "a ")
- self.assertNotEqual(cmdparse._remove_unquoted_whitespace(" a"), " a")
- self.assertNotEqual(cmdparse._remove_unquoted_whitespace("a "), "a ")
- self.assertNotEqual(cmdparse._remove_unquoted_whitespace(" a "), " a ")
- self.assertNotEqual(cmdparse._remove_unquoted_whitespace(" a "), "b")
-
- self.assertEqual(cmdparse._remove_unquoted_whitespace("\"abc\""), "\"abc\"")
- self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"abc\""), "\"abc\"")
- self.assertEqual(cmdparse._remove_unquoted_whitespace("\"abc\" "), "\"abc\"")
- self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"abc\" "), "\"abc\"")
-
- self.assertEqual(cmdparse._remove_unquoted_whitespace("\" abc\""), "\" abc\"")
- self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"a bc\""), "\"a bc\"")
- self.assertEqual(cmdparse._remove_unquoted_whitespace("\"ab c\" "), "\"ab c\"")
- self.assertEqual(cmdparse._remove_unquoted_whitespace(" \"abc \" "), "\"abc \"")
- self.assertEqual(cmdparse._remove_unquoted_whitespace(" \" a b c \" "), "\" a b c \"")
-
- self.assertEqual(cmdparse._remove_unquoted_whitespace("a\" abc\"a"), "a\" abc\"a")
- self.assertEqual(cmdparse._remove_unquoted_whitespace("a \"a bc\"a"), "a\"a bc\"a")
- self.assertEqual(cmdparse._remove_unquoted_whitespace("a\"ab c\" a"), "a\"ab c\"a")
- self.assertEqual(cmdparse._remove_unquoted_whitespace("a \"abc \" a"), "a\"abc \"a")
- self.assertEqual(cmdparse._remove_unquoted_whitespace("a \" a b c \" a"), "a\" a b c \"a")
-
- # short-hand function to make the set of tests more readable
- def rws(self, a, b):
- self.assertEqual(cmdparse._remove_list_and_map_whitespace(a), b)
-
- def test_remove_list_whitespace(self):
- self.rws("a", "a")
- self.rws(" a ", " a ")
- self.rws(" [a] ", " [a] ")
- self.rws(" [ a] ", " [a] ")
- self.rws(" [ a ] ", " [a] ")
- self.rws(" [ a b c ] ", " [abc] ")
- self.rws(" [ a \"b c\" ] ", " [a\"b c\"] ")
- self.rws("a [ a \"b c\" ] a", "a [a\"b c\"] a")
- self.rws("a] [ a \"b c\" ] a", "a] [a\"b c\"] a")
- self.rws(" [ a [b c] ] ", " [a[bc]] ")
- self.rws(" [ a b][ c d ] ", " [ab][cd] ")
- self.rws(" [ a b] [ c d ] ", " [ab] [cd] ")
-
- self.rws("a", "a")
- self.rws(" a ", " a ")
- self.rws(" {a} ", " {a} ")
- self.rws(" { a} ", " {a} ")
- self.rws(" { a } ", " {a} ")
- self.rws(" { a b c } ", " {abc} ")
- self.rws(" { a \"b c\" } ", " {a\"b c\"} ")
- self.rws("a { a \"b c\" } a", "a {a\"b c\"} a")
- self.rws("a} { a \"b c\" } a", "a} {a\"b c\"} a")
- self.rws(" { a {b c} } ", " {a{bc}} ")
- self.rws(" { a b}{ c d } ", " {ab}{cd} ")
- self.rws(" { a b} { c d } ", " {ab} {cd} ")
-
- self.rws(" [ a b]{ c d } ", " [ab]{cd} ")
- self.rws(" [ a b{ c d }] ", " [ab{cd}] ")
- self.rws(" [ a b{ \"c d\" }] ", " [ab{\"c d\"}] ")
-
-
-if __name__== "__main__":
- unittest.main()
-
+++ /dev/null
-/b10-cfgmgr
-/b10-cfgmgr.py
-/b10-cfgmgr.8
+++ /dev/null
-SUBDIRS = . plugins local_plugins tests
-
-pkglibexecdir = $(libexecdir)/@PACKAGE@
-
-pkglibexec_SCRIPTS = b10-cfgmgr
-
-CLEANFILES = b10-cfgmgr b10-cfgmgr.pyc
-
-b10_cfgmgrdir = @localstatedir@/@PACKAGE@
-#B10_cfgmgr_DATA =
-
-man_MANS = b10-cfgmgr.8
-DISTCLEANFILES = $(man_MANS)
-EXTRA_DIST = $(man_MANS) b10-cfgmgr.xml
-
-if GENERATE_DOCS
-
-b10-cfgmgr.8: b10-cfgmgr.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-cfgmgr.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 BIND 10 > $@
-
-endif
-
-# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
-b10-cfgmgr: b10-cfgmgr.py
- $(SED) "s|@@PYTHONPATH@@|@pyexecdir@|" b10-cfgmgr.py >$@
- chmod a+x $@
-
-install-data-local:
- $(mkinstalldirs) $(DESTDIR)/@localstatedir@/@PACKAGE@
-
-install-data-hook:
- -chmod 2770 $(DESTDIR)/@localstatedir@/@PACKAGE@
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-import sys; sys.path.append ('@@PYTHONPATH@@')
-
-import bind10_config
-from isc.cc import SessionError
-import isc.util.process
-import signal
-import os
-from optparse import OptionParser
-import glob
-import os.path
-import imp
-import isc.log
-isc.log.init("b10-cfgmgr", buffer=True)
-from isc.config.cfgmgr import ConfigManager, ConfigManagerDataReadError, logger
-from isc.log_messages.cfgmgr_messages import *
-import isc.util.traceback_handler
-
-isc.util.process.rename()
-
-# Import some paths from our configuration
-DATA_PATH = bind10_config.DATA_PATH
-PLUGIN_PATHS = bind10_config.PLUGIN_PATHS
-PREFIX = bind10_config.PREFIX
-DEFAULT_CONFIG_FILE = "b10-config.db"
-
-cm = None
-
-def parse_options(args=sys.argv[1:], Parser=OptionParser):
- parser = Parser()
- parser.add_option("-p", "--data-path", dest="data_path",
- help="Directory to search for configuration files " +
- "(default=" + DATA_PATH + ")", default=None)
- parser.add_option("-c", "--config-filename", dest="config_file",
- help="Configuration database filename " +
- "(default=" + DEFAULT_CONFIG_FILE + ")",
- default=None)
- parser.add_option("--clear-config", action="store_true",
- dest="clear_config", default=False,
- help="Back up the configuration file and start with " +
- "a clean one")
- (options, args) = parser.parse_args(args)
- if args:
- parser.error("No non-option arguments allowed")
- return options
-
-def signal_handler(signal, frame):
- global cm
- if cm:
- cm.running = False
-
-def load_plugins(path, cm):
- """Load all python files in the given path and treat them as plugins."""
- # Find the python files
- plugins = glob.glob(path + os.sep + '*.py')
- for plugin in plugins:
- # Generate the name of the plugin
- filename = os.path.basename(plugin)
- name = filename[:-3]
- # Load it
- module = imp.load_source(name, plugin)
- # Ask it to provide the spec and checking function
- (spec, check_func) = module.load()
- # And insert it into the manager
- cm.set_virtual_module(spec, check_func)
-
-
-def determine_path_and_file(data_path_option, config_file_option):
- """Given the data path and config file as specified on the command line
- (or not specified, as may be the case), determine the full path and
- file to use when starting the config manager;
- - if neither are given, use defaults
- - if both are given, use both
- - if only data path is given, use default file in that path
- - if only file is given, use cwd() + file (if file happens to
- be an absolute file name, path will be ignored)
- Arguments are either a string, or None.
- Returns a tuple containing (result_path, result_file).
- """
- data_path = data_path_option
- config_file = config_file_option
- if config_file is None:
- config_file = DEFAULT_CONFIG_FILE
- if data_path is None:
- data_path = DATA_PATH
- else:
- if data_path is None:
- data_path = os.getcwd()
- return (data_path, config_file)
-
-def main():
- options = parse_options()
- global cm
- try:
- (data_path, config_file) = determine_path_and_file(options.data_path,
- options.config_file)
- cm = ConfigManager(data_path, config_file, None, options.clear_config)
- signal.signal(signal.SIGINT, signal_handler)
- signal.signal(signal.SIGTERM, signal_handler)
- cm.read_config()
- for ppath in PLUGIN_PATHS:
- load_plugins(ppath, cm)
- cm.notify_b10_init()
- cm.run()
- except SessionError as se:
- logger.fatal(CFGMGR_CC_SESSION_ERROR, se)
- return 1
- except KeyboardInterrupt as kie:
- logger.info(CFGMGR_STOPPED_BY_KEYBOARD)
- except ConfigManagerDataReadError as cmdre:
- logger.fatal(CFGMGR_DATA_READ_ERROR, cmdre)
- return 2
- return 0
-
-if __name__ == "__main__":
- sys.exit(isc.util.traceback_handler.traceback_handler(main))
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>June 20, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-cfgmgr</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-cfgmgr</refname>
- <refpurpose>Configuration manager</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2010</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>b10-cfgmgr</command>
- <arg><option>-c <replaceable>config-filename</replaceable></option></arg>
- <arg><option>-p <replaceable>data_path</replaceable></option></arg>
- <arg><option>--clear-config</option></arg>
- <arg><option>--config-filename <replaceable>config-filename</replaceable></option></arg>
- <arg><option>--data-path <replaceable>data_path</replaceable></option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>
- The <command>b10-cfgmgr</command> daemon handles all BIND 10
- system configuration. It provides persistent storage for
- configuration, and notifies running BIND 10 modules of
- configuration changes.
- </para>
-
- <para>
- The <command>bindctl</command> can be used to talk to this
- configuration manager via a <command>b10-cmdctl</command> connection.
- </para>
-
-<!-- TODO: briefly explain why both msqg channel and cmdctl communication -->
- <para>
- This daemon communicates over a <command>b10-msgq</command> C-Channel
- connection. If this connection is not established,
- <command>b10-cfgmgr</command> will exit.
- </para>
-<!-- TODO: will it exit? -->
-
- <para>
- The daemon may be cleanly stopped by sending the SIGTERM
- signal to the process. This shutdown does not notify the
- subscribers.
- </para>
-
-<!-- TODO: add a verbose or quiet switch so it is not so noisy -->
- </refsect1>
-
- <refsect1>
- <title>ARGUMENTS</title>
-
- <para>The arguments are as follows:</para>
-
- <variablelist>
- <varlistentry>
- <term>
- <option>--clear-config</option>
- </term>
- <listitem>
- <para>
- This will create a backup of the existing configuration
- file, remove it, and
- <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
- will use the default configurations.
- The name of the backup file can be found in the logs
- (<varname>CFGMGR_BACKED_UP_CONFIG_FILE</varname>).
- (It will append a number to the backup filename if a
- previous backup file exists.)
- </para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>-c</option> <replaceable>config-filename</replaceable>,
- <option>--config-filename</option> <replaceable>config-filename</replaceable>
- </term>
- <listitem>
- <para>The configuration database filename to use. Can be either
- absolute or relative to data path.
- It defaults to "<filename>b10-config.db</filename>".</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>-p</option> <replaceable>data-path</replaceable>,
- <option>--data-path</option> <replaceable>data-path</replaceable>
- </term>
- <listitem>
- <para>The path where BIND 10 looks for files. The
- configuration file is looked for here, if it is relative. If it is
- absolute, the path is ignored.</para>
- </listitem>
- </varlistentry>
- </variablelist>
- </refsect1>
-
- <refsect1>
- <title>FILES</title>
-<!-- TODO: fix path -->
- <para><filename>/usr/local/var/bind10/b10-config.db</filename>
- — Configuration storage file.
- </para>
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>.
- <citerefentry>
- <refentrytitle>b10-init</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-msgq</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- The <command>b10-cfgmgr</command> daemon and configuration
- specification were initially designed by Jelte Jansen of ISC.
- Its development began in October 2009.
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-/datasrc.spec
+++ /dev/null
-# Nothing is installed from this directory. This local_plugins
-# directory overrides the plugins directory when lettuce is run, and the
-# spec file here is used to serve the static zone from the source tree
-# for testing (instead of installation prefix).
-
-noinst_DATA = datasrc.spec
-
-datasrc.spec: ../plugins/datasrc.spec.pre
- $(SED) -e "s|@@STATIC_ZONE_FILE@@|$(abs_top_builddir)/src/lib/datasrc/static.zone|;s|@@SQLITE3_DATABASE_FILE@@|$(abs_top_builddir)/local.zone.sqlite3|" ../plugins/datasrc.spec.pre >$@
-
-CLEANFILES = datasrc.spec
+++ /dev/null
-/datasrc.spec
-/datasrc.spec.pre
+++ /dev/null
-SUBDIRS = tests
-
-EXTRA_DIST = README logging.spec tsig_keys.spec
-
-datasrc.spec: datasrc.spec.pre
- $(SED) -e "s|@@STATIC_ZONE_FILE@@|$(pkgdatadir)/static.zone|;s|@@SQLITE3_DATABASE_FILE@@|$(localstatedir)/$(PACKAGE)/zone.sqlite3|" datasrc.spec.pre >$@
-
-config_plugindir = @prefix@/share/@PACKAGE@/config_plugins
-config_plugin_DATA = logging.spec tsig_keys.spec
-
-python_PYTHON = b10logging.py tsig_keys.py
-pythondir = $(config_plugindir)
-
-CLEANFILES = b10logging.pyc tsig_keys.pyc datasrc.spec
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-How to write a configuration plugin
-===================================
-
-The plugins are used to describe configuration modules that have no hosting
-process. Therefore there's no process to provide their specification and to
-check them for correctness. So the plugin takes this responsibility.
-
-Each plugin is a python file installed into the
-`@prefix@/share/@PACKAGE@/config_plugins` directory (usually
-`/usr/share/bind10/config_plugins`). It is loaded automatically at startup.
-
-The entrypoint of a plugin is function called `load()`. It should return a
-tuple, first value should be the module specification (usually instance of
-`isc.config.module_spec.ModuleSpec`, loaded by `module_spec_from_file()`).
-
-The second value is a callable object. It will be called whenever the
-configuration of module needs to be checked. The only parameter will be the new
-config (as python dictionary). To accept the new configuration, return None. If
-you return a string, it is taken as an error message. If you raise an
-exception, the config is rejected as well, however it is not considered a
-graceful rejection, but a failure of the module.
-
-So, this is how a plugin could look like:
-
- from isc.config.module_spec import module_spec_from_file
-
- def check(config):
- if config['bogosity'] > 1:
- return "Too bogus to live with"
- else:
- return None
-
- def load():
- return (module_spec_from_file('module_spec.spec'), check)
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# This is the configuration plugin for logging options
-# The name is 'b10logging' because logging.py is an existing module
-#
-# For a technical background, see
-# http://bind10.isc.org/wiki/LoggingCppApiDesign
-#
-
-from isc.config.module_spec import module_spec_from_file
-from isc.util.file import path_search
-from bind10_config import PLUGIN_PATHS
-spec = module_spec_from_file(path_search('logging.spec', PLUGIN_PATHS))
-
-ALLOWED_SEVERITIES = [ 'default',
- 'debug',
- 'info',
- 'warn',
- 'error',
- 'fatal',
- 'none' ]
-ALLOWED_DESTINATIONS = [ 'console',
- 'file',
- 'syslog' ]
-ALLOWED_STREAMS = [ 'stdout',
- 'stderr' ]
-
-def check(config):
- # Check the data layout first
- errors=[]
- if not spec.validate_config(False, config, errors):
- return ' '.join(errors)
- # The 'layout' is ok, now check for specific values
- if 'loggers' in config:
- for logger in config['loggers']:
- # name should always be present
- name = logger['name']
- # report an error if name starts with * but not *.,
- # or if * is not the first character.
- # TODO: we might want to also warn or error if the
- # logger name is not an existing module, but we can't
- # really tell that from here at this point
- star_pos = name.find('*')
- if star_pos > 0 or\
- name == '*.' or\
- (star_pos == 0 and len(name) > 1 and name[1] != '.'):
- errors.append("Bad logger name: '" + name + "': * can "
- "only be used instead of the full "
- "first-level name, e.g. '*' or "
- "'*.subsystem'")
-
- if 'severity' in logger and\
- logger['severity'].lower() not in ALLOWED_SEVERITIES:
- errors.append("bad severity value for logger " + name +
- ": " + logger['severity'])
- if 'output_options' in logger:
- for output_option in logger['output_options']:
- if 'destination' in output_option:
- destination = output_option['destination'].lower()
- if destination not in ALLOWED_DESTINATIONS:
- errors.append("bad destination for logger " +
- name + ": " + output_option['destination'])
- else:
- # if left to default, output is stdout, and
- # it will not show in the updated config,
- # so 1. we only need to check it if present,
- # and 2. if destination is changed, so should
- # output. So first check checks 'in', and the
- # others 'not in' for 'output'
- if destination == "console" and\
- 'output' in output_option and\
- output_option['output'] not in ALLOWED_STREAMS:
- errors.append("bad output for logger " + name +
- ": " + output_option['output'] +
- ", must be stdout or stderr")
- elif destination == "file" and\
- ('output' not in output_option or\
- output_option['output'] == ""):
- errors.append("destination set to file but "
- "output not set to any "
- "filename for logger "
- + name)
- elif destination == "syslog" and\
- 'output' not in output_option or\
- output_option['output'] == "":
- errors.append("destination set to syslog but "
- "output not set to any facility"
- " for logger " + name)
-
- if errors:
- return ', '.join(errors)
- return None
-
-def load():
- return (spec, check)
-
+++ /dev/null
-{
- "module_spec": {
- "module_name": "data_sources",
- "module_description": "The sources of authoritative DNS data",
- "config_data": [
- {
- "item_name": "classes",
- "item_type": "named_set",
- "item_optional": false,
- "item_default": {
- "IN": [
- {
- "type": "sqlite3",
- "params": {
- "database_file": "@@SQLITE3_DATABASE_FILE@@"
- }
- }
- ],
- "CH": [
- {
- "type": "MasterFiles",
- "cache-enable": true,
- "params": {"BIND": "@@STATIC_ZONE_FILE@@"}
- }
- ]
- },
- "named_set_item_spec": {
- "item_name": "class",
- "item_type": "list",
- "item_optional": false,
- "item_default": [],
- "list_item_spec": {
- "item_name": "source",
- "item_type": "map",
- "item_optional": false,
- "item_default": {},
- "map_item_spec": [
- {
- "item_name": "type",
- "item_type": "string",
- "item_optional": false,
- "item_default": ""
- },
- {
- "item_name": "params",
- "item_type": "any",
- "item_optional": false,
- "item_default": null
- },
- {
- "item_name": "cache-enable",
- "item_type": "boolean",
- "item_optional": false,
- "item_default": false
- },
- {
- "item_name": "cache-zones",
- "item_type": "list",
- "item_optional": true,
- "list_item_spec": {
- "item_name": "zone",
- "item_type": "string",
- "item_optional": false,
- "item_default": ""
- }
- },
- {
- "item_name": "name",
- "item_type": "string",
- "item_optional": true
- },
- {
- "item_name": "cache-type",
- "item_type": "string",
- "item_optional": true,
- "item_default": "local"
- }
- ]
- }
- }
- }
- ],
- "commands": []
- }
-}
+++ /dev/null
-# Copyright (C) 2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from isc.config.module_spec import module_spec_from_file
-from isc.util.file import path_search
-from bind10_config import PLUGIN_PATHS
-import isc.dns
-import isc.datasrc
-import json
-import os.path
-import copy
-
-spec = module_spec_from_file(path_search('datasrc.spec', PLUGIN_PATHS))
-
-def check(config):
- """
- Check the configuration.
- """
- # Check the data layout first
- errors=[]
- if not spec.validate_config(False, config, errors):
- return ' '.join(errors)
-
- classes = config.get('classes')
- # Nothing passed here
- if classes is None:
- return None
-
- for rr_class_str in classes:
- try:
- rr_class = isc.dns.RRClass(rr_class_str)
- except isc.dns.InvalidRRClass as irc:
- return "The class '" + rr_class_str + "' is invalid"
-
- dlist = isc.datasrc.ConfigurableClientList(rr_class)
- # We get a copy here, as we are going to mangle the configuration.
- # But we don't want our changes to propagate outside, to the real
- # configuration.
- client_config = copy.deepcopy(classes.get(rr_class_str))
-
- for client in client_config:
- if client['type'] == 'MasterFiles':
- if not client.get('cache-enable', False):
- return 'The cache must be enabled in MasterFiles type'
- params = client.get('params')
- if type(params) != dict:
- return 'Params of MasterFiles must be a named set'
- for name in params:
- try:
- isc.dns.Name(name)
- except Exception as e: # There are many related exceptions
- return str(e)
- if not os.path.exists(params[name]):
- return "Master file " + params[name] + " does not exist"
- # We remove the list of zones locally. We already checked them,
- # and the client list would have skipped them anyway, as we
- # forbid cache. But it would produce a warning and we don't
- # want that here.
- client['params'] = {}
-
- try:
- dlist.configure(json.dumps(client_config), False)
- except isc.datasrc.Error as dse:
- return str(dse)
- return None
-
-def load():
- return (spec, check)
+++ /dev/null
-{
- "module_spec": {
- "module_name": "Logging",
- "module_description": "Logging options",
- "config_data": [
- {
- "item_name": "loggers",
- "item_type": "list",
- "item_optional": false,
- "item_default": [],
- "list_item_spec": {
- "item_name": "logger",
- "item_type": "map",
- "item_optional": false,
- "item_default": {},
- "map_item_spec": [
- { "item_name": "name",
- "item_type": "string",
- "item_optional": false,
- "item_default": ""
- },
- { "item_name": "severity",
- "item_type": "string",
- "item_optional": false,
- "item_default": "INFO"
- },
- { "item_name": "debuglevel",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0
- },
- { "item_name": "additive",
- "item_type": "boolean",
- "item_optional": false,
- "item_default": false
- },
- { "item_name": "output_options",
- "item_type": "list",
- "item_optional": false,
- "item_default": [],
- "list_item_spec": {
- "item_name": "output_option",
- "item_type": "map",
- "item_optional": false,
- "item_default": {},
- "map_item_spec": [
- { "item_name": "destination",
- "item_type": "string",
- "item_optional": false,
- "item_default": "console"
- },
- { "item_name": "output",
- "item_type": "string",
- "item_optional": false,
- "item_default": "stdout"
- },
- { "item_name": "flush",
- "item_type": "boolean",
- "item_optional": false,
- "item_default": true
- },
- { "item_name": "maxsize",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0
- },
- { "item_name": "maxver",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0
- }
- ]
- }
- }
- ]
- }
- }
- ],
- "commands": []
- }
-}
+++ /dev/null
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = tsig_keys_test.py logging_test.py
-
-EXTRA_DIST = $(PYTESTS)
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# test using command-line arguments, so use check-local target instead of TESTS
-# We need to set B10_FROM_BUILD as some of the tests need to create lock file
-# for logging.
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- B10_TEST_PLUGIN_DIR=$(abs_srcdir)/..:$(abs_builddir)/.. \
- B10_FROM_BUILD=$(abs_top_builddir) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/cfgmgr:$(abs_top_builddir)/src/lib/dns/python/.libs \
- $(LIBRARY_PATH_PLACEHOLDER) \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
-
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# Make sure we can load the module, put it into path
-import sys
-import os
-import unittest
-import json
-sys.path.extend(os.environ["B10_TEST_PLUGIN_DIR"].split(':'))
-import isc.log
-
-import datasrc_config_plugin
-
-class DatasrcTest(unittest.TestCase):
- def reject(self, config):
- """
- Just a shortcut to check the config is rejected.
- """
- old = json.dumps(config)
- self.assertIsNotNone(datasrc_config_plugin.check({"classes":
- config}))
- # There's some data mangling inside the plugin. Check it does
- # not propagate out, as it could change the real configuration.
- self.assertEqual(old, json.dumps(config))
-
- def accept(self, config):
- """
- Just a shortcut to check the config is accepted.
- """
- old = json.dumps(config)
- self.assertIsNone(datasrc_config_plugin.check({"classes":
- config}))
- # There's some data mangling inside the plugin. Check it does
- # not propagate out, as it could change the real configuration.
- self.assertEqual(old, json.dumps(config))
-
- def test_load(self):
- """
- Checks the entry point returns the correct values.
- """
- (spec, check) = datasrc_config_plugin.load()
- # It returns the checking function
- self.assertEqual(check, datasrc_config_plugin.check)
- # The plugin stores it's spec
- self.assertEqual(spec, datasrc_config_plugin.spec)
-
- def test_empty(self):
- """
- Check an empty input is OK.
- """
- self.accept({})
-
- def test_invalid_spec(self):
- """
- Check it rejects stuff that is not well-formed according
- to the spec.
- """
- self.reject("test")
- self.reject(13)
- self.reject([])
- self.reject({"IN": {}})
- self.reject({"IN": [{"bad-name": True}]})
-
- def test_class(self):
- """
- The class is rejected, if it is wrong.
- """
- self.reject({"BAD": []})
- self.reject({"": []})
- # But with a good one, it works
- for c in ["IN", "CH", "HS"]:
- self.accept({c: []})
-
- def test_mem_ok(self):
- """
- Test we accept an in-memory data source. It doesn't really matter
- which one it is. We just want to make sure we accept something
- and this one does not need any kind of path mangling to find
- plugins.
- """
- self.accept({"IN": [{
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {}
- }]})
-
- def test_invalid_mem_params(self):
- """
- The client list skips in-memory sources. So we check it locally that
- invalid things are rejected.
- """
- # The 'params' key is mandatory for MasterFiles
- self.reject({"IN": [{
- "type": "MasterFiles",
- "cache-enable": True
- }]})
- # The cache must be enabled
- self.reject({"IN": [{
- "type": "MasterFiles",
- "cache-enable": False,
- "params": {}
- }]})
- self.reject({"IN": [{
- "type": "MasterFiles",
- "params": {}
- }]})
- # Bad params type
- self.reject({"IN": [{
- "type": "MasterFiles",
- "cache-enable": True,
- "params": []
- }]})
- # Bad name
- self.reject({"IN": [{
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {
- "example....org.": '/file/does/not/exist'
- }
- }]})
-
- def test_no_such_file_mem(self):
- """
- We also check the existence of master files. Not the actual content,
- though.
- """
- self.reject({"IN": [{
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {
- "example.org.": '/file/does/not/exist'
- }
- }]})
-
- def test_names_present(self):
- """
- Test we don't choke on configuration with the "name" being present on
- some items.
- """
- self.accept({"IN": [{
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {},
- "name": "Whatever"
- }]})
-
- def test_names_default_classes(self):
- """
- Test we can have a client of the same type in different classes
- without specified name. The defaults should be derived both from
- the type and the class.
- """
- self.accept({
- "IN": [{
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {}
- }],
- "CH": [{
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {}
- }]})
-
- def test_names_collision(self):
- """
- Reject when two names are the same.
-
- Cases are:
- - Explicit names.
- - Two default names turn out to be the same (same type and class).
- - One explicit is set to the same as the default one.
- """
- self.reject({"IN": [
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {},
- "name": "Whatever"
- },
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {},
- "name": "Whatever"
- }]})
- # The same, but across different classes is allowed (we would
- # identify the data source by class+name tuple)
- self.accept({
- "IN": [
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {},
- "name": "Whatever"
- }
- ],
- "CH": [
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {},
- "name": "Whatever"
- }
- ]})
- self.reject({"IN": [
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {}
- },
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {}
- }]})
- self.reject({"IN": [
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {},
- "name": "MasterFiles"
- },
- {
- "type": "MasterFiles",
- "cache-enable": True,
- "params": {}
- }]})
-
-if __name__ == '__main__':
- isc.log.init("bind10")
- isc.log.resetUnitTestRootLogger()
- unittest.main()
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# Make sure we can load the module, put it into path
-import sys
-import os
-sys.path.extend(os.environ["B10_TEST_PLUGIN_DIR"].split(':'))
-
-import b10logging
-import unittest
-
-class LoggingConfCheckTest(unittest.TestCase):
- def test_load(self):
- """
- Checks the entry point returns the correct values.
- """
- (spec, check) = b10logging.load()
- # It returns the checking function
- self.assertEqual(check, b10logging.check)
- # The plugin stores it's spec
- self.assertEqual(spec, b10logging.spec)
-
- def test_logger_conf(self):
- self.assertEqual(None,
- b10logging.check({'loggers':
- [{'name': '*',
- 'severity': 'DEBUG',
- 'debuglevel': 50,
- 'output_options':
- [{'destination': 'file',
- 'output': '/some/file'
- }]
- },
- {'name': 'b10-resolver',
- 'severity': 'WARN',
- 'additive': True,
- 'output_options':
- [{'destination': 'console',
- 'output': 'stderr',
- 'flush': True
- }]
- },
- {'name': 'b10-resolver.resolver',
- 'severity': 'ERROR',
- 'output_options': []
- },
- {'name': '*.cache',
- 'severity': 'INFO'
- }
- ]}))
- def do_bad_name_test(self, name):
- err_str = "Bad logger name: '" + name + "': * can only be "\
- "used instead of the full first-level name, e.g. "\
- "'*' or '*.subsystem'"
- self.assertEqual(err_str,
- b10logging.check({'loggers':
- [{'name': name,
- 'severity': 'DEBUG'},
- ]}))
-
- def test_logger_bad_name(self):
- self.do_bad_name_test("*.")
- self.do_bad_name_test("*foo")
- self.do_bad_name_test("*foo.lib")
- self.do_bad_name_test("foo*")
- self.do_bad_name_test("foo*.lib")
-
- def test_logger_bad_severity(self):
- self.assertEqual('bad severity value for logger *: BADVAL',
- b10logging.check({'loggers':
- [{'name': '*',
- 'severity': 'BADVAL'}]}))
-
- def test_logger_bad_destination(self):
- self.assertEqual('bad destination for logger *: baddest',
- b10logging.check({'loggers':
- [{'name': '*',
- 'severity': 'INFO',
- 'output_options': [
- { 'destination': 'baddest' }
- ]}]}))
-
- def test_logger_bad_console_output(self):
- self.assertEqual('bad output for logger *: bad_output, must be stdout or stderr',
- b10logging.check({'loggers':
- [{'name': '*',
- 'severity': 'INFO',
- 'output_options': [
- { 'destination': 'console',
- 'output': 'bad_output'
- }
- ]}]}))
-
- def test_logger_bad_file_output(self):
- self.assertEqual('destination set to file but output not set to any filename for logger *',
- b10logging.check({'loggers':
- [{'name': '*',
- 'severity': 'INFO',
- 'output_options': [
- { 'destination': 'file' }
- ]}]}))
-
- def test_logger_bad_syslog_output(self):
- self.assertEqual('destination set to syslog but output not set to any facility for logger *',
- b10logging.check({'loggers':
- [{'name': '*',
- 'severity': 'INFO',
- 'output_options': [
- { 'destination': 'syslog' }
- ]}]}))
-
- def test_logger_bad_type(self):
- self.assertEqual('123 should be a string',
- b10logging.check({'loggers':
- [{'name': 123,
- 'severity': 'INFO'}]}))
- self.assertEqual('123 should be a string',
- b10logging.check({'loggers':
- [{'name': 'bind10',
- 'severity': 123}]}))
-
-if __name__ == '__main__':
- unittest.main()
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# Make sure we can load the module, put it into path
-import sys
-import os
-sys.path.extend(os.environ["B10_TEST_PLUGIN_DIR"].split(':'))
-
-import tsig_keys
-import unittest
-import isc.config.module_spec
-
-class TSigKeysTest(unittest.TestCase):
- def test_load(self):
- """
- Checks the entry point returns the correct values.
- """
- (spec, check) = tsig_keys.load()
- # It returns the checking function
- self.assertEqual(check, tsig_keys.check)
- # The plugin stores it's spec
- self.assertEqual(spec, tsig_keys.spec)
-
- def test_spec(self):
- """
- Checks the spec is looking sane (doesn't do really deep check here).
- """
- spec = tsig_keys.spec
- # In python, we don't generally check the type of something, because
- # of the duck typing.
- # But this is unittest, so we check it does what we intend and
- # supplying that's behaving the same but is different is not our
- # intention
- self.assertTrue(isinstance(spec, isc.config.module_spec.ModuleSpec))
- # Correct name
- self.assertEqual("tsig_keys", spec.get_module_name())
- # There are no commands, nobody would handle them anyway
- self.assertEqual([], spec.get_commands_spec())
- # There's some nonempty configuration
- self.assertNotEqual({}, spec.get_config_spec())
-
- def test_missing_keys(self):
- """
- Test that missing keys doesn't kill us. There are just no keys there.
- """
- self.assertEqual(None, tsig_keys.check({}))
-
- def test_data_empty(self):
- """Check we accept valid config with empty set of tsig keys."""
- self.assertEqual(None, tsig_keys.check({'keys': []}))
-
- def test_keys_valid(self):
- """
- Check we accept some valid keys (we don't check all the algorithms,
- that's the job of isc.dns.TSIGKey).
- """
- self.assertEqual(None, tsig_keys.check({'keys':
- ['testkey:QklORCAxMCBpcyBjb29sCg==',
- 'test.key:QklORCAxMCBpcyBjb29sCg==:hmac-sha1']}))
-
- def test_keys_same_name(self):
- """
- Test we reject when we have multiple keys with the same name.
- """
- self.assertEqual("Multiple TSIG keys with name 'test.key.'",
- tsig_keys.check({'keys':
- ['test.key:QklORCAxMCBpcyBjb29sCg==',
- 'test.key:b3RoZXIK']}))
-
- def test_invalid_key(self):
- """
- Test we reject invalid key.
- """
- self.assertEqual("TSIG: Invalid TSIG key string: invalid.key",
- tsig_keys.check({'keys': ['invalid.key']}))
- self.assertEqual(
- "TSIG: Incomplete input for base64: 123",
- tsig_keys.check({'keys': ['invalid.key:123']}))
-
- def test_bad_format(self):
- """
- Test we fail on bad format. We don't really care much how here, though,
- as this should not get in through config manager anyway.
- """
- self.assertNotEqual(None, tsig_keys.check({'bad_name': {}}))
- self.assertNotEqual(None, tsig_keys.check({'keys': 'not_list'}))
- self.assertNotEqual(None, tsig_keys.check({'keys': 42}))
- self.assertNotEqual(None, tsig_keys.check({'keys': {}}))
-
-if __name__ == '__main__':
- unittest.main()
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# This is the plugin for tsig_keys configuration section. The TSIG keyring
-# lives there (eg. all the shared secrets, with some exceptions where there
-# are some TSIG keys elsewhere, but these should be removed soon). We do
-# sanity checking of user configuration here, simply by trying to construct
-# all the keys here.
-
-from isc.config.module_spec import module_spec_from_file
-from isc.util.file import path_search
-from pydnspp import TSIGKey, InvalidParameter
-from bind10_config import PLUGIN_PATHS
-spec = module_spec_from_file(path_search('tsig_keys.spec', PLUGIN_PATHS))
-
-def check(config):
- # Check the data layout first
- errors=[]
- if not spec.validate_config(False, config, errors):
- return ' '.join(errors)
- # Get the list of keys, if any
- keys = config.get('keys', [])
- # Run through them, check they can be constructed and there are no
- # duplicates
- keyNames = set()
- for key in keys:
- try:
- name = str(TSIGKey(key).get_key_name())
- except InvalidParameter as e:
- return "TSIG: " + str(e)
- if name in keyNames:
- return "Multiple TSIG keys with name '" + name + "'"
- keyNames.add(name)
- # No error found, so let's assume it's OK
- return None
-
-def load():
- return (spec, check)
+++ /dev/null
-{
- "module_spec": {
- "module_name": "tsig_keys",
- "module_description": "The TSIG keyring is stored here",
- "config_data": [
- {
- "item_name": "keys",
- "item_type": "list",
- "item_optional": false,
- "item_default": [],
- "list_item_spec": {
- "item_name": "key",
- "item_type": "string",
- "item_optional": false,
- "item_default": ""
- }
- }
- ],
- "commands": []
- }
-}
+++ /dev/null
-/b10-cfgmgr_test.py
+++ /dev/null
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = b10-cfgmgr_test.py
-
-noinst_SCRIPTS = $(PYTESTS)
-EXTRA_DIST = testdata/plugins/testplugin.py
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- chmod +x $(abs_builddir)/$$pytest ; \
- TESTDATA_PATH=$(abs_srcdir)/testdata \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/cfgmgr:$(abs_top_builddir)/src/lib/python/isc/config \
- $(PYCOVERAGE_RUN) $(abs_builddir)/$$pytest || exit ; \
- done
-
-CLEANDIRS = testdata/plugins/__pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-#
-# Tests for the configuration manager run script
-#
-
-import unittest
-import os
-import sys
-import bind10_config
-from isc.testutils.parse_args import OptsError, TestOptParser
-
-class MyConfigManager:
- def __init__(self, path, filename, session=None, rename_config_file=False):
- self._path = path
- self.read_config_called = False
- self.notify_b10_init_called = False
- self.run_called = False
- self.write_config_called = False
- self.rename_config_called = False
- self.running = True
- self.virtual_modules = []
-
- def read_config(self):
- self.read_config_called = True
-
- def notify_b10_init(self):
- self.notify_b10_init_called = True
-
- def run(self):
- self.run_called = True
-
- def write_config(self):
- self.write_config_called = True
-
- def rename_config_file(self, ofile, nfile):
- self.rename_config_called = True
-
- def set_virtual_module(self, spec, function):
- self.virtual_modules.append((spec, function))
-
-class TestPlugins(unittest.TestCase):
- def test_load_plugins(self):
- """Test we can successfully find and load the mock plugin."""
- # Let it load the plugin
- b = __import__("b10-cfgmgr")
- # The parameters aren't important for this test
- cm = MyConfigManager(None, None)
- b.load_plugins(os.environ['TESTDATA_PATH'] + os.sep + 'plugins', cm)
- # Check exactly one plugin was loaded and the right data were fed into
- # the cm
- self.assertEqual(len(cm.virtual_modules), 1)
- (spec, check) = cm.virtual_modules[0]
- self.assertEqual(spec.get_module_name(), "mock_config_plugin")
- self.assertEqual(check(None), "Mock config plugin")
-
-class TestConfigManagerStartup(unittest.TestCase):
- def test_cfgmgr(self):
- # some creative module use;
- # b10-cfgmgr has a hyphen, so we use __import__
- # this also gives us the chance to override the imported
- # module ConfigManager in it.
- b = __import__("b10-cfgmgr")
- orig_load = b.load_plugins
- b.PLUGIN_PATHS = ["/plugin/path"]
- self.loaded_plugins = False
- def load_plugins(path, cm):
- # Check it's called with proper arguments
- self.assertEqual(path, "/plugin/path")
- self.assertTrue(isinstance(cm, MyConfigManager))
- self.loaded_plugins = True
- b.load_plugins = load_plugins
- b.ConfigManager = MyConfigManager
-
- b.main()
- b.load_plugins = orig_load
-
- self.assertTrue(b.cm.read_config_called)
- self.assertTrue(b.cm.notify_b10_init_called)
- self.assertTrue(b.cm.run_called)
- self.assertTrue(self.loaded_plugins)
- # if there are no changes, config is not written
- self.assertFalse(b.cm.write_config_called)
- self.assertFalse(b.cm.rename_config_called)
-
- self.assertTrue(b.cm.running)
- b.signal_handler(None, None)
- self.assertFalse(b.cm.running)
-
- # TODO: take value from the 'global config module'
- # (and rename the .in away from this file again)
- data_path = "@localstatedir@/@PACKAGE@".replace("${prefix}", "@prefix@")
- self.assertEqual(data_path, b.DATA_PATH)
-
- # remove the 'module' again, or later tests may fail
- # (if it is already present it won't be loaded again)
- sys.modules.pop("b10-cfgmgr")
-
- def test_cfgmgr_from_source(self):
- tmp_env_var = "/just/some/dir"
- env_var = None
- if "B10_FROM_SOURCE" in os.environ:
- env_var = os.environ["B10_FROM_SOURCE"]
-
- os.environ["B10_FROM_SOURCE"] = tmp_env_var
- bind10_config.reload()
- b = __import__("b10-cfgmgr", globals(), locals())
- b.PLUGIN_PATH = [] # It's enough to test plugins in one test
- b.ConfigManager = MyConfigManager
- self.assertEqual(tmp_env_var, b.DATA_PATH)
-
- if env_var != None:
- os.environ["B10_FROM_SOURCE"] = env_var
- bind10_config.reload()
-
- sys.modules.pop("b10-cfgmgr")
-
-class TestParseArgs(unittest.TestCase):
- """
- Test for the parsing of command line arguments. We provide a different
- array to parse instead.
- """
-
- def test_defaults(self):
- """
- Test the default values when no options are provided.
- """
- # Pass it empty array, not our arguments
- b = __import__("b10-cfgmgr")
- parsed = b.parse_options([], TestOptParser)
- self.assertEqual(None, parsed.data_path)
- self.assertEqual(None, parsed.config_file)
-
- def test_wrong_args(self):
- """
- Test it fails when we pass invalid option.
- """
- b = __import__("b10-cfgmgr")
- self.assertRaises(OptsError, b.parse_options, ['--wrong-option'],
- TestOptParser)
-
- def test_not_arg(self):
- """
- Test it fails when there's an argument that's not option
- (eg. without -- at the beginning).
- """
- b = __import__("b10-cfgmgr")
- self.assertRaises(OptsError, b.parse_options, ['not-option'],
- TestOptParser)
-
- def test_datapath(self):
- """
- Test overwriting the data path.
- """
- b = __import__("b10-cfgmgr")
- parsed = b.parse_options(['--data-path=/path'], TestOptParser)
- self.assertEqual('/path', parsed.data_path)
- self.assertEqual(None, parsed.config_file)
- parsed = b.parse_options(['-p', '/path'], TestOptParser)
- self.assertEqual('/path', parsed.data_path)
- self.assertEqual(None, parsed.config_file)
- self.assertRaises(OptsError, b.parse_options, ['-p'], TestOptParser)
- self.assertRaises(OptsError, b.parse_options, ['--data-path'],
- TestOptParser)
-
- def test_db_filename(self):
- """
- Test setting the configuration database file.
- """
- b = __import__("b10-cfgmgr")
- parsed = b.parse_options(['--config-filename=filename'],
- TestOptParser)
- self.assertEqual(None, parsed.data_path)
- self.assertEqual("filename", parsed.config_file)
- parsed = b.parse_options(['-c', 'filename'], TestOptParser)
- self.assertEqual(None, parsed.data_path)
- self.assertEqual("filename", parsed.config_file)
- self.assertRaises(OptsError, b.parse_options, ['-c'], TestOptParser)
- self.assertRaises(OptsError, b.parse_options, ['--config-filename'],
- TestOptParser)
-
- def test_determine_path_and_file(self):
- b = __import__("b10-cfgmgr")
- self.assertEqual((b.DATA_PATH, b.DEFAULT_CONFIG_FILE),
- b.determine_path_and_file(None, None))
- self.assertEqual(("/foo", b.DEFAULT_CONFIG_FILE),
- b.determine_path_and_file("/foo", None))
- self.assertEqual((os.getcwd(), "file.config"),
- b.determine_path_and_file(None, "file.config"))
- self.assertEqual(("/foo", "bar"),
- b.determine_path_and_file("/foo", "bar"))
-
- def test_clear_config(self):
- b = __import__("b10-cfgmgr")
- parsed = b.parse_options([], TestOptParser)
- self.assertFalse(parsed.clear_config)
- parsed = b.parse_options(['--clear-config'], TestOptParser)
- self.assertTrue(parsed.clear_config)
-
-if __name__ == '__main__':
- unittest.main()
-
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# A test plugin. It does mostly nothing, just provides a function we can
-# recognize from the test. However, it looks like a real plugin, with the
-# (almost) correct interface, even when it's not used.
-
-class MockSpec:
- """Mock spec, contains no data, it can only provide it's name.
- It'll not really be used for anything."""
- def get_module_name(self):
- return "mock_config_plugin"
-
-def mock_check_config(config):
- """Mock function to check config. Does nothing, only returns
- an "error" string to indicate it's this one."""
- return "Mock config plugin"
-
-def load():
- """When a plugin is loaded, this is called to provide the spec and
- checking function."""
- return (MockSpec(), mock_check_config)
+++ /dev/null
-/b10-certgen
-/b10-certgen.1
-/b10-cmdctl
-/b10-cmdctl.8
-/cmdctl-certfile.pem
-/cmdctl-keyfile.pem
-/cmdctl.py
-/cmdctl.spec
-/cmdctl.spec.pre
-/run_b10-cmdctl.sh
+++ /dev/null
-SUBDIRS = . tests
-
-pkglibexecdir = $(libexecdir)/@PACKAGE@
-
-pkglibexec_SCRIPTS = b10-cmdctl
-
-bin_PROGRAMS = b10-certgen
-
-nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.py
-pylogmessagedir = $(pyexecdir)/isc/log_messages/
-
-b10_cmdctldir = $(pkgdatadir)
-
-CERTFILES = cmdctl-keyfile.pem cmdctl-certfile.pem
-
-b10_cmdctl_DATA = cmdctl.spec
-
-CLEANFILES= b10-cmdctl cmdctl.pyc cmdctl.spec
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.py
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.pyc
-
-man_MANS = b10-cmdctl.8 b10-certgen.1
-DISTCLEANFILES = $(man_MANS) cmdctl-certfile.pem cmdctl-keyfile.pem
-EXTRA_DIST = $(man_MANS) b10-certgen.xml b10-cmdctl.xml cmdctl_messages.mes
-EXTRA_DIST += cmdctl-accounts.csv
-
-if GENERATE_DOCS
-
-b10-cmdctl.8: b10-cmdctl.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-cmdctl.xml
-
-b10-certgen.1: b10-certgen.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-certgen.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 BIND 10 > $@
-
-endif
-
-cmdctl.spec: cmdctl.spec.pre
- $(SED) -e "s|@@SYSCONFDIR@@|$(sysconfdir)|" cmdctl.spec.pre >$@
-
-$(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.py : cmdctl_messages.mes
- $(top_builddir)/src/lib/log/compiler/message \
- -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/cmdctl_messages.mes
-
-# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
-b10-cmdctl: cmdctl.py $(PYTHON_LOGMSGPKG_DIR)/work/cmdctl_messages.py $(CERTFILES)
- $(SED) "s|@@PYTHONPATH@@|@pyexecdir@|" cmdctl.py >$@
- chmod a+x $@
-
-b10_certgen_SOURCES = b10-certgen.cc
-b10_certgen_CXXFLAGS = $(BOTAN_INCLUDES)
-b10_certgen_LDFLAGS = $(BOTAN_LIBS)
-
-# Generate the initial certificates immediately
-cmdctl-keyfile.pem: b10-certgen
- ./b10-certgen -q -w
-
-# This is a hack, as b10-certgen creates both cmdctl-keyfile.pem and
-# cmdctl-certfile.pem, and in a parallel make, making these targets
-# simultaneously may result in corrupted files. With GNU make, there is
-# a non-portable way of working around this with pattern rules, but we
-# adopt this hack instead. The downside is that cmdctl-certfile.pem will
-# not be re-generated if cmdctl-keyfile.pem exists and is older. See
-# Trac ticket #2962.
-cmdctl-certfile.pem: cmdctl-keyfile.pem
- touch $(builddir)/cmdctl-keyfile.pem
-
-if INSTALL_CONFIGURATIONS
-
-# Below we intentionally use ${INSTALL} -m 640 instead of $(INSTALL_DATA)
-# because these file will contain sensitive information.
-install-data-local:
- $(mkinstalldirs) $(DESTDIR)/@sysconfdir@/@PACKAGE@
- for f in $(CERTFILES) ; do \
- if test ! -f $(DESTDIR)$(sysconfdir)/@PACKAGE@/$$f; then \
- ${INSTALL} -m 640 $$f $(DESTDIR)$(sysconfdir)/@PACKAGE@/ ; \
- fi ; \
- done
-
-endif
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-When b10-cmdctrl runs, user should provide one certificate and create one account for bindctl.
-
-
-1. Get a certificate
-
-b10-cmdctrl is a https server, so a certificate should be acquired for the SSL-encrypted connection services it provides.
-There are many ways of acquiring appropriate cetificates, such as buying one from a certification authority. Another common practice is to generate a self-signed certificate. The simplest way to do this is with the OpenSSL package, using something like the following:
-
- openssl req -new -x509 -days 365 -nodes -out cert.pem -keyout cert.pem
-
- copy the content from the generated cert.pem. to create_your_cert.pem.
-
-2. Generate one account for bindctl or other web client tools.
-
- run command:
- python \parkinglot\tools\usr_mgr\usr_mgr.py
-
- The account information is saved in passwd.csv
-
-
-
-
+++ /dev/null
-. Add return code for RESTful API document of b10-cmdctl.
-. Update man page for b10-cmdctl?
-. Add check for the content of key/certificate file
- (when cmdctl starts or is configured by bindctl).
-. Use only one msgq/session to communicate with other modules?
-. Add more test cases, especially about the cases where CmdctlException
- is raised
+++ /dev/null
-// Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-//
-// Permission to use, copy, modify, and/or distribute this software for any
-// purpose with or without fee is hereby granted, provided that the above
-// copyright notice and this permission notice appear in all copies.
-//
-// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-// PERFORMANCE OF THIS SOFTWARE.
-
-#include <botan/botan.h>
-#include <botan/x509self.h>
-#include <botan/x509stor.h>
-#include <botan/rsa.h>
-#include <botan/dsa.h>
-#include <botan/data_src.h>
-using namespace Botan;
-
-#include <iostream>
-#include <fstream>
-#include <memory>
-#include <getopt.h>
-
-// For cleaner 'does not exist or is not readable' output than
-// botan provides
-#include <unistd.h>
-#include <errno.h>
-
-// This is a simple tool that creates a self-signed PEM certificate
-// for use with BIND 10. It creates a simple certificate for initial
-// setup. Currently, all values are hardcoded defaults. For future
-// versions, we may want to add more options for administrators.
-
-// It will create a PEM file containing a certificate with the following
-// values:
-// common name: localhost
-// organization: BIND10
-// country code: US
-
-// Additional error return codes; these are specifically
-// chosen to be distinct from validation error codes as
-// provided by Botan. Their main use is to distinguish
-// error cases in the unit tests.
-const int DECODING_ERROR = 100;
-const int BAD_OPTIONS = 101;
-const int READ_ERROR = 102;
-const int WRITE_ERROR = 103;
-const int UNKNOWN_ERROR = 104;
-const int NO_SUCH_FILE = 105;
-const int FILE_PERMISSION_ERROR = 106;
-
-void
-usage() {
- std::cout << "Usage: b10-certgen [OPTION]..." << std::endl;
- std::cout << "Validate, create, or update a self-signed certificate for "
- "use with b10-cmdctl" << std::endl;
- std::cout << "" << std::endl;
- std::cout << "Options:" << std::endl;
- std::cout << "-c, --certfile=FILE\t\tfile to read or store the certificate"
- << std::endl;
- std::cout << "-f, --force\t\t\toverwrite existing certificate even if it"
- << std::endl <<"\t\t\t\tis valid" << std::endl;
- std::cout << "-h, --help\t\t\tshow this help" << std::endl;
- std::cout << "-k, --keyfile=FILE\t\tfile to store the generated private key"
- << std::endl;
- std::cout << "-w, --write\t\t\tcreate a new certificate if the given file"
- << std::endl << "\t\t\t\tdoes not exist, or if is is not valid"
- << std::endl;
- std::cout << "-q, --quiet\t\t\tprint no output when creating or validating"
- << std::endl;
-}
-
-/// \brief Returns true if the given file exists
-///
-/// \param filename The file to check
-/// \return true if file exists
-bool
-fileExists(const std::string& filename) {
- return (access(filename.c_str(), F_OK) == 0);
-}
-
-/// \brief Returns true if the given file exists and is readable
-///
-/// \param filename The file to check
-/// \return true if file exists and is readable
-bool
-fileIsReadable(const std::string& filename) {
- return (access(filename.c_str(), R_OK) == 0);
-}
-
-/// \brief Returns true if the given file exists and is writable
-///
-/// \param filename The file to check
-/// \return true if file exists and is writable
-bool
-fileIsWritable(const std::string& filename) {
- return (access(filename.c_str(), W_OK) == 0);
-}
-
-/// \brief Helper function for readable error output;
-///
-/// Returns string representation of X509 result code
-/// This does not appear to be provided by Botan itself
-///
-/// \param code An \c X509_Code instance
-/// \return A human-readable c string
-const char*
-X509CodeToString(const X509_Code& code) {
- // note that this list provides more than we would
- // need in this context, it is just the enum from
- // the source code of Botan.
- switch (code) {
- case VERIFIED:
- return ("verified");
- case UNKNOWN_X509_ERROR:
- return ("unknown x509 error");
- case CANNOT_ESTABLISH_TRUST:
- return ("cannot establish trust");
- case CERT_CHAIN_TOO_LONG:
- return ("certificate chain too long");
- case SIGNATURE_ERROR:
- return ("signature error");
- case POLICY_ERROR:
- return ("policy error");
- case INVALID_USAGE:
- return ("invalid usage");
- case CERT_FORMAT_ERROR:
- return ("certificate format error");
- case CERT_ISSUER_NOT_FOUND:
- return ("certificate issuer not found");
- case CERT_NOT_YET_VALID:
- return ("certificate not yet valid");
- case CERT_HAS_EXPIRED:
- return ("certificate has expired");
- case CERT_IS_REVOKED:
- return ("certificate has been revoked");
- case CRL_FORMAT_ERROR:
- return ("crl format error");
- case CRL_NOT_YET_VALID:
- return ("crl not yet valid");
- case CRL_HAS_EXPIRED:
- return ("crl has expired");
- case CA_CERT_CANNOT_SIGN:
- return ("CA cert cannot sign");
- case CA_CERT_NOT_FOR_CERT_ISSUER:
- return ("CA certificate not for certificate issuer");
- case CA_CERT_NOT_FOR_CRL_ISSUER:
- return ("CA certificate not for crl issuer");
- default:
- return ("Unknown X509 code");
- }
-}
-
-class CertificateTool {
-public:
- CertificateTool(bool quiet) : quiet_(quiet) {}
-
- int
- createKeyAndCertificate(const std::string& key_file_name,
- const std::string& cert_file_name) {
- try {
- AutoSeeded_RNG rng;
-
- // Create and store a private key
- print("Creating key file " + key_file_name);
- RSA_PrivateKey key(rng, 2048);
- std::ofstream key_file(key_file_name.c_str());
- if (!key_file.good()) {
- print(std::string("Error writing to ") + key_file_name +
- ": " + std::strerror(errno));
- return (WRITE_ERROR);
- }
- key_file << PKCS8::PEM_encode(key, rng, "");
- if (!key_file.good()) {
- print(std::string("Error writing to ") + key_file_name +
- ": " + std::strerror(errno));
- return (WRITE_ERROR);
- }
- key_file.close();
-
- // Certificate options, currently hardcoded.
- // For a future version we may want to make these
- // settable.
- X509_Cert_Options opts;
- opts.common_name = "localhost";
- opts.organization = "UNKNOWN";
- opts.country = "XX";
-
- opts.CA_key();
-
- print("Creating certificate file " + cert_file_name);
-
- // The exact call changed after 1.8, adding the
- // hash function option
-#if BOTAN_VERSION_CODE >= BOTAN_VERSION_CODE_FOR(1,9,0)
- X509_Certificate cert =
- X509::create_self_signed_cert(opts, key, "SHA-256", rng);
-#else
- X509_Certificate cert =
- X509::create_self_signed_cert(opts, key, rng);
-#endif
-
- std::ofstream cert_file(cert_file_name.c_str());
- if (!cert_file.good()) {
- print(std::string("Error writing to ") + cert_file_name +
- ": " + std::strerror(errno));
- return (WRITE_ERROR);
- }
- cert_file << cert.PEM_encode();
- if (!cert_file.good()) {
- print(std::string("Error writing to ") + cert_file_name +
- ": " + std::strerror(errno));
- return (WRITE_ERROR);
- }
- cert_file.close();
- } catch(std::exception& e) {
- std::cout << "Error creating key or certificate: " << e.what()
- << std::endl;
- return (UNKNOWN_ERROR);
- }
- return (0);
- }
-
- int
- validateCertificate(const std::string& certfile) {
- // Since we are dealing with a self-signed certificate here, we
- // also use the certificate to check itself; i.e. we add it
- // as a trusted certificate, then validate the certificate itself.
- //const X509_Certificate cert(certfile);
- try {
- X509_Store store;
- DataSource_Stream in(certfile);
- store.add_trusted_certs(in);
-
- const X509_Code result = store.validate_cert(certfile);
-
- if (result == VERIFIED) {
- print(certfile + " is valid");
- } else {
- print(certfile + " failed to verify: " +
- X509CodeToString(result));
- }
- return (result);
- } catch (const Botan::Decoding_Error& bde) {
- print(certfile + " failed to verify: " + bde.what());
- return (DECODING_ERROR);
- } catch (const Botan::Stream_IO_Error& bsie) {
- print(certfile + " not read: " + bsie.what());
- return (READ_ERROR);
- }
- }
-
- /// \brief Runs the tool
- ///
- /// \param create_cert Create certificate if true, validate if false.
- /// Does nothing if certificate exists and is valid.
- /// \param force_create Create new certificate even if it is valid.
- /// \param certfile Certificate file to read to or write from.
- /// \param keyfile Key file to write if certificate is created.
- /// Ignored if create_cert is false
- /// \return zero on success, non-zero on failure
- int
- run(bool create_cert, bool force_create, const std::string& certfile,
- const std::string& keyfile)
- {
- if (create_cert) {
- // Unless force is given, only create it if the current
- // one is not OK
-
- // First do some basic permission checks; both files
- // should either not exist, or be both readable
- // and writable
- // The checks are done one by one so all errors can
- // be enumerated in one go
- if (fileExists(certfile)) {
- if (!fileIsReadable(certfile)) {
- print(certfile + " not readable: " + std::strerror(errno));
- create_cert = false;
- }
- if (!fileIsWritable(certfile)) {
- print(certfile + " not writable: " + std::strerror(errno));
- create_cert = false;
- }
- }
- // The key file really only needs write permissions (for
- // b10-certgen that is)
- if (fileExists(keyfile)) {
- if (!fileIsWritable(keyfile)) {
- print(keyfile + " not writable: " + std::strerror(errno));
- create_cert = false;
- }
- }
- if (!create_cert) {
- print("Not creating new certificate, "
- "check file permissions");
- return (FILE_PERMISSION_ERROR);
- }
-
- // If we reach this, we know that if they exist, we can both
- // read and write them, so now it's up to content checking
- // and/or force_create
-
- if (force_create || !fileExists(certfile) ||
- validateCertificate(certfile) != VERIFIED) {
- return (createKeyAndCertificate(keyfile, certfile));
- } else {
- print("Not creating a new certificate (use -f to force)");
- }
- } else {
- if (!fileExists(certfile)) {
- print(certfile + ": " + std::strerror(errno));
- return (NO_SUCH_FILE);
- }
- if (!fileIsReadable(certfile)) {
- print(certfile + " not readable: " + std::strerror(errno));
- return (FILE_PERMISSION_ERROR);
- }
- int result = validateCertificate(certfile);
- if (result != 0) {
- print("Running with -w would overwrite the certificate");
- }
- return (result);
- }
- return (0);
- }
-private:
- /// Prints the message to stdout unless quiet_ is true
- void print(const std::string& msg) {
- if (!quiet_) {
- std::cout << msg << std::endl;
- }
- }
-
- bool quiet_;
-};
-
-int
-main(int argc, char* argv[])
-{
- Botan::LibraryInitializer init;
-
- // create or check certificate
- bool create_cert = false;
- // force creation even if not necessary
- bool force_create = false;
- // don't print any output
- bool quiet = false;
-
- // default certificate file
- std::string certfile("cmdctl-certfile.pem");
- // default key file
- std::string keyfile("cmdctl-keyfile.pem");
-
- // whether or not the above values have been
- // overridden (used in command line checking)
- bool certfile_default = true;
- bool keyfile_default = true;
-
- // It would appear some environments insist on
- // char* here (Sunstudio on Solaris), so we const_cast
- // them to get rid of compiler warnings.
- const struct option long_options[] = {
- { const_cast<char*>("certfile"), required_argument, NULL, 'c' },
- { const_cast<char*>("force"), no_argument, NULL, 'f' },
- { const_cast<char*>("help"), no_argument, NULL, 'h' },
- { const_cast<char*>("keyfile"), required_argument, NULL, 'k' },
- { const_cast<char*>("write"), no_argument, NULL, 'w' },
- { const_cast<char*>("quiet"), no_argument, NULL, 'q' },
- { NULL, 0, NULL, 0 }
- };
-
- int opt, option_index;
- while ((opt = getopt_long(argc, argv, "c:fhk:wq", long_options,
- &option_index)) != -1) {
- switch (opt) {
- case 'c':
- certfile = optarg;
- certfile_default = false;
- break;
- case 'f':
- force_create = true;
- break;
- case 'h':
- usage();
- return (0);
- break;
- case 'k':
- keyfile = optarg;
- keyfile_default = false;
- break;
- case 'w':
- create_cert = true;
- break;
- case 'q':
- quiet = true;
- break;
- default:
- // A message will have already been output about the error.
- return (BAD_OPTIONS);
- }
- }
-
- if (optind < argc) {
- std::cout << "Error: extraneous arguments" << std::endl << std::endl;
- usage();
- return (BAD_OPTIONS);
- }
-
- // Some sanity checks on option combinations
- if (create_cert && (certfile_default ^ keyfile_default)) {
- std::cout << "Error: keyfile and certfile must both be specified "
- "if one of them is when calling b10-certgen in write "
- "mode." << std::endl;
- return (BAD_OPTIONS);
- }
- if (!create_cert && !keyfile_default) {
- std::cout << "Error: keyfile is not used when not in write mode"
- << std::endl;
- return (BAD_OPTIONS);
- }
-
- // Initialize the tool and perform the appropriate action(s)
- CertificateTool tool(quiet);
- return (tool.run(create_cert, force_create, certfile, keyfile));
-}
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>November 15, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-certgen</refentrytitle>
- <manvolnum>1</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-certgen</refname>
- <refpurpose>X509 Certificate generation tool for use with b10-cmdctl</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2012</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>b10-certgen</command>
- <group choice="opt">
- <arg choice="[OPTION]..."><option>-</option></arg>
- </group>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>The <command>b10-certgen</command> tool validates, creates, or
- updates a self-signed X509 certificate for use in b10-cmdctl.
- </para>
-
- <para>
- The connection between <command>bindctl</command> and
- <command>b10-cmdctl</command> is done over HTTPS, and therefore
- <command>b10-cmdctl</command> needs a certificate. Since these
- certificates have expiry dates, they also need to be regenerated at
- some point.
-
- There are many tools to do so, but for ease of use, <command>
- b10-certgen</command> can create a simple self-signed certificate.
-
- By default, it will not create anything, but it will merely check an
- existing certificate (if not specified, cmdctl-certfile.pem, in the
- current working directory). And print whether it is valid, and
- whether it would update if the option '-w' is given.
-
- With that option, the certificate could then be replaced by a newly
- created one. If the certificate is still valid, it would still not
- be overwritten (however, if it is found to be invalid, for example
- because it has expired, it would create a new one).
-
- A new certificate is always created if the certificate file does
- not exist, or if creation is forced (with the -f option).
- </para>
- </refsect1>
-
- <refsect1>
- <title>ARGUMENTS</title>
-
- <para>The arguments are as follows:</para>
-
- <variablelist>
-
- <varlistentry>
- <term>
- <option>-c <replaceable>file</replaceable></option>,
- <option>--certfile=<replaceable>file</replaceable></option>
- </term>
- <listitem>
- <para>
- File to read the certificate from, or write the certificate to.
- If <option>-w</option> and <option>-c</option> are used,
- <option>-k</option> is mandatory as well.
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>
- <option>-f</option>,
- <option>--force</option>
- </term>
- <listitem>
- <para>
- Force updating of certificate when <option>-w</option> is used,
- even if the existing certificate is still valid.
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>
- <option>-h</option>,
- <option>--help</option>
- </term>
- <listitem>
- <para>
- Print the command line arguments and exit.
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>
- <option>-k <replaceable>file</replaceable></option>,
- <option>--keyfile=<replaceable>file</replaceable></option>
- </term>
- <listitem>
- <para>
- File to write the private key to. This option is only valid when <option>-w</option> is used, and if this option is used, <option>-c</option> is mandatory as well.
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>
- <option>-w</option>,
- <option>--write</option>
- </term>
- <listitem>
- <para>
- Check the given certificate file. If it does not exist, a new
- private key and certificate are created. If it does exist, the
- certificate is validated. If it is not valid (for instance
- because it has expired), it is overwritten with a newly created
- certificate. If it is valid, nothing happens (use
- <option>-f</option> to force an update in that case).
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>
- <option>-q</option>,
- <option>--quiet</option>
- </term>
- <listitem>
- <para>
- Don't print informational messages (only command-line errors are
- printed). Useful in scripts when only the return code is needed.
- </para>
- </listitem>
- </varlistentry>
- </variablelist>
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- The <command>b10-certgen</command> tool was first implemented
- in November 2012 for the ISC BIND 10 project.
- </para>
- </refsect1>
-
- <refsect1>
- <title>EXAMPLE</title>
- <para>
- To update an expired certificate in BIND 10 that has been installed to
- /usr/local:
- <screen>
-$> cd /usr/local/etc/bind10/
-
-$> b10-certgen
-cmdctl-certfile.pem failed to verify: certificate has expired
-Running with -w would overwrite the certificate
-
-$> b10-certgen --write
-cmdctl-certfile.pem failed to verify: certificate has expired
-Creating key file cmdctl-keyfile.pem
-Creating certificate file cmdctl-certfile.pem
-
-$> b10-certgen --write
-cmdctl-certfile.pem is valid
-Not creating a new certificate (use -f to force)
- </screen>
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2010-2012 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>February 28, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-cmdctl</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-cmdctl</refname>
- <refpurpose>BIND 10 remote control daemon</refpurpose>
-<!-- TODO: is that okay? -->
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2010-2012</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>b10-cmdctl</command>
-
- <arg><option>-a <replaceable>string</replaceable></option></arg>
- <arg><option>-h</option></arg>
- <arg><option>-i <replaceable>number</replaceable></option></arg>
- <arg><option>-p <replaceable>number</replaceable></option></arg>
- <arg><option>-v</option></arg>
- <arg><option>--address <replaceable>string</replaceable></option></arg>
- <arg><option>--help</option></arg>
- <arg><option>--idle-timeout <replaceable>number</replaceable></option></arg>
- <arg><option>--port <replaceable>number</replaceable></option></arg>
- <arg><option>--verbose</option></arg>
- <arg><option>--version</option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>The <command>b10-cmdctl</command> daemon provides an entry
- for commands sent to the BIND 10 services.
- For example, the <command>bindctl</command> user interface
- communicates via <command>b10-cmdctl</command>.
- </para>
-
- <para>
- It is a lightweight HTTPS server with HTTP Digest Authentication
- (username and password validation).
- It offers a RESTful style interface.
-<!-- TODO: document the RESTful APIs -->
- </para>
- </refsect1>
-
- <refsect1>
- <title>OPTIONS</title>
-
- <para>The arguments are as follows:</para>
-
- <variablelist>
-
- <varlistentry>
- <term><option>-a <replaceable>string</replaceable></option>,
- <option>--address <replaceable>string</replaceable></option></term>
- <listitem><para>
- The IP address that <command>b10-cmdctl</command> will listen on.
- The default is 127.0.0.1.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-h</option>,
- <option>--help</option></term>
- <listitem><para>
- Display command usage.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-i <replaceable>number</replaceable></option>,
- <option>--idle-timeout <replaceable>number</replaceable></option></term>
- <listitem><para>
- The socket idle timeout for the HTTPS connection in seconds.
- The default is 1200 seconds.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-p <replaceable>number</replaceable></option>,
- <option>--port <replaceable>number</replaceable></option></term>
- <listitem><para>
- The port number <command>b10-cmdctl</command> will listen on.
- The default is 8080.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-v</option></term>
- <term><option>--verbose</option></term>
- <listitem><para>
- Enable verbose mode.</para>
- </listitem>
- </varlistentry>
-
- <varlistentry>
- <term>
- <option>--version</option></term>
- <listitem><para>
- Display the version number and exit.</para>
- </listitem>
- </varlistentry>
- </variablelist>
-
- </refsect1>
-
- <refsect1>
- <title>CONFIGURATION AND COMMANDS</title>
- <para>
- The configurable settings are:
- </para>
-
- <para>
- <varname>accounts_file</varname> defines the path to the
- user accounts database.
- The default is
- <filename>/usr/local/etc/bind10/cmdctl-accounts.csv</filename>.
- </para>
-
- <para>
- <varname>cert_file</varname> defines the path to the
- PEM certificate file.
- The default is
- <filename>/usr/local/etc/bind10/cmdctl-certfile.pem</filename>.
- </para>
-
- <para>
- <varname>key_file</varname> defines the path to the PEM private key
- file.
- The default is
- <filename>/usr/local/etc/bind10/cmdctl-keyfile.pem</filename>.
- </para>
-
-<!-- TODO: formating -->
- <para>
- The configuration command is:
- </para>
-
- <para>
- <command>shutdown</command> exits <command>b10-cmdctl</command>.
- This has an optional <varname>pid</varname> argument to
- select the process ID to stop.
- (Note that the b10-init process may restart this service
- if configured.)
- </para>
-
- </refsect1>
-
- <refsect1>
- <title>FILES</title>
-<!-- TODO: replace /usr/local -->
-<!-- TODO: permissions -->
-<!-- TODO: what about multiple accounts? -->
-<!-- TODO: shouldn't the password file name say cmdctl in it? -->
- <para><filename>/usr/local/etc/bind10/cmdctl-accounts.csv</filename>
- — account database containing the name, hashed password,
- and the salt.
- </para>
-<!-- TODO: replace /usr/local -->
-<!-- TODO: permissions -->
-<!-- TODO: shouldn't have both in same file, will be configurable -->
- <para><filename>/usr/local/etc/bind10/cmdctl-keyfile.pem</filename>
- — contains the Private key.
- </para>
- <para><filename>/usr/local/etc/bind10/cmdctl-certfile.pem</filename>
- — contains the Certificate.
- </para>
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-init</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>
- </citerefentry>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>AUTHORS</title>
- <para>
- The <command>b10-cmdctl</command> daemon was initially designed
- and coded by Zhang Likun of CNNIC.
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-root,6f0c73bd33101a5ec0294b3ca39fec90ef4717fe,"\7f{5hV&$^(]!uV,3H>E~=f`I;,HgMl""`Eyao4^0l|Nlz|%R9Y0v)#/t'u@CzJ$U\7f)"\r
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-''' cmdctl module is the configuration entry point for all commands from bindctl
-or some other web tools client of bind10. cmdctl is pure https server which provi-
-des RESTful API. When command client connecting with cmdctl, it should first login
-with legal username and password.
- When cmdctl starting up, it will collect command specification and
-configuration specification/data of other available modules from configmanager, then
-wait for receiving request from client, parse the request and resend the request to
-the proper module. When getting the request result from the module, send back the
-resut to client.
-'''
-
-import sys; sys.path.append ('@@PYTHONPATH@@')
-import os
-import socketserver
-import http.server
-import urllib.parse
-import json
-import re
-import ssl, socket
-import isc
-import pprint
-import csv
-import random
-import time
-import signal
-import errno
-from isc.config import ccsession
-import isc.cc.proto_defs
-import isc.util.process
-import isc.net.parse
-from optparse import OptionParser, OptionValueError
-from hashlib import sha1
-from isc.util import socketserver_mixin
-from isc.log_messages.cmdctl_messages import *
-import isc.util.traceback_handler
-
-isc.log.init("b10-cmdctl", buffer=True)
-logger = isc.log.Logger("cmdctl")
-
-# Debug level for communication with BIND10
-DBG_CMDCTL_MESSAGING = logger.DBGLVL_COMMAND
-
-try:
- import threading
-except ImportError:
- import dummy_threading as threading
-
-isc.util.process.rename()
-
-__version__ = 'BIND10'
-URL_PATTERN = re.compile('/([\w]+)(?:/([\w]+))?/?')
-CONFIG_DATA_URL = 'config_data'
-MODULE_SPEC_URL = 'module_spec'
-
-
-# If B10_FROM_BUILD is set in the environment, we use data files
-# from a directory relative to that, otherwise we use the ones
-# installed on the system
-if "B10_FROM_BUILD" in os.environ:
- SPECFILE_PATH = os.environ["B10_FROM_BUILD"] + "/src/bin/cmdctl"
-else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
-SPECFILE_LOCATION = SPECFILE_PATH + os.sep + "cmdctl.spec"
-
-class CmdctlException(Exception):
- pass
-
-def check_file(file_name):
- # TODO: Check contents of certificate file
- if not os.path.exists(file_name):
- raise CmdctlException("'%s' does not exist" % file_name)
-
- if not os.path.isfile(file_name):
- raise CmdctlException("'%s' is not a file" % file_name)
-
- if not os.access(file_name, os.R_OK):
- raise CmdctlException("'%s' is not readable" % file_name)
-
-
-class SecureHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
- '''https connection request handler.
- Currently only GET and POST are supported. '''
- def __init__(self, request, client_address, server):
- http.server.BaseHTTPRequestHandler.__init__(self, request,
- client_address, server)
- self.session_id = None
-
- def do_GET(self):
- '''The client should send its session id in header with
- the name 'cookie'
- '''
- self.session_id = self.headers.get('cookie')
- rcode, reply = http.client.OK, []
- if self._is_session_valid():
- if self._is_user_logged_in():
- rcode, reply = self._handle_get_request()
- else:
- rcode, reply = http.client.UNAUTHORIZED, ["please login"]
- else:
- rcode = http.client.BAD_REQUEST
-
- self.send_response(rcode)
- self.end_headers()
- self.wfile.write(json.dumps(reply).encode())
-
- def _handle_get_request(self):
- '''Currently only support the following three url GET request '''
- id, module = self._parse_request_path()
- return self.server.get_reply_data_for_GET(id, module)
-
- def _is_session_valid(self):
- return self.session_id is not None
-
- def _is_user_logged_in(self):
- login_time = self.server.user_sessions.get(self.session_id)
- if not login_time:
- return False
-
- idle_time = time.time() - login_time
- if idle_time > self.server.idle_timeout:
- return False
- # Update idle time
- self.server.user_sessions[self.session_id] = time.time()
- return True
-
- def _parse_request_path(self):
- '''Parse the url, the legal url should like /ldh or /ldh/ldh '''
- groups = URL_PATTERN.match(self.path)
- if not groups:
- return (None, None)
- else:
- return (groups.group(1), groups.group(2))
-
- def do_POST(self):
- '''Process POST request. '''
- '''Process user login and send command to proper module
- The client should send its session id in header with
- the name 'cookie'
- '''
- self.session_id = self.headers.get('cookie')
- rcode, reply = http.client.OK, []
- if self._is_session_valid():
- if self.path == '/login':
- rcode, reply = self._handle_login()
- elif self._is_user_logged_in():
- rcode, reply = self._handle_post_request()
- else:
- rcode, reply = http.client.UNAUTHORIZED, ["please login"]
- else:
- rcode, reply = http.client.BAD_REQUEST, ["session isn't valid"]
-
- self.send_response(rcode)
- self.end_headers()
- self.wfile.write(json.dumps(reply).encode())
-
- def _handle_login(self):
- if self._is_user_logged_in():
- return http.client.OK, ["user has already login"]
- is_user_valid, error_info = self._check_user_name_and_pwd()
- if is_user_valid:
- self.server.save_user_session_id(self.session_id)
- return http.client.OK, ["login success"]
- else:
- return http.client.UNAUTHORIZED, error_info
-
- def _check_user_name_and_pwd(self):
- '''Check user name and its password '''
- length = self.headers.get('Content-Length')
-
- if not length:
- return False, ["invalid username or password"]
-
- try:
- user_info = json.loads((self.rfile.read(int(length))).decode())
- except:
- return False, ["invalid username or password"]
-
- user_name = user_info.get('username')
- if not user_name:
- return False, ["need user name"]
- if not self.server.get_user_info(user_name):
- logger.info(CMDCTL_NO_SUCH_USER, user_name)
- return False, ["username or password error"]
-
- user_pwd = user_info.get('password')
- if not user_pwd:
- return False, ["need password"]
- local_info = self.server.get_user_info(user_name)
- pwd_hashval = sha1((user_pwd + local_info[1]).encode())
- if pwd_hashval.hexdigest() != local_info[0]:
- logger.info(CMDCTL_BAD_PASSWORD, user_name)
- return False, ["username or password error"]
-
- return True, None
-
-
- def _handle_post_request(self):
- '''Handle all the post request from client. '''
- mod, cmd = self._parse_request_path()
- if (not mod) or (not cmd):
- return http.client.BAD_REQUEST, ['malformed url']
-
- param = None
- len = self.headers.get('Content-Length')
- if len:
- try:
- post_str = str(self.rfile.read(int(len)).decode())
- param = json.loads(post_str)
- except:
- pass
-
- rcode, reply = self.server.send_command_to_module(mod, cmd, param)
- ret = http.client.OK
- if rcode != 0:
- ret = http.client.BAD_REQUEST
- return ret, reply
-
- def log_request(self, code='-', size='-'):
- '''Rewrite the log request function, log nothing.'''
- pass
-
-
-class CommandControl():
- '''Get all modules' config data/specification from configmanager.
- receive command from client and resend it to proper module.
- '''
- def __init__(self, httpserver, verbose = False):
- ''' httpserver: the http server which use the object of
- CommandControl to communicate with other modules. '''
- self._verbose = verbose
- self._httpserver = httpserver
- self.__msg_handler_thread = None # set in _start_msg_handle_thread
- self._lock = threading.Lock()
- self._setup_session()
- self.modules_spec = self._get_modules_specification()
- self._config_data = self._get_config_data_from_config_manager()
- self._serving = True
- self._start_msg_handle_thread()
-
- def _setup_session(self):
- '''Setup the session for receving the commands
- sent from other modules. There are two sessions
- for cmdctl, one(self.module_cc) is used for receiving
- commands sent from other modules, another one (self._cc)
- is used to send the command from Bindctl or other tools
- to proper modules.'''
- self._cc = isc.cc.Session()
- self._module_cc = isc.config.ModuleCCSession(SPECFILE_LOCATION,
- self.config_handler,
- self.command_handler)
- self._module_name = self._module_cc.get_module_spec().get_module_name()
- self._cmdctl_config_data = self._module_cc.get_full_config()
- self._module_cc.start()
-
- def _accounts_file_check(self, filepath):
- ''' Check whether the accounts file is valid, each row
- should be a list with 3 items.'''
- csvfile = None
- errstr = None
- try:
- csvfile = open(filepath)
- reader = csv.reader(csvfile)
- for row in reader:
- a = (row[0], row[1], row[2])
- except (IOError, IndexError) as e:
- errstr = 'Invalid accounts file: ' + str(e)
- finally:
- if csvfile:
- csvfile.close()
-
- return errstr
-
- def _config_data_check(self, new_config):
- ''' Check whether the new config data is valid or
- not. '''
- errstr = None
- for key in new_config:
- if key == 'version':
- continue
- elif key in ['key_file', 'cert_file']:
- # TODO: we only check whether the file exist, is a
- # file, and is readable; but further check need to be done:
- # eg. whether the private/certificate is valid.
- path = new_config[key]
- try:
- check_file(path)
- except CmdctlException as cce:
- errstr = str(cce)
- elif key == 'accounts_file':
- errstr = self._accounts_file_check(new_config[key])
- else:
- errstr = 'unknown config item: ' + key
-
- if errstr != None:
- logger.error(CMDCTL_BAD_CONFIG_DATA, errstr);
- return ccsession.create_answer(1, errstr)
-
- return ccsession.create_answer(0)
-
- def config_handler(self, new_config):
- answer = self._config_data_check(new_config)
- rcode, val = ccsession.parse_answer(answer)
- if rcode != 0:
- return answer
-
- with self._lock:
- for key in new_config:
- if key in self._cmdctl_config_data:
- self._cmdctl_config_data[key] = new_config[key]
- return answer
-
- def _get_current_thread(self):
- """A simple wrapper of returning the 'current' thread object.
-
- This is extracted as a 'protected' method so tests can override for
- their convenience.
-
- """
- return threading.currentThread()
-
- def command_handler(self, command, args):
- """Handle commands from other modules.
-
- This method must not be called by any other threads than
- __msg_handler_thread invoked at the intialization of the class;
- otherwise it would cause critical race or dead locks.
-
- """
- # Check the restriction described above.
- assert self._get_current_thread() == self.__msg_handler_thread
-
- answer = ccsession.create_answer(0)
- if command == ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE:
- # The 'value' of a specification update can be either
- # a specification, or None. In the first case, simply
- # set it. If it is None, delete the module if it is
- # known.
- with self._lock:
- if args[1] is None:
- if args[0] in self.modules_spec:
- del self.modules_spec[args[0]]
- else:
- answer = ccsession.create_answer(1,
- 'No such module: ' +
- args[0])
- else:
- self.modules_spec[args[0]] = args[1]
-
- elif command == ccsession.COMMAND_SHUTDOWN:
- #When cmdctl get 'shutdown' command from b10-init,
- #shutdown the outer httpserver.
- self._module_cc.send_stopping()
- self._httpserver.shutdown()
- self._serving = False
-
- else:
- answer = ccsession.create_answer(1, 'unknown command: ' + command)
-
- return answer
-
- def _start_msg_handle_thread(self):
- ''' Start one thread to handle received message from msgq.'''
- td = threading.Thread(target=self._handle_msg_from_msgq)
- td.daemon = True
- self.__msg_handler_thread = td
- td.start()
-
- def _handle_msg_from_msgq(self):
- '''Process all the received commands with module session. '''
- while self._serving:
- self._module_cc.check_command(False)
-
- def _parse_command_result(self, rcode, reply):
- '''Ignore the error reason when command rcode isn't 0, '''
- if rcode != 0:
- return {}
- return reply
-
- def _get_config_data_from_config_manager(self):
- '''Get config data for all modules from configmanager '''
- rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_CONFIG)
- return self._parse_command_result(rcode, reply)
-
- def _update_config_data(self, module_name, command_name):
- '''Get lastest config data for all modules from configmanager '''
- if module_name == 'ConfigManager' and command_name == ccsession.COMMAND_SET_CONFIG:
- data = self._get_config_data_from_config_manager()
- with self._lock:
- self._config_data = data
-
- def get_config_data(self):
- with self._lock:
- data = self._config_data
- return data
-
- def get_modules_spec(self):
- with self._lock:
- spec = self.modules_spec
- return spec
-
- def _get_modules_specification(self):
- '''Get all the modules' specification files. '''
- rcode, reply = self.send_command('ConfigManager', ccsession.COMMAND_GET_MODULE_SPEC)
- return self._parse_command_result(rcode, reply)
-
- def send_command_with_check(self, module_name, command_name, params=None):
- '''Before send the command to modules, check if module_name, command_name
- parameters are legal according the spec file of the module.
- Return rcode, dict. TODO, the rcode should be defined properly.
- rcode = 0: dict is the correct returned value.
- rcode > 0: dict is : { 'error' : 'error reason' }
- '''
- # core module ConfigManager does not have a specification file
- if module_name == 'ConfigManager':
- return self.send_command(module_name, command_name, params)
-
- specs = self.get_modules_spec()
- if module_name not in specs.keys():
- return 1, {'error' : 'unknown module'}
-
- spec_obj = isc.config.module_spec.ModuleSpec(specs[module_name], False)
- errors = []
- if not spec_obj.validate_command(command_name, params, errors):
- return 1, {'error': errors[0]}
-
- return self.send_command(module_name, command_name, params)
-
- def send_command(self, module_name, command_name, params=None):
- """Send the command from bindctl to proper module.
-
- Note that commands sent to Cmdctl itself are also delivered via the
- CC session. Since this method is called from a thread handling a
- particular HTTP session, it cannot directly call command_handler().
-
- """
- errstr = 'unknown error'
- answer = None
- logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_SEND_COMMAND,
- command_name, module_name)
-
- # FIXME: Due to the fact that we use a separate session
- # from the module one (due to threads and blocking), and
- # because the plain cc session does not have the high-level
- # rpc-call method, we use the low-level way and create the
- # command ourselves.
- msg = ccsession.create_command(command_name, params)
- seq = self._cc.group_sendmsg(msg, module_name, want_answer=True)
- logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_COMMAND_SENT, command_name,
- module_name)
- # TODO, it may be blocked, msqg need to add a new interface waiting
- # in timeout.
- try:
- answer, env = self._cc.group_recvmsg(False, seq)
- except isc.cc.session.SessionTimeout:
- errstr = "Module '%s' not responding" % module_name
-
- if answer:
- try:
- rcode, arg = ccsession.parse_answer(answer)
- if rcode == 0:
- self._update_config_data(module_name, command_name)
- if arg != None:
- return rcode, arg
- else:
- return rcode, {}
- else:
- errstr = \
- str(answer[isc.cc.proto_defs.CC_PAYLOAD_RESULT][1])
- except ccsession.ModuleCCSessionError as mcse:
- errstr = str("Error in ccsession answer:") + str(mcse)
-
- logger.error(CMDCTL_COMMAND_ERROR, command_name, module_name, errstr)
- return 1, {'error': errstr}
-
- def get_cmdctl_config_data(self):
- ''' If running in source code tree, use keyfile, certificate
- and user accounts file in source code. '''
- if "B10_FROM_SOURCE" in os.environ:
- sysconf_path = os.environ["B10_FROM_SOURCE"] + "/src/bin/cmdctl/"
- accountsfile = sysconf_path + "cmdctl-accounts.csv"
- keyfile = sysconf_path + "cmdctl-keyfile.pem"
- certfile = sysconf_path + "cmdctl-certfile.pem"
- return (keyfile, certfile, accountsfile)
-
- with self._lock:
- keyfile = self._cmdctl_config_data.get('key_file')
- certfile = self._cmdctl_config_data.get('cert_file')
- accountsfile = self._cmdctl_config_data.get('accounts_file')
-
- return (keyfile, certfile, accountsfile)
-
-class SecureHTTPServer(socketserver_mixin.NoPollMixIn,
- socketserver.ThreadingMixIn,
- http.server.HTTPServer):
- '''Make the server address can be reused.'''
- allow_reuse_address = True
-
- def __init__(self, server_address, RequestHandlerClass,
- CommandControlClass,
- idle_timeout = 1200, verbose = False):
- '''idle_timeout: the max idle time for login'''
- socketserver_mixin.NoPollMixIn.__init__(self)
- try:
- http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
- logger.debug(DBG_CMDCTL_MESSAGING, CMDCTL_STARTED,
- server_address[0], server_address[1])
- except socket.error as err:
- if err.errno == errno.EADDRINUSE:
- raise CmdctlException(("Error creating server, because " +\
- "port %d on address %s is " +\
- "already in use. Please stop " +\
- "the application that uses it or " +\
- "see the guide about using a " +\
- "different port for b10-cmdctl.") % \
- (server_address[1], \
- server_address[0]))
- else:
- raise CmdctlException("Error creating server, because: %s" % str(err))
-
- self.user_sessions = {}
- self.idle_timeout = idle_timeout
- self.cmdctl = CommandControlClass(self, verbose)
- self._verbose = verbose
- self._lock = threading.Lock()
- self._user_infos = {}
- self.__accounts_file = None
- self.__accounts_file_mtime = 0
-
- def _create_user_info(self, accounts_file):
- '''Read all user's name and its' salt, hashed password
- from accounts file.'''
-
- # If the file does not exist, set accounts to empty, and return
- if not os.path.exists(accounts_file):
- self._user_infos = {}
- self.__accounts_file = None
- self.__accounts_file_mtime = 0
- return
-
- # If the filename hasn't changed, and the file itself
- # has neither, do nothing
- accounts_file_mtime = os.stat(accounts_file).st_mtime
- if self.__accounts_file == accounts_file and\
- accounts_file_mtime <= self.__accounts_file_mtime:
- return
-
- with self._lock:
- self._user_infos = {}
- csvfile = None
- try:
- csvfile = open(accounts_file)
- reader = csv.reader(csvfile)
- for row in reader:
- self._user_infos[row[0]] = [row[1], row[2]]
- except (IOError, IndexError) as e:
- logger.error(CMDCTL_USER_DATABASE_READ_ERROR,
- accounts_file, e)
- finally:
- if csvfile:
- csvfile.close()
-
- self.__accounts_file = accounts_file
- self.__accounts_file_mtime = accounts_file_mtime
- if len(self._user_infos) == 0:
- logger.error(CMDCTL_NO_USER_ENTRIES_READ)
-
- def get_user_info(self, username):
- '''Get user's salt and hashed string. If the user
- doesn't exist, return None, or else, the list
- [salt, hashed password] will be returned.'''
- with self._lock:
- info = self._user_infos.get(username)
- return info
-
- def save_user_session_id(self, session_id):
- ''' Record user's id and login time. '''
- self.user_sessions[session_id] = time.time()
-
- def _check_key_and_cert(self, key, cert):
- check_file(key)
- check_file(cert);
-
- def _wrap_socket_in_ssl_context(self, sock, key, cert):
- try:
- self._check_key_and_cert(key, cert)
- ssl_sock = ssl.wrap_socket(sock,
- server_side=True,
- certfile=cert,
- keyfile=key,
- ssl_version=ssl.PROTOCOL_SSLv23)
- # Return here (if control leaves this blocks it will raise an
- # error)
- return ssl_sock
- except ssl.SSLError as err:
- self.close_request(sock)
- logger.error(CMDCTL_SSL_SETUP_FAILURE_USER_DENIED, err)
- raise
- except (CmdctlException, IOError, socket.error) as cce:
- self.close_request(sock)
- logger.error(CMDCTL_SSL_SETUP_FAILURE_READING_CERT, cce)
- raise
-
- def get_request(self):
- '''Get client request socket and wrap it in SSL context. '''
- key, cert, account_file = self.cmdctl.get_cmdctl_config_data()
- self._create_user_info(account_file)
- newsocket, fromaddr = self.socket.accept()
- ssl_sock = self._wrap_socket_in_ssl_context(newsocket, key, cert)
- return (ssl_sock, fromaddr)
-
- def get_reply_data_for_GET(self, id, module):
- '''Currently only support the following three url GET request '''
- rcode, reply = http.client.NO_CONTENT, []
- if not module:
- if id == CONFIG_DATA_URL:
- rcode, reply = http.client.OK, self.cmdctl.get_config_data()
- elif id == MODULE_SPEC_URL:
- rcode, reply = http.client.OK, self.cmdctl.get_modules_spec()
-
- return rcode, reply
-
- def send_command_to_module(self, module_name, command_name, params):
- return self.cmdctl.send_command_with_check(module_name, command_name, params)
-
-httpd = None
-
-def signal_handler(signal, frame):
- if httpd:
- httpd.shutdown()
- sys.exit(0)
-
-def set_signal_handler():
- signal.signal(signal.SIGTERM, signal_handler)
- signal.signal(signal.SIGINT, signal_handler)
-
-def run(addr = 'localhost', port = 8080, idle_timeout = 1200, verbose = False):
- ''' Start cmdctl as one https server. '''
- httpd = SecureHTTPServer((addr, port), SecureHTTPRequestHandler,
- CommandControl, idle_timeout, verbose)
-
- httpd.serve_forever()
-
-def check_port(option, opt_str, value, parser):
- try:
- parser.values.port = isc.net.parse.port_parse(value)
- except ValueError as e:
- raise OptionValueError(str(e))
-
-def check_addr(option, opt_str, value, parser):
- try:
- isc.net.parse.addr_parse(value)
- parser.values.addr = value
- except ValueError as e:
- raise OptionValueError(str(e))
-
-def set_cmd_options(parser):
- parser.add_option('-p', '--port', dest = 'port', type = 'int',
- action = 'callback', callback=check_port,
- default = '8080', help = 'port cmdctl will use')
-
- parser.add_option('-a', '--address', dest = 'addr', type = 'string',
- action = 'callback', callback=check_addr,
- default = '127.0.0.1', help = 'IP address cmdctl will use')
-
- parser.add_option('-i', '--idle-timeout', dest = 'idle_timeout', type = 'int',
- default = '1200', help = 'login idle time out')
-
- parser.add_option("-v", "--verbose", dest="verbose", action="store_true", default=False,
- help="display more about what is going on")
-
-def main():
- set_signal_handler()
- parser = OptionParser(version = __version__)
- set_cmd_options(parser)
- (options, args) = parser.parse_args()
- result = 1 # in case of failure
- try:
- if options.verbose:
- logger.set_severity("DEBUG", 99)
- run(options.addr, options.port, options.idle_timeout, options.verbose)
- result = 0
- except isc.cc.SessionError as err:
- logger.fatal(CMDCTL_CC_SESSION_ERROR, err)
- except isc.cc.SessionTimeout:
- logger.fatal(CMDCTL_CC_SESSION_TIMEOUT)
- except KeyboardInterrupt:
- logger.info(CMDCTL_STOPPED_BY_KEYBOARD)
- except CmdctlException as err:
- logger.fatal(CMDCTL_UNCAUGHT_EXCEPTION, err);
-
- if httpd:
- httpd.shutdown()
-
- logger.info(CMDCTL_EXITING)
-
- sys.exit(result)
-
-if __name__ == '__main__':
- isc.util.traceback_handler.traceback_handler(main)
+++ /dev/null
-{
- "module_spec": {
- "module_name": "Cmdctl",
- "module_description": "Interface for command and control",
- "config_data": [
- {
- "item_name": "key_file",
- "item_type": "string",
- "item_optional": false,
- "item_default": "@@SYSCONFDIR@@/@PACKAGE@/cmdctl-keyfile.pem"
- },
- {
- "item_name": "cert_file",
- "item_type": "string",
- "item_optional": false,
- "item_default": "@@SYSCONFDIR@@/@PACKAGE@/cmdctl-certfile.pem"
- },
- {
- "item_name": "accounts_file",
- "item_type": "string",
- "item_optional": false,
- "item_default": "@@SYSCONFDIR@@/@PACKAGE@/cmdctl-accounts.csv"
- }
- ],
- "commands": [
- {
- "command_name": "shutdown",
- "command_description": "shutdown cmdctl",
- "command_args": [
- {
- "item_name": "pid",
- "item_type": "integer",
- "item_optional": true
- }
- ]
- }
- ]
- }
-}
-
-
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-# PERFORMANCE OF THIS SOFTWARE.
-
-# No namespace declaration - these constants go in the global namespace
-# of the cmdctl_messages python module.
-
-% CMDCTL_BAD_CONFIG_DATA error in config data: %1
-There was an error reading the updated configuration data. The specific
-error is printed.
-
-% CMDCTL_BAD_PASSWORD bad password for user: %1
-A login attempt was made to b10-cmdctl, but the password was wrong.
-Users can be managed with the tool b10-cmdctl-usermgr.
-
-% CMDCTL_CC_SESSION_ERROR error reading from cc channel: %1
-There was a problem reading from the command and control channel. The
-most likely cause is that the message bus daemon is not running.
-
-% CMDCTL_CC_SESSION_TIMEOUT timeout on cc channel
-A timeout occurred when waiting for essential data from the cc session.
-This usually occurs when b10-cfgmgr is not running or not responding.
-Since we are waiting for essential information, this is a fatal error,
-and the cmdctl daemon will now shut down.
-
-% CMDCTL_COMMAND_ERROR error in command %1 to module %2: %3
-An error was encountered sending the given command to the given module.
-Either there was a communication problem with the module, or the module
-was not able to process the command, and sent back an error. The
-specific error is printed in the message.
-
-% CMDCTL_COMMAND_SENT command '%1' to module '%2' was sent
-This debug message indicates that the given command has been sent to
-the given module.
-
-% CMDCTL_EXITING exiting
-The b10-cmdctl daemon is exiting.
-
-% CMDCTL_NO_SUCH_USER username not found in user database: %1
-A login attempt was made to b10-cmdctl, but the username was not known.
-Users can be added with the tool b10-cmdctl-usermgr.
-
-% CMDCTL_NO_USER_ENTRIES_READ failed to read user information, all users will be denied
-The b10-cmdctl daemon was unable to find any user data in the user
-database file. Either it was unable to read the file (in which case
-this message follows a message CMDCTL_USER_DATABASE_READ_ERROR
-containing a specific error), or the file was empty. Users can be added
-with the tool b10-cmdctl-usermgr.
-
-% CMDCTL_SEND_COMMAND sending command %1 to module %2
-This debug message indicates that the given command is being sent to
-the given module.
-
-% CMDCTL_SSL_SETUP_FAILURE_READING_CERT failed to read certificate or key: %1
-The b10-cmdctl daemon is unable to read either the certificate file or
-the private key file, and is therefore unable to accept any SSL connections.
-The specific error is printed in the message.
-The administrator should solve the issue with the files, or recreate them
-with the b10-certgen tool.
-
-% CMDCTL_SSL_SETUP_FAILURE_USER_DENIED failed to create an SSL connection (user denied): %1
-The user was denied because the SSL connection could not successfully
-be set up. The specific error is given in the log message. Possible
-causes may be that the ssl request itself was bad, or the local key or
-certificate file could not be read.
-
-% CMDCTL_STARTED cmdctl is listening for connections on %1:%2
-The cmdctl daemon has started and is now listening for connections.
-
-% CMDCTL_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
-There was a keyboard interrupt signal to stop the cmdctl daemon. The
-daemon will now shut down.
-
-% CMDCTL_UNCAUGHT_EXCEPTION uncaught exception: %1
-The b10-cmdctl daemon encountered an uncaught exception and
-will now shut down. This is indicative of a programming error and
-should not happen under normal circumstances. The exception message
-is printed.
-
-% CMDCTL_USER_DATABASE_READ_ERROR failed to read user database file %1: %2
-The b10-cmdctl daemon was unable to read the user database file. The
-file may be unreadable for the daemon, or it may be corrupted. In the
-latter case, it can be recreated with b10-cmdctl-usermgr. The specific
-error is printed in the log message.
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-CMD_CTRLD_PATH=@abs_top_builddir@/src/bin/cmdctl
-PYTHONPATH=@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python/isc/cc:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/xfr/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/python/isc/config:@abs_top_builddir@/src/lib/python/isc/acl/.libs:@abs_top_builddir@/src/lib/python/isc/datasrc/.libs
-export PYTHONPATH
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-SET_ENV_LIBRARY_PATH=@SET_ENV_LIBRARY_PATH@
-if test $SET_ENV_LIBRARY_PATH = yes; then
- @ENV_LIBRARY_PATH@=@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/cryptolink/.libs:@abs_top_builddir@/src/lib/cc/.libs:@abs_top_builddir@/src/lib/config/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/util/.libs:@abs_top_builddir@/src/lib/util/threads/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/exceptions/.libs:@abs_top_builddir@/src/lib/datasrc/.libs:$@ENV_LIBRARY_PATH@
- export @ENV_LIBRARY_PATH@
-fi
-
-BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
-export BIND10_MSGQ_SOCKET_FILE
-
-cd ${CMD_CTRLD_PATH}
-exec ${PYTHON_EXEC} b10-cmdctl "$@"
+++ /dev/null
-/cmdctl_test
+++ /dev/null
-PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
-PYTESTS = cmdctl_test.py b10-certgen_test.py
-EXTRA_DIST = $(PYTESTS)
-EXTRA_DIST += testdata/expired-certfile.pem
-EXTRA_DIST += testdata/mangled-certfile.pem
-EXTRA_DIST += testdata/noca-certfile.pem
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-CLEANFILES = test-keyfile.pem test-certfile.pem
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/cmdctl \
- CMDCTL_SRC_PATH=$(abs_top_srcdir)/src/bin/cmdctl \
- CMDCTL_BUILD_PATH=$(abs_top_builddir)/src/bin/cmdctl \
- B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
+++ /dev/null
-# Copyright (C) 2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# Note: the main code is in C++, but what we are mostly testing is
-# options and behaviour (output/file creation, etc), which is easier
-# to test in python.
-
-import unittest
-import os
-from subprocess import call
-import subprocess
-import ssl
-import stat
-
-def run(command):
- """
- Small helper function that returns a tuple of (rcode, stdout, stderr) after
- running the given command (an array of command and arguments, as passed on
- to subprocess).
- """
- subp = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- (stdout, stderr) = subp.communicate()
- return (subp.returncode, stdout, stderr)
-
-class FileDeleterContext:
- """
- Simple Context Manager that deletes a given set of files when the context
- is left.
- """
- def __init__(self, files):
- self.files = files
-
- def __enter__(self):
- pass
-
- def __exit__(self, type, value, traceback):
- for f in self.files:
- if os.path.exists(f):
- os.unlink(f)
-
-class FilePermissionContext:
- """
- Simple Context Manager that temporarily modifies file permissions for
- a given file
- """
- def __init__(self, f, unset_flags = [], set_flags = []):
- """
- Initialize file permission context.
- See the stat module for possible flags to set or unset.
- The flags are changed when the context is entered (i.e.
- you can create the context first without any change)
- The flags are changed back when the context is left.
-
- Parameters:
- f: string, file to change permissions for
- unset_flags: list of flags to unset
- set_flags: list of flags to set
- """
- self.file = f
- self.orig_mode = os.stat(f).st_mode
- new_mode = self.orig_mode
- for flag in unset_flags:
- new_mode = new_mode & ~flag
- for flag in set_flags:
- new_mode = new_mode | flag
- self.new_mode = new_mode
-
- def __enter__(self):
- os.chmod(self.file, self.new_mode)
-
- def __exit__(self, type, value, traceback):
- os.chmod(self.file, self.orig_mode)
-
-def read_file_data(filename):
- """
- Simple text file reader that returns its contents as an array
- """
- with open(filename) as f:
- return f.readlines()
-
-class TestCertGenTool(unittest.TestCase):
- TOOL = '../b10-certgen'
-
- def run_check(self, expected_returncode, expected_stdout, expected_stderr, command):
- """
- Runs the given command, and checks return code, and outputs (if provided).
- Arguments:
- expected_returncode, return code of the command
- expected_stdout, (multiline) string that is checked agains stdout.
- May be None, in which case the check is skipped.
- expected_stderr, (multiline) string that is checked agains stderr.
- May be None, in which case the check is skipped.
- """
- (returncode, stdout, stderr) = run(command)
- self.assertEqual(expected_returncode, returncode, " ".join(command))
- if expected_stdout is not None:
- self.assertEqual(expected_stdout, stdout.decode())
- if expected_stderr is not None:
- self.assertEqual(expected_stderr, stderr.decode())
-
- def validate_certificate(self, expected_result, certfile):
- """
- Validate a certificate, using the quiet option of the tool; it runs
- the check option (-c) for the given base name of the certificate (-f
- <certfile>), and compares the return code to the given
- expected_result value
- """
- self.run_check(expected_result, '', '',
- [self.TOOL, '-q', '-c', certfile])
- # Same with long options
- self.run_check(expected_result, '', '',
- [self.TOOL, '--quiet', '--certfile', certfile])
-
-
- def test_basic_creation(self):
- """
- Tests whether basic creation with no arguments (except output
- file name) successfully creates a key and certificate
- """
- keyfile = 'test-keyfile.pem'
- certfile = 'test-certfile.pem'
- command = [ self.TOOL, '-q', '-w', '-c', certfile, '-k', keyfile ]
- self.creation_helper(command, certfile, keyfile)
- # Do same with long options
- command = [ self.TOOL, '--quiet', '--write', '--certfile=' + certfile, '--keyfile=' + keyfile ]
- self.creation_helper(command, certfile, keyfile)
-
- def creation_helper(self, command, certfile, keyfile):
- """
- Helper method for test_basic_creation.
- Performs the actual checks
- """
- with FileDeleterContext([keyfile, certfile]):
- self.assertFalse(os.path.exists(keyfile))
- self.assertFalse(os.path.exists(certfile))
- self.run_check(0, '', '', command)
- self.assertTrue(os.path.exists(keyfile))
- self.assertTrue(os.path.exists(certfile))
-
- # Validate the certificate that was just created
- self.validate_certificate(0, certfile)
-
- # When run with the same options, it should *not* create it again,
- # as the current certificate should still be valid
- certdata = read_file_data(certfile)
- keydata = read_file_data(keyfile)
-
- self.run_check(0, '', '', command)
-
- self.assertEqual(certdata, read_file_data(certfile))
- self.assertEqual(keydata, read_file_data(keyfile))
-
- # but if we add -f, it should force a new creation
- command.append('-f')
- self.run_check(0, '', '', command)
- self.assertNotEqual(certdata, read_file_data(certfile))
- self.assertNotEqual(keydata, read_file_data(keyfile))
-
- def test_check_bad_certificates(self):
- """
- Tests a few pre-created certificates with the -c option
- """
- path = os.environ['CMDCTL_SRC_PATH'] + '/tests/testdata/'
- self.validate_certificate(10, path + 'expired-certfile.pem')
- self.validate_certificate(100, path + 'mangled-certfile.pem')
- self.validate_certificate(17, path + 'noca-certfile.pem')
-
- def test_bad_options(self):
- """
- Tests some combinations of commands that should fail.
- """
- # specify -c but not -k
- self.run_check(101,
- 'Error: keyfile and certfile must both be specified '
- 'if one of them is when calling b10-certgen in write '
- 'mode.\n',
- '', [self.TOOL, '-w', '-c', 'foo'])
- self.run_check(101,
- 'Error: keyfile and certfile must both be specified '
- 'if one of them is when calling b10-certgen in write '
- 'mode.\n',
- '', [self.TOOL, '-w', '-k', 'foo'])
- self.run_check(101,
- 'Error: keyfile is not used when not in write mode\n',
- '', [self.TOOL, '-k', 'foo'])
- # Extraneous argument
- self.run_check(101, None, None, [self.TOOL, 'foo'])
- # No such file
- self.run_check(105, None, None, [self.TOOL, '-c', 'foo'])
-
- @unittest.skipIf(os.getuid() == 0,
- 'test cannot be run as root user')
- def test_permissions(self):
- """
- Test some combinations of correct and bad permissions.
- """
- keyfile = 'mod-keyfile.pem'
- certfile = 'mod-certfile.pem'
- command = [ self.TOOL, '-q', '-w', '-c', certfile, '-k', keyfile ]
- # Delete them at the end
- with FileDeleterContext([keyfile, certfile]):
- # Create the two files first
- self.run_check(0, '', '', command)
- self.validate_certificate(0, certfile)
-
- # Make the key file unwritable
- with FilePermissionContext(keyfile, unset_flags = [stat.S_IWUSR]):
- self.run_check(106, '', '', command)
- # Should have no effect on validation
- self.validate_certificate(0, certfile)
-
- # Make the cert file unwritable
- with FilePermissionContext(certfile, unset_flags = [stat.S_IWUSR]):
- self.run_check(106, '', '', command)
- # Should have no effect on validation
- self.validate_certificate(0, certfile)
-
- # Make the key file unreadable (this should not matter)
- with FilePermissionContext(keyfile, unset_flags = [stat.S_IRUSR]):
- self.run_check(0, '', '', command)
-
- # unreadable key file should also not have any effect on
- # validation
- self.validate_certificate(0, certfile)
-
- # Make the cert file unreadable (this should matter)
- with FilePermissionContext(certfile, unset_flags = [stat.S_IRUSR]):
- self.run_check(106, '', '', command)
-
- # Unreadable cert file should also fail validation
- self.validate_certificate(106, certfile)
-
- # Not directly a permission problem, but trying to check or create
- # in a nonexistent directory returns different error codes
- self.validate_certificate(105, 'fakedir/cert')
- self.run_check(103, '', '', [ self.TOOL, '-q', '-w', '-c',
- 'fakedir/cert', '-k', 'fakedir/key' ])
-
-if __name__== '__main__':
- unittest.main()
-
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-CMDCTL_TEST_PATH=@abs_top_srcdir@/src/bin/cmdctl/tests
-PYTHONPATH=@abs_top_srcdir@/src/bin/cmdctl
-export PYTHONPATH
-
-cd ${CMDCTL_TEST_PATH}
-exec ${PYTHON_EXEC} -O cmdctl_test.py $*
-
+++ /dev/null
-# Copyright (C) 2009 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-
-import unittest
-import ssl, socket
-import tempfile
-import time
-import stat
-import sys
-from cmdctl import *
-import isc.log
-
-assert 'CMDCTL_SRC_PATH' in os.environ,\
- "Please run this test with 'make check'"
-SRC_FILE_PATH = os.environ['CMDCTL_SRC_PATH'] + os.sep
-
-assert 'CMDCTL_BUILD_PATH' in os.environ,\
- "Please run this test with 'make check'"
-BUILD_FILE_PATH = os.environ['CMDCTL_BUILD_PATH'] + os.sep
-
-# Rewrite the class for unittest.
-class MySecureHTTPRequestHandler(SecureHTTPRequestHandler):
- def __init__(self):
- self.session_id = None
-
- def send_response(self, rcode):
- self.rcode = rcode
-
- def end_headers(self):
- pass
-
-class FakeSecureHTTPServer(SecureHTTPServer):
- def __init__(self):
- self.user_sessions = {}
- self.cmdctl = FakeCommandControlForTestRequestHandler()
- self._verbose = True
- self._user_infos = {}
- self.idle_timeout = 1200
- self._lock = threading.Lock()
-
-class FakeCommandControlForTestRequestHandler(CommandControl):
- def __init__(self):
- self._config_data = {}
- self.modules_spec = {}
- self._lock = threading.Lock()
-
- def send_command(self, mod, cmd, param):
- return 0, {}
-
-# context to temporarily make a file unreadable
-class UnreadableFile:
- def __init__(self, file_name):
- self.file_name = file_name
- self.orig_mode = os.stat(file_name).st_mode
-
- def __enter__(self):
- os.chmod(self.file_name, self.orig_mode & ~stat.S_IRUSR)
-
- def __exit__(self, type, value, traceback):
- os.chmod(self.file_name, self.orig_mode)
-
-class TmpTextFile:
- """
- Context class for temporarily creating a text file with some
- lines of content.
-
- The file is automatically deleted if the context is left, so
- make sure to not use the path of an existing file!
- """
- def __init__(self, path, contents):
- self.__path = path
- self.__contents = contents
-
- def __enter__(self):
- with open(self.__path, 'w') as f:
- f.write("\n".join(self.__contents) + "\n")
-
- def __exit__(self, type, value, traceback):
- os.unlink(self.__path)
-
-
-class TestSecureHTTPRequestHandler(unittest.TestCase):
- def setUp(self):
- self.old_stdout = sys.stdout
- sys.stdout = open(os.devnull, 'w')
- self.handler = MySecureHTTPRequestHandler()
- self.handler.server = FakeSecureHTTPServer()
- self.handler.server.user_sessions = {}
- self.handler.server._user_infos = {}
- self.handler.headers = {}
- self.handler.rfile = open('input.tmp', 'w+b')
- self.handler.wfile = open('output.tmp', 'w+b')
-
- def tearDown(self):
- sys.stdout.close()
- sys.stdout = self.old_stdout
- self.handler.wfile.close()
- os.remove('output.tmp')
- self.handler.rfile.close()
- os.remove('input.tmp')
-
- def test_is_session_valid(self):
- self.assertIsNone(self.handler.session_id)
- self.assertFalse(self.handler._is_session_valid())
- self.handler.session_id = 4234
- self.assertTrue(self.handler._is_session_valid())
-
- def test_parse_request_path(self):
- self.handler.path = ''
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == None) and (cmd == None))
-
- self.handler.path = '/abc'
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == 'abc') and (cmd == None))
-
- self.handler.path = '/abc/edf'
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == 'abc') and (cmd == 'edf'))
-
- self.handler.path = '/abc/edf/ghi'
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == 'abc') and (cmd == 'edf'))
-
- def test_parse_request_path_1(self):
- self.handler.path = '/ab*c'
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == 'ab') and cmd == None)
-
- self.handler.path = '/abc/ed*fdd/ddd'
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == 'abc') and cmd == 'ed')
-
- self.handler.path = '/-*/edfdd/ddd'
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == None) and (cmd == None))
-
- self.handler.path = '/-*/edfdd/ddd'
- mod, cmd = self.handler._parse_request_path()
- self.assertTrue((mod == None) and (cmd == None))
-
- def test_do_GET(self):
- self.handler.do_GET()
- self.assertEqual(self.handler.rcode, http.client.BAD_REQUEST)
-
- def test_do_GET_1(self):
- self.handler.headers['cookie'] = 12345
- self.handler.do_GET()
- self.assertEqual(self.handler.rcode, http.client.UNAUTHORIZED)
-
- def test_do_GET_2(self):
- self.handler.headers['cookie'] = 12345
- self.handler.server.user_sessions[12345] = time.time() + 1000000
- self.handler.path = '/how/are'
- self.handler.do_GET()
- self.assertEqual(self.handler.rcode, http.client.NO_CONTENT)
-
- def test_do_GET_3(self):
- self.handler.headers['cookie'] = 12346
- self.handler.server.user_sessions[12346] = time.time() + 1000000
- path_vec = ['config_data', 'module_spec']
- for path in path_vec:
- self.handler.path = '/' + path
- self.handler.do_GET()
- self.assertEqual(self.handler.rcode, http.client.OK)
-
- def test_is_user_logged_in(self):
- self.handler.server.user_sessions = {}
- self.handler.session_id = 12345
- self.assertTrue(self.handler._is_user_logged_in() == False)
-
- self.handler.server.user_sessions[12345] = time.time()
- self.assertTrue(self.handler._is_user_logged_in())
-
- self.handler.server.user_sessions[12345] = time.time() - 1500
- self.handler.idle_timeout = 1200
- self.assertTrue(self.handler._is_user_logged_in() == False)
-
- def test_check_user_name_and_pwd(self):
- self.handler.headers = {}
- ret, msg = self.handler._check_user_name_and_pwd()
- self.assertFalse(ret)
- self.assertEqual(msg, ['invalid username or password'])
-
- def test_check_user_name_and_pwd_1(self):
- user_info = {'username':'root', 'password':'abc123'}
- len = self.handler.rfile.write(json.dumps(user_info).encode())
- self.handler.headers['Content-Length'] = len
- self.handler.rfile.seek(0, 0)
-
- self.handler.server._user_infos['root'] = ['aa', 'aaa']
- ret, msg = self.handler._check_user_name_and_pwd()
- self.assertFalse(ret)
- self.assertEqual(msg, ['username or password error'])
-
- def test_check_user_name_and_pwd_2(self):
- user_info = {'username':'root', 'password':'abc123'}
- len = self.handler.rfile.write(json.dumps(user_info).encode())
- self.handler.headers['Content-Length'] = len - 1
- self.handler.rfile.seek(0, 0)
-
- ret, msg = self.handler._check_user_name_and_pwd()
- self.assertFalse(ret)
- self.assertEqual(msg, ['invalid username or password'])
-
- def test_check_user_name_and_pwd_3(self):
- user_info = {'usernae':'root', 'password':'abc123'}
- len = self.handler.rfile.write(json.dumps(user_info).encode())
- self.handler.headers['Content-Length'] = len
- self.handler.rfile.seek(0, 0)
-
- ret, msg = self.handler._check_user_name_and_pwd()
- self.assertFalse(ret)
- self.assertEqual(msg, ['need user name'])
-
- def test_check_user_name_and_pwd_4(self):
- user_info = {'username':'root', 'pssword':'abc123'}
- len = self.handler.rfile.write(json.dumps(user_info).encode())
- self.handler.headers['Content-Length'] = len
- self.handler.rfile.seek(0, 0)
-
- self.handler.server._user_infos['root'] = ['aa', 'aaa']
- ret, msg = self.handler._check_user_name_and_pwd()
- self.assertFalse(ret)
- self.assertEqual(msg, ['need password'])
-
- def test_check_user_name_and_pwd_5(self):
- user_info = {'username':'root', 'password':'abc123'}
- len = self.handler.rfile.write(json.dumps(user_info).encode())
- self.handler.headers['Content-Length'] = len
- self.handler.rfile.seek(0, 0)
-
- ret, msg = self.handler._check_user_name_and_pwd()
- self.assertFalse(ret)
- self.assertEqual(msg, ['username or password error'])
-
- def test_do_POST(self):
- self.handler.headers = {}
- self.handler.do_POST()
- self.assertEqual(self.handler.rcode, http.client.BAD_REQUEST)
-
- def test_do_POST_1(self):
- self.handler.headers = {}
- self.handler.headers['cookie'] = 12345
- self.handler.path = '/'
- self.handler.do_POST()
- self.assertEqual(self.handler.rcode, http.client.UNAUTHORIZED)
-
- def test_handle_post_request(self):
- self.handler.path = '/cfgmgr/revert'
- self.handler.headers = {}
- rcode, reply = self.handler._handle_post_request()
- self.assertEqual(http.client.BAD_REQUEST, rcode)
-
- def test_handle_post_request_1(self):
- self.handler.path = '/*d/revert'
- self.handler.headers = {}
- rcode, reply = self.handler._handle_post_request()
- self.assertEqual(http.client.BAD_REQUEST, rcode)
-
- def _gen_module_spec(self):
- spec = { 'commands': [
- { 'command_name' :'command',
- 'command_args': [ {
- 'item_name' : 'param1',
- 'item_type' : 'integer',
- 'item_optional' : False,
- 'item_default' : 0
- } ],
- 'command_description' : 'cmd description'
- }
- ]
- }
-
- return spec
-
- def test_handle_post_request_2(self):
- params = {'param1':123}
- len = self.handler.rfile.write(json.dumps(params).encode())
- self.handler.headers['Content-Length'] = len
-
- self.handler.rfile.seek(0, 0)
- self.handler.path = '/module/command'
- self.handler.server.cmdctl.modules_spec = {}
- self.handler.server.cmdctl.modules_spec['module'] = self._gen_module_spec()
- rcode, reply = self.handler._handle_post_request()
- self.assertEqual(http.client.OK, rcode)
-
- def test_handle_post_request_3(self):
- params = {'param1':'abc'}
- len = self.handler.rfile.write(json.dumps(params).encode())
- self.handler.headers['Content-Length'] = len
-
- self.handler.rfile.seek(0, 0)
- self.handler.path = '/module/command'
- self.handler.server.cmdctl.modules_spec = {}
- self.handler.server.cmdctl.modules_spec['module'] = self._gen_module_spec()
- rcode, reply = self.handler._handle_post_request()
- self.assertEqual(http.client.BAD_REQUEST, rcode)
-
- def test_handle_login(self):
- orig_is_user_logged_in = self.handler._is_user_logged_in
- orig_check_user_name_and_pwd = self.handler._check_user_name_and_pwd
- try:
- def create_is_user_logged_in(status):
- '''Create a replacement _is_user_logged_in() method.'''
- def my_is_user_logged_in():
- return status
- return my_is_user_logged_in
-
- # Check case where _is_user_logged_in() returns True
- self.handler._is_user_logged_in = create_is_user_logged_in(True)
- self.handler.headers['cookie'] = 12345
- self.handler.path = '/login'
- self.handler.do_POST()
- self.assertEqual(self.handler.rcode, http.client.OK)
- self.handler.wfile.seek(0, 0)
- d = self.handler.wfile.read()
- self.assertEqual(json.loads(d.decode()),
- ['user has already login'])
-
- # Clear the output
- self.handler.wfile.seek(0, 0)
- self.handler.wfile.truncate()
-
- # Check case where _is_user_logged_in() returns False
- self.handler._is_user_logged_in = create_is_user_logged_in(False)
-
- def create_check_user_name_and_pwd(status, error_info=None):
- '''Create a replacement _check_user_name_and_pwd() method.'''
- def my_check_user_name_and_pwd():
- return status, error_info
- return my_check_user_name_and_pwd
-
- # (a) Check case where _check_user_name_and_pwd() returns
- # valid user status
- self.handler._check_user_name_and_pwd = \
- create_check_user_name_and_pwd(True)
- self.handler.do_POST()
- self.assertEqual(self.handler.rcode, http.client.OK)
- self.handler.wfile.seek(0, 0)
- d = self.handler.wfile.read()
- self.assertEqual(json.loads(d.decode()), ['login success'])
-
- # Clear the output
- self.handler.wfile.seek(0, 0)
- self.handler.wfile.truncate()
-
- # (b) Check case where _check_user_name_and_pwd() returns
- # invalid user status
- self.handler._check_user_name_and_pwd = \
- create_check_user_name_and_pwd(False, ['login failed'])
- self.handler.do_POST()
- self.assertEqual(self.handler.rcode, http.client.UNAUTHORIZED)
- self.handler.wfile.seek(0, 0)
- d = self.handler.wfile.read()
- self.assertEqual(json.loads(d.decode()), ['login failed'])
-
- finally:
- self.handler._is_user_logged_in = orig_is_user_logged_in
- self.handler._check_user_name_and_pwd = orig_check_user_name_and_pwd
-
-class MockSession:
- """Act like isc.cc.Session, stealing group_sendmsg/recvmsg().
-
- The initial simple version only records given parameters in
- group_sendmsg() for later inspection and raise a timeout exception
- from recvmsg(). As we see the need for more test cases these methods
- should be extended.
-
- """
- def __init__(self, sent_messages):
- self.__sent_messages = sent_messages
-
- def group_sendmsg(self, msg, module_name, want_answer):
- self.__sent_messages.append((msg, module_name))
-
- def group_recvmsg(self, nonblock, seq):
- raise isc.cc.session.SessionTimeout('dummy timeout')
-
-class MyCommandControl(CommandControl):
- def __init__(self, httpserver, verbose):
- super().__init__(httpserver, verbose)
- self.sent_messages = [] # for inspection; allow tests to see it
- self._cc = MockSession(self.sent_messages)
-
- def _get_modules_specification(self):
- return {}
-
- def _get_config_data_from_config_manager(self):
- return {}
-
- def _setup_session(self):
- spec_file = BUILD_FILE_PATH + 'cmdctl.spec'
- module_spec = isc.config.module_spec_from_file(spec_file)
- config = isc.config.config_data.ConfigData(module_spec)
- self._module_name = 'Cmdctl'
- self._cmdctl_config_data = config.get_full_config()
-
- def _handle_msg_from_msgq(self):
- pass
-
- def _start_msg_handle_thread(self): # just not bother to be threads
- pass
-
- def _get_current_thread(self):
- return None
-
-class TestCommandControl(unittest.TestCase):
-
- def setUp(self):
- self.old_stdout = sys.stdout
- sys.stdout = open(os.devnull, 'w')
- self.cmdctl = MyCommandControl(None, True)
-
- def tearDown(self):
- sys.stdout.close()
- sys.stdout = self.old_stdout
-
- def _check_config(self, cmdctl):
- key, cert, account = cmdctl.get_cmdctl_config_data()
- self.assertIsNotNone(key)
- self.assertIsNotNone(cert)
- self.assertIsNotNone(account)
-
- def test_get_cmdctl_config_data(self):
- old_env = os.environ
- if 'B10_FROM_SOURCE' in os.environ:
- del os.environ['B10_FROM_SOURCE']
- self.cmdctl.get_cmdctl_config_data()
- self._check_config(self.cmdctl)
- os.environ = old_env
-
- old_env = os.environ
- os.environ['B10_FROM_SOURCE'] = '../'
- self._check_config(self.cmdctl)
- os.environ = old_env
-
- def test_parse_command_result(self):
- self.assertEqual({}, self.cmdctl._parse_command_result(1, {'error' : 1}))
- self.assertEqual({'a': 1}, self.cmdctl._parse_command_result(0, {'a' : 1}))
-
- def _check_answer(self, answer, rcode_, msg_):
- rcode, msg = ccsession.parse_answer(answer)
- self.assertEqual(rcode, rcode_)
- self.assertEqual(msg, msg_)
-
- def test_command_handler(self):
- answer = self.cmdctl.command_handler('unknown-command', None)
- self._check_answer(answer, 1, 'unknown command: unknown-command')
-
- # Send a real command. Mock stuff so the shutdown command doesn't
- # cause an exception.
- class ModuleCC:
- def send_stopping():
- pass
- self.cmdctl._module_cc = ModuleCC
- called = []
- class Server:
- def shutdown():
- called.append('shutdown')
- self.cmdctl._httpserver = Server
- answer = self.cmdctl.command_handler('shutdown', None)
- rcode, msg = ccsession.parse_answer(answer)
- self.assertEqual(rcode, 0)
- self.assertIsNone(msg)
- self.assertEqual(['shutdown'], called)
-
- def test_command_handler_spec_update(self):
- # Should not be present
- self.assertFalse("foo" in self.cmdctl.modules_spec)
-
- answer = self.cmdctl.command_handler(
- ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE, [ "foo", {} ])
- rcode, msg = ccsession.parse_answer(answer)
- self.assertEqual(rcode, 0)
- self.assertEqual(msg, None)
-
- # Should now be present
- self.assertTrue("foo" in self.cmdctl.modules_spec)
-
- # When sending specification 'None', it should be removed
- answer = self.cmdctl.command_handler(
- ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE, [ "foo", None ])
- rcode, msg = ccsession.parse_answer(answer)
- self.assertEqual(rcode, 0)
- self.assertEqual(msg, None)
-
- # Should no longer be present
- self.assertFalse("foo" in self.cmdctl.modules_spec)
-
- # Don't store 'None' if it wasn't there in the first place!
- answer = self.cmdctl.command_handler(
- ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE, [ "foo", None ])
- rcode, msg = ccsession.parse_answer(answer)
- self.assertEqual(rcode, 1)
- self.assertEqual(msg, "No such module: foo")
-
- # Should still not present
- self.assertFalse("foo" in self.cmdctl.modules_spec)
-
-
- def test_check_config_handler(self):
- answer = self.cmdctl.config_handler({'non-exist': 123})
- self._check_answer(answer, 1, 'unknown config item: non-exist')
-
- old_env = os.environ
- os.environ['B10_FROM_SOURCE'] = '../'
- self._check_config(self.cmdctl)
- os.environ = old_env
-
- answer = self.cmdctl.config_handler({'key_file': '/user/non-exist_folder'})
- self._check_answer(answer, 1, "'/user/non-exist_folder' does not exist")
-
- answer = self.cmdctl.config_handler({'cert_file': '/user/non-exist_folder'})
- self._check_answer(answer, 1, "'/user/non-exist_folder' does not exist")
-
- answer = self.cmdctl.config_handler({'accounts_file': '/user/non-exist_folder'})
- self._check_answer(answer, 1,
- "Invalid accounts file: [Errno 2] No such file or directory: '/user/non-exist_folder'")
-
- # Test with invalid accounts file
- file_name = 'tmp.account.file'
- temp_file = open(file_name, 'w')
- writer = csv.writer(temp_file)
- writer.writerow(['a', 'b'])
- temp_file.close()
- answer = self.cmdctl.config_handler({'accounts_file': file_name})
- self._check_answer(answer, 1, "Invalid accounts file: list index out of range")
- os.remove(file_name)
-
- def test_send_command(self):
- # Send a command to other module. We check an expected message
- # is sent via the session (cmdct._cc). Due to the behavior of
- # our mock session object the anser will be "fail", but it's not
- # the subject of this test, and so it's okay.
- # TODO: more detailed cases should be tested.
- rcode, value = self.cmdctl.send_command('Init', 'shutdown', None)
- self.assertEqual(1, len(self.cmdctl.sent_messages))
- self.assertEqual(({'command': ['shutdown']}, 'Init'),
- self.cmdctl.sent_messages[-1])
- self.assertEqual(1, rcode)
-
- # Send a command to cmdctl itself. Should be the same effect.
- rcode, value = self.cmdctl.send_command('Cmdctl', 'shutdown',
- None)
- self.assertEqual(2, len(self.cmdctl.sent_messages))
- self.assertEqual(({'command': ['shutdown']}, 'Cmdctl'),
- self.cmdctl.sent_messages[-1])
- self.assertEqual(1, rcode)
-
-class MySecureHTTPServer(SecureHTTPServer):
- def server_bind(self):
- pass
-
-class TestSecureHTTPServer(unittest.TestCase):
- def setUp(self):
- self.old_stdout = sys.stdout
- self.old_stderr = sys.stderr
- sys.stdout = open(os.devnull, 'w')
- sys.stderr = sys.stdout
- self.server = MySecureHTTPServer(('localhost', 8080),
- MySecureHTTPRequestHandler,
- MyCommandControl, verbose=True)
-
- def tearDown(self):
- # both sys.stdout and sys.stderr are the same, so closing one is
- # sufficient
- sys.stdout.close()
- sys.stdout = self.old_stdout
- sys.stderr = self.old_stderr
-
- def test_addr_in_use(self):
- server_one = None
- try:
- server_one = SecureHTTPServer(('localhost', 53531),
- MySecureHTTPRequestHandler,
- MyCommandControl)
- except CmdctlException:
- pass
- else:
- self.assertRaises(CmdctlException, SecureHTTPServer,
- ('localhost', 53531),
- MySecureHTTPRequestHandler, MyCommandControl)
- if server_one:
- server_one.server_close()
-
- def test_create_user_info(self):
- self.server._create_user_info('/local/not-exist')
- self.assertEqual(0, len(self.server._user_infos))
-
- self.server._create_user_info(SRC_FILE_PATH + 'cmdctl-accounts.csv')
- self.assertEqual(1, len(self.server._user_infos))
- self.assertTrue('root' in self.server._user_infos)
-
- def test_get_user_info(self):
- self.assertIsNone(self.server.get_user_info('root'))
- self.server._create_user_info(SRC_FILE_PATH + 'cmdctl-accounts.csv')
- self.assertIn('6f0c73bd33101a5ec0294b3ca39fec90ef4717fe',
- self.server.get_user_info('root'))
-
- # When the file is not changed calling _create_user_info() again
- # should have no effect. In order to test this, we overwrite the
- # user-infos that were just set and make sure it isn't touched by
- # the call (so make sure it isn't set to some empty value)
- fake_users_val = { 'notinfile': [] }
- self.server._user_infos = fake_users_val
- self.server._create_user_info(SRC_FILE_PATH + 'cmdctl-accounts.csv')
- self.assertEqual(fake_users_val, self.server._user_infos)
-
- def test_create_user_info_changing_file_time(self):
- self.assertEqual(0, len(self.server._user_infos))
-
- # Create a file
- accounts_file = BUILD_FILE_PATH + 'new_file.csv'
- with TmpTextFile(accounts_file, ['root,foo,bar']):
- self.server._create_user_info(accounts_file)
- self.assertEqual(1, len(self.server._user_infos))
- self.assertTrue('root' in self.server._user_infos)
-
- # Make sure re-reading is a noop if file was not modified
- fake_users_val = { 'notinfile': [] }
- self.server._user_infos = fake_users_val
- self.server._create_user_info(accounts_file)
- self.assertEqual(fake_users_val, self.server._user_infos)
-
- # create the file again, this time read should not be a noop
- with TmpTextFile(accounts_file, ['otherroot,foo,bar']):
- # Set mtime in future
- stat = os.stat(accounts_file)
- os.utime(accounts_file, (stat.st_atime, stat.st_mtime + 10))
- self.server._create_user_info(accounts_file)
- self.assertEqual(1, len(self.server._user_infos))
- self.assertTrue('otherroot' in self.server._user_infos)
-
- def test_create_user_info_changing_file_name(self):
- """
- Check that the accounts file is re-read if the file name is different
- """
- self.assertEqual(0, len(self.server._user_infos))
-
- # Create two files
- accounts_file1 = BUILD_FILE_PATH + 'new_file.csv'
- accounts_file2 = BUILD_FILE_PATH + 'new_file2.csv'
- with TmpTextFile(accounts_file2, ['otherroot,foo,bar']):
- with TmpTextFile(accounts_file1, ['root,foo,bar']):
- self.server._create_user_info(accounts_file1)
- self.assertEqual(1, len(self.server._user_infos))
- self.assertTrue('root' in self.server._user_infos)
-
- # Make sure re-reading is a noop if file was not modified
- fake_users_val = { 'notinfile': [] }
- self.server._user_infos = fake_users_val
- self.server._create_user_info(accounts_file1)
- self.assertEqual(fake_users_val, self.server._user_infos)
-
- # But a different file should be read
- self.server._create_user_info(accounts_file2)
- self.assertEqual(1, len(self.server._user_infos))
- self.assertTrue('otherroot' in self.server._user_infos)
-
- def test_create_user_info_nonexistent_file(self):
- # Even if there was data initially, if set to a nonexistent
- # file it should result in no users
- accounts_file = BUILD_FILE_PATH + 'new_file.csv'
- self.assertFalse(os.path.exists(accounts_file))
- fake_users_val = { 'notinfile': [] }
- self.server._user_infos = fake_users_val
- self.server._create_user_info(accounts_file)
- self.assertEqual({}, self.server._user_infos)
-
- # Should it now be created it should be read
- with TmpTextFile(accounts_file, ['root,foo,bar']):
- self.server._create_user_info(accounts_file)
- self.assertEqual(1, len(self.server._user_infos))
- self.assertTrue('root' in self.server._user_infos)
-
- def test_check_file(self):
- # Just some file that we know exists
- file_name = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
- check_file(file_name)
- self.assertRaises(CmdctlException, check_file, '/local/not-exist')
- self.assertRaises(CmdctlException, check_file, '/')
-
- @unittest.skipIf(os.getuid() == 0,
- 'test cannot be run as root user')
- def test_check_file_for_unreadable(self):
- file_name = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
- with UnreadableFile(file_name):
- self.assertRaises(CmdctlException, check_file, file_name)
-
- def test_check_key_and_cert(self):
- keyfile = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
- certfile = BUILD_FILE_PATH + 'cmdctl-certfile.pem'
-
- # no exists
- self.assertRaises(CmdctlException, self.server._check_key_and_cert,
- keyfile, '/local/not-exist')
- self.assertRaises(CmdctlException, self.server._check_key_and_cert,
- '/local/not-exist', certfile)
-
- # not a file
- self.assertRaises(CmdctlException, self.server._check_key_and_cert,
- keyfile, '/')
- self.assertRaises(CmdctlException, self.server._check_key_and_cert,
- '/', certfile)
-
- # All OK (also happens to check the context code above works)
- self.server._check_key_and_cert(keyfile, certfile)
-
- @unittest.skipIf(os.getuid() == 0,
- 'test cannot be run as root user')
- def test_check_key_and_cert_for_unreadable(self):
- keyfile = BUILD_FILE_PATH + 'cmdctl-keyfile.pem'
- certfile = BUILD_FILE_PATH + 'cmdctl-certfile.pem'
-
- # no read permission
- with UnreadableFile(certfile):
- self.assertRaises(CmdctlException,
- self.server._check_key_and_cert,
- keyfile, certfile)
-
- with UnreadableFile(keyfile):
- self.assertRaises(CmdctlException,
- self.server._check_key_and_cert,
- keyfile, certfile)
-
- def test_wrap_sock_in_ssl_context(self):
- sock = socket.socket()
-
- # Bad files should result in a CmdctlException in the basic file
- # checks
- self.assertRaises(CmdctlException,
- self.server._wrap_socket_in_ssl_context,
- sock,
- 'no_such_file', 'no_such_file')
-
- # Using a non-certificate file would cause an SSLError
- self.assertRaises(socket.error,
- self.server._wrap_socket_in_ssl_context,
- sock,
- BUILD_FILE_PATH + 'cmdctl.py',
- BUILD_FILE_PATH + 'cmdctl-certfile.pem')
-
- # Should succeed
- sock1 = socket.socket()
- ssl_sock = self.server._wrap_socket_in_ssl_context(sock1,
- BUILD_FILE_PATH + 'cmdctl-keyfile.pem',
- BUILD_FILE_PATH + 'cmdctl-certfile.pem')
- self.assertTrue(isinstance(ssl_sock, ssl.SSLSocket))
-
- # wrap_socket can also raise IOError, which should be caught and
- # handled like the other errors.
- # Force this by temporarily disabling our own file checks
- orig_check_func = self.server._check_key_and_cert
- try:
- self.server._check_key_and_cert = lambda x,y: None
- self.assertRaises(IOError,
- self.server._wrap_socket_in_ssl_context,
- sock,
- 'no_such_file', 'no_such_file')
- finally:
- self.server._check_key_and_cert = orig_check_func
-
-class TestFuncNotInClass(unittest.TestCase):
- def test_check_port(self):
- self.assertRaises(OptionValueError, check_port, None, 'port', -1, None)
- self.assertRaises(OptionValueError, check_port, None, 'port', 65536, None)
- self.assertRaises(OptionValueError, check_addr, None, 'ipstr', 'a.b.d', None)
- self.assertRaises(OptionValueError, check_addr, None, 'ipstr', '1::0:a.b', None)
-
-
-if __name__== "__main__":
- isc.log.resetUnitTestRootLogger()
- unittest.main()
-
-
+++ /dev/null
------BEGIN CERTIFICATE-----
-MIIDhzCCAvCgAwIBAgIJALwngNFik7ONMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD
-VQQGEwJjbjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwG
-A1UEChMFY25uaWMxDjAMBgNVBAsTBWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3Vu
-MSIwIAYJKoZIhvcNAQkBFhN6aGFuZ2xpa3VuQGNubmljLmNuMB4XDTEwMDEwNzEy
-NDcxOFoXDTExMDEwNzEyNDcxOFowgYoxCzAJBgNVBAYTAmNuMRAwDgYDVQQIEwdi
-ZWlqaW5nMRAwDgYDVQQHEwdiZWlqaW5nMQ4wDAYDVQQKEwVjbm5pYzEOMAwGA1UE
-CxMFY25uaWMxEzARBgNVBAMTCnpoYW5nbGlrdW4xIjAgBgkqhkiG9w0BCQEWE3po
-YW5nbGlrdW5AY25uaWMuY24wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOkg
-JbEkYoy9SEsU9t/mfxlaiCqNhxCqqgeodVEdiPKJ7LoVl21mRjazWBiHQbQ1e2Ka
-UiCJz68RwV7u92bIqe1bsNgNqoCPQqsQPtEoCPzfbiM1tIke0s/h6+8l6ne+yg21
-O825x5Anjq+6THLGCDcO4L2RWo+4PwJnVGrgBPKLAgMBAAGjgfIwge8wHQYDVR0O
-BBYEFJKM/O0ViGlwtb3JEci/DLTO/7DaMIG/BgNVHSMEgbcwgbSAFJKM/O0ViGlw
-tb3JEci/DLTO/7DaoYGQpIGNMIGKMQswCQYDVQQGEwJjbjEQMA4GA1UECBMHYmVp
-amluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwGA1UEChMFY25uaWMxDjAMBgNVBAsT
-BWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3VuMSIwIAYJKoZIhvcNAQkBFhN6aGFu
-Z2xpa3VuQGNubmljLmNuggkAvCeA0WKTs40wDAYDVR0TBAUwAwEB/zANBgkqhkiG
-9w0BAQUFAAOBgQBh5N6isMAQAFFD+pbfpppjQlO4vUNcEdzPdeuBFaf9CsX5ZdxV
-jmn1ZuGm6kRzqUPwPSxvCIAY0wuSu1g7YREPAZ3XBVwcg6262iGOA6n7E+nv5PLz
-EuZ1oUg+IfykUIoflKH6xZB4MyPL+EgkMT+i9BrngaXHXF8tEO30YppMiA==
------END CERTIFICATE-----
+++ /dev/null
------BEGIN CERTIFICATE-----
-MIIDhzCCAvCgAwIBAgIJALwngNFik7ONMA0GCSqGSIb3DQEBBQUAMIGKMQswCQYD
-VQQGEwJjbjEQMA4GA1UECBMHYmVpamluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwG
-A1UEChMFY25uaWMxDjAMBgNVBAsTBWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3Vu
-MSIwIAYJKoZIhvcNAQkBFhN6aGFuZ2xpa3VuQGNubmljLmNuMB4XDTEwMDEwNzEy
-NDcxOFoXDTExMDEwNzEyNDcxOFowgYoxCzAJBgNVBAYTAmNuMRAwDgYDVQQIEwdi
-ZWlqaW5nMraWDgYDVQQHEwdiZWlqaW5nMQ4wDAYDVQQKEwVjbm5pYzEOMAwGA1UE
-CxMFY25uaWMxeZaRBgNVBAMTCnpoYW5nbGlrdW4xIjAgBgkqhkiG9w0BCQEWE3po
-YW5nbGlrdW5AY25UAwMuY24wgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAOkg
-JbEkYoy9SEsU9t/mfxLAICqNhxCqqgeodVEdiPKJ7LoVl21mRjazWBiHQbQ1e2Ka
-UiCJz68RwV7u92bIqe1bsNgNqoCPQqsQPtEoCPzfbiM1tIke0s/h6+8l6ne+yg21
-O825x5Anjq+6THLGCDcO4L2RWo+4PwJnVGrgBPKLAgMBAAGjgfIwge8wHQYDVR0O
-BBYEFJKM/O0ViGlwtb3JEci/DLTO/7DaMIG/BgNVHSMEgbcwgbSAFJKM/O0ViGlw
-tb3JEci/DLTO/7DaoYGQpIGNMIGKMQswCQYDVQQGEwJjbjEQMA4GA1UECBMHYmVp
-amluZzEQMA4GA1UEBxMHYmVpamluZzEOMAwGA1UEChMFY25uaWMxDjAMBgNVBAsT
-BWNubmljMRMwEQYDVQQDEwp6aGFuZ2xpa3VuMSIwIAYJKoZIhvcNAQkBFhN6aGFu
-Z2xpa3VuQGNubmljLmNuggkAvCeA0WKTs40wDAYDVR0TBAUwAwEB/zANBgkqhkiG
-9w0BAQUFAAOBgQBh5N6isMAQAFFD+pbfpppjQlO4vUNcEdzPdeuBFaf9CsX5ZdxV
-jmn1ZuGm6kRzqUPwPSxvCIAY0wuSu1g7YREPAZ3XBVwcg6262iGOA6n7E+nv5PLz
-EuZ1oUg+IfykUIoflKH6xZB4MyPL+EgkMT+i9BrngaXHXF8tEO30YppMiA==
------END CERTIFICATE-----
+++ /dev/null
------BEGIN CERTIFICATE-----
-MIIDBjCCAe6gAwIBAgIRALUIj3nnW5uDE/+fglPvUDwwDQYJKoZIhvcNAQELBQAw
-HjELMAkGA1UEBhMCVVMxDzANBgNVBAMTBkJJTkQxMDAeFw0xMjExMTQxMjQ5MjVa
-Fw0xMzExMTQxMjQ5MjVaMB4xCzAJBgNVBAYTAlVTMQ8wDQYDVQQDEwZCSU5EMTAw
-ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDkIOPfs3Aw9kNDu1JqA2w3
-84/n9oUgAwAlHVmuJv7ZDw1MDaIKHjsh3DW09z+nv67GVksI7pFtAw5O4mnTDxpa
-JT0NKzhvYGfe8VdV/hWDogTIdk1QBJNZ2/id8z0h8z5001sARXPf+4mHBJslenH3
-YtZs22BG5RBLULtZ/2Nr7JkdfLlc6D5PCoDG22r1OiFkYVdCWfLDjisVIbSYPBtY
-BlKAIrvbmOtWcaGM+vQAhl0T5N8WRCKhaQH0DEmzQNckkYd7rSECo57KYiuvOdzp
-d+3bWTgGGy2ff0o3LZypv0O5s0TDC2H6hYtN4bUbcChUJbFu9b5sVZaOEVZtUsyD
-AgMBAAGjPzA9MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgTwMB0GA1UdDgQW
-BBSqGzsEDNs9E7gBL5pD6XVAwUo4DTANBgkqhkiG9w0BAQsFAAOCAQEAMTNB8NCU
-dnLFZ0jNpvecbECkX/OWGlBYU4/CsoNiibwp4CtUYS2A4NFVjWAyuzLSHhRQi0vJ
-CCWLpKL4VTkaDN5Oft42iUhvEXMnriJqpfXHnjCiBwFFSPl5WKfMIaRNK+tF4zbB
-F+FGNEEmYG3t/ni82orDLq4oy+7CoQwzZNzj5yoV6q7O9kLR9OMPNwJrc27A4erB
-7VMRZslSrNA4uA6YhMZl8iEvO1H801ct0zTxawrCihPOZOCSLew35xjztO7d3YH8
-YavOu5kzeu7AgZ2n75H/qU47ZgBjbonn9Osvrct+RIwZuWTB2bDML8JhNaZCq0aA
-TDBC0QWqIYypLg==
------END CERTIFICATE-----
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-
-PYTESTS = d2_test.py
-EXTRA_DIST = $(PYTESTS)
-
# Explicitly specify paths to dynamic libraries required by loadable python
# modules. That is required on Mac OS systems. Otherwise we will get exception
# about python not being able to load liblog library.
LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
endif
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
- B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
- $(LIBRARY_PATH_PLACEHOLDER) \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
-
AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
AM_CPPFLAGS += -I$(top_srcdir)/src/bin
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-
-PYTESTS = dhcp4_test.py
-EXTRA_DIST = $(PYTESTS)
# Explicitly specify paths to dynamic libraries required by loadable python
# modules. That is required on Mac OS systems. Otherwise we will get exception
LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
endif
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
- B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
- $(LIBRARY_PATH_PLACEHOLDER) \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
-
AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib
AM_CPPFLAGS += -I$(top_builddir)/src/bin # for generated spec_config.h header
AM_CPPFLAGS += -I$(top_srcdir)/src/bin
+++ /dev/null
-# Copyright (C) 2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-from init import ProcessInfo
-
-import unittest
-import sys
-import os
-import signal
-import socket
-from isc.net.addr import IPAddr
-import time
-import isc
-import fcntl
-
-class TestDhcpv4Daemon(unittest.TestCase):
- def setUp(self):
- # Don't redirect stdout/stderr here as we want to print out things
- # during the test
- #
- # However, we do want to set the logging lock directory to somewhere
- # to which we can write - use the current working directory. We then
- # set the appropriate environment variable. os.putenv() may be not
- # supported on some platforms as suggested in
- # http://docs.python.org/release/3.2/library/os.html?highlight=putenv#os.environ:
- # "If the platform supports the putenv() function...". It was checked
- # that it does not work on Ubuntu. To overcome this problem we access
- # os.environ directly.
- lockdir_envvar = "B10_LOCKFILE_DIR_FROM_BUILD"
- if lockdir_envvar not in os.environ:
- os.environ[lockdir_envvar] = os.getcwd()
-
- def tearDown(self):
- pass
-
- def readPipe(self, pipe_fd):
- """
- Reads bytes from a pipe and returns a character string. If nothing is
- read, or if there is an error, an empty string is returned.
-
- pipe_fd - Pipe file descriptor to read
- """
- try:
- data = os.read(pipe_fd, 16384)
- # Make sure we have a string
- if (data is None):
- data = ""
- else:
- data = str(data)
- except OSError:
- data = ""
-
- return data
-
- def runCommand(self, params, wait=1):
- """
- This method runs a command and returns a tuple: (returncode, stdout, stderr)
- """
- ## @todo: Convert this into generic method and reuse it in dhcp4 and dhcp6
-
- print("Running command: %s" % (" ".join(params)))
-
- # redirect stdout to a pipe so we can check that our
- # process spawning is doing the right thing with stdout
- self.stdout_old = os.dup(sys.stdout.fileno())
- self.stdout_pipes = os.pipe()
- os.dup2(self.stdout_pipes[1], sys.stdout.fileno())
- os.close(self.stdout_pipes[1])
-
- # do the same trick for stderr:
- self.stderr_old = os.dup(sys.stderr.fileno())
- self.stderr_pipes = os.pipe()
- os.dup2(self.stderr_pipes[1], sys.stderr.fileno())
- os.close(self.stderr_pipes[1])
-
- # note that we use dup2() to restore the original stdout
- # to the main program ASAP in each test... this prevents
- # hangs reading from the child process (as the pipe is only
- # open in the child), and also insures nice pretty output
-
- pi = ProcessInfo('Test Process', params)
- pi.spawn()
- time.sleep(wait)
- os.dup2(self.stdout_old, sys.stdout.fileno())
- os.dup2(self.stderr_old, sys.stderr.fileno())
- self.assertNotEqual(pi.process, None)
- self.assertTrue(type(pi.pid) is int)
-
- # Set non-blocking read on pipes. Process may not print anything
- # on specific output and the we would hang without this.
- fd = self.stdout_pipes[0]
- fl = fcntl.fcntl(fd, fcntl.F_GETFL)
- fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
-
- fd = self.stderr_pipes[0]
- fl = fcntl.fcntl(fd, fcntl.F_GETFL)
- fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
-
- # As we don't know how long the subprocess will take to start and
- # produce output, we'll loop and sleep for 250 ms between each
- # iteration. To avoid an infinite loop, we'll loop for a maximum
- # of five seconds: that should be enough.
- for count in range(20):
- # Read something from stderr and stdout (these reads don't block).
- output = self.readPipe(self.stdout_pipes[0])
- error = self.readPipe(self.stderr_pipes[0])
-
- # If the process has already exited, or if it has output something,
- # quit the loop now.
- if pi.process.poll() is not None or len(error) > 0 or len(output) > 0:
- break
-
- # Process still running, try again in 250 ms.
- time.sleep(0.25)
-
- # Exited loop, kill the process if it is still running
- if pi.process.poll() is None:
- try:
- pi.process.terminate()
- except OSError:
- print("Ignoring failed kill attempt. Process is dead already.")
-
- # call this to get returncode, process should be dead by now
- rc = pi.process.wait()
-
- # Clean up our stdout/stderr munging.
- os.dup2(self.stdout_old, sys.stdout.fileno())
- os.close(self.stdout_old)
- os.close(self.stdout_pipes[0])
-
- os.dup2(self.stderr_old, sys.stderr.fileno())
- os.close(self.stderr_old)
- os.close(self.stderr_pipes[0])
-
- # Free up resources (file descriptors) from the ProcessInfo object
- # TODO: For some reason, this gives an error if the process has ended,
- # although it does cause all descriptors still allocated to the
- # object to be freed.
- pi = None
-
- print ("Process finished, return code=%d, stdout=%d bytes, stderr=%d bytes"
- % (rc, len(output), len(error)) )
-
- return (rc, output, error)
-
- def test_alive(self):
- print("Note: Purpose of some of the tests is to check if DHCPv4 server can be started,")
- print(" not that is can bind sockets correctly. Please ignore binding errors.")
-
- (returncode, output, error) = self.runCommand(["../b10-dhcp4", "-v"])
- output_text = str(output) + str(error)
- self.assertEqual(output_text.count("DHCP4_STARTING"), 1)
-
- def test_portnumber_0(self):
- print("Check that specifying port number 0 is not allowed.")
-
- (returncode, output, error) = self.runCommand(['../b10-dhcp4', '-v', '-p', '0'])
-
- # When invalid port number is specified, return code must not be success
- self.assertTrue(returncode != 0)
-
- # Check that there is an error message about invalid port number printed on stderr
- self.assertEqual( str(error).count("Failed to parse port number"), 1)
-
- def test_portnumber_missing(self):
- print("Check that -p option requires a parameter.")
-
- (returncode, output, error) = self.runCommand(['../b10-dhcp4', '-p'])
-
- # When invalid port number is specified, return code must not be success
- self.assertTrue(returncode != 0)
-
- # Check that there is an error message about invalid port number printed on stderr
- self.assertEqual( str(error).count("option requires an argument"), 1)
-
- def test_portnumber_invalid1(self):
- print("Check that -p option is check against bogus port number (999999).")
-
- (returncode, output, error) = self.runCommand(['../b10-dhcp4', '-p','999999'])
-
- # When invalid port number is specified, return code must not be success
- self.assertTrue(returncode != 0)
-
- # Check that there is an error message about invalid port number printed on stderr
- self.assertEqual( str(error).count("Failed to parse port number"), 1)
-
- def test_portnumber_invalid2(self):
- print("Check that -p option is check against bogus port number (123garbage).")
-
- (returncode, output, error) = self.runCommand(['../b10-dhcp4', '-p','123garbage'])
-
- # When invalid port number is specified, return code must not be success
- self.assertTrue(returncode != 0)
-
- # Check that there is an error message about invalid port number printed on stderr
- self.assertEqual( str(error).count("Failed to parse port number"), 1)
-
-if __name__ == '__main__':
- unittest.main()
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = dhcp6_test.py
SHTESTS =
# The test of dynamic reconfiguration based on signals will work only
# if we are using file based configuration approach.
SHTESTS += dhcp6_sigterm_test.sh
SHTESTS += dhcp6_sigint_test.sh
endif
-EXTRA_DIST = $(PYTESTS) $(SHTESTS)
+EXTRA_DIST = $(SHTESTS)
# Explicitly specify paths to dynamic libraries required by loadable python
# modules. That is required on Mac OS systems. Otherwise we will get exception
# test using command-line arguments, so use check-local target instead of TESTS
check-local:
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_srcdir)/src/bin:$(abs_top_builddir)/src/bin/bind10:$(abs_top_builddir)/src/lib/util/io/.libs \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
-
for shtest in $(SHTESTS) ; do \
echo Running test: $$shtest ; \
export B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir); \
+++ /dev/null
-/b10-msgq
-/msgq.py
-/run_msgq.sh
-/b10-msgq.8
+++ /dev/null
-SUBDIRS = . tests
-
-pkglibexecdir = $(libexecdir)/@PACKAGE@
-
-pkglibexec_SCRIPTS = b10-msgq
-
-b10_msgqdir = $(pkgdatadir)
-b10_msgq_DATA = msgq.spec
-
-CLEANFILES = b10-msgq msgq.pyc
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/msgq_messages.py
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/msgq_messages.pyc
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/msgq_messages.pyo
-
-man_MANS = b10-msgq.8
-DISTCLEANFILES = $(man_MANS)
-EXTRA_DIST = $(man_MANS) msgq.xml msgq_messages.mes msgq.spec
-
-nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/msgq_messages.py
-pylogmessagedir = $(pyexecdir)/isc/log_messages/
-BUILT_SOURCES = $(PYTHON_LOGMSGPKG_DIR)/work/msgq_messages.py
-
-if GENERATE_DOCS
-
-b10-msgq.8: msgq.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/msgq.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 BIND 10 > $@
-
-endif
-
-# Define rule to build logging source files from message file
-$(PYTHON_LOGMSGPKG_DIR)/work/msgq_messages.py : msgq_messages.mes
- $(top_builddir)/src/lib/log/compiler/message \
- -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/msgq_messages.mes
-
-# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
-b10-msgq: msgq.py
- $(SED) "s|@@PYTHONPATH@@|@pyexecdir@|" msgq.py >$@
- chmod a+x $@
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-== TODO for MsgQ
-
-* The code could use some refactoring.
-
-* Refactor: Right now, the MsgQ class manages each connection using several
-lists. There should be a Client class which holds most of this per-client
-data. Some will need to remain in the main "driver" MsgQ class (such as the
-list of clients and their lnames) but buffer management, reading from a
-socket, etc. could all go into this client class.
-
-* More tests: A test to send to strange locations should be added, to test
-that code path (a bug which I am committing with this file...)
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-import sys; sys.path.append ('@@PYTHONPATH@@')
-
-"""This code implements the msgq daemon."""
-
-import subprocess
-import signal
-import os
-import socket
-import sys
-import struct
-import errno
-import time
-import select
-import random
-import threading
-import isc.config.ccsession
-from optparse import OptionParser, OptionValueError
-import isc.util.process
-import isc.util.traceback_handler
-from isc.cc.proto_defs import *
-import isc.log
-from isc.log_messages.msgq_messages import *
-
-import isc.cc
-
-isc.util.process.rename()
-
-isc.log.init("b10-msgq", buffer=True)
-# Logger that is used in the actual msgq handling - startup, shutdown and the
-# poller thread.
-logger = isc.log.Logger("msgq")
-# A separate copy for the master/config thread when the poller thread runs.
-# We use a separate instance, since the logger itself doesn't have to be
-# thread safe.
-config_logger = isc.log.Logger("msgq")
-TRACE_START = logger.DBGLVL_START_SHUT
-TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
-TRACE_DETAIL = logger.DBGLVL_TRACE_DETAIL
-
-# This is the version that gets displayed to the user.
-# The VERSION string consists of the module name, the module version
-# number, and the overall BIND 10 version number (set in configure.ac).
-VERSION = "b10-msgq 20110127 (BIND 10 @PACKAGE_VERSION@)"
-
-# If B10_FROM_BUILD is set in the environment, we use data files
-# from a directory relative to that, otherwise we use the ones
-# installed on the system
-if "B10_FROM_SOURCE" in os.environ:
- SPECFILE_PATH = os.environ["B10_FROM_SOURCE"] + "/src/bin/msgq"
-else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- SPECFILE_PATH = "@datadir@/@PACKAGE@".replace("${datarootdir}",
- DATAROOTDIR). \
- replace("${prefix}", PREFIX)
-SPECFILE_LOCATION = SPECFILE_PATH + "/msgq.spec"
-
-class MsgQReceiveError(Exception): pass
-
-class MsgQRunningError(Exception): pass
-
-class MsgQCloseOnReceive(Exception):
- """Exception raised when reading data from a socket results in 'shutdown'.
-
- This happens when msgq received 0-length data. This class holds whether
- it happens in the middle of reading (i.e. after reading some) via
- partial_read parameter, which is set to True if and only if so.
- This will be used by an upper layer catching the exception to distinguish
- the severity of the event.
-
- """
- def __init__(self, reason, partial_read):
- self.partial_read = partial_read
- self.__reason = reason
-
- def __str__(self):
- return self.__reason
-
-class SubscriptionManager:
- def __init__(self, cfgmgr_ready):
- """
- Initialize the subscription manager.
- parameters:
- * cfgmgr_ready: A callable object run once the config manager
- subscribes. This is a hackish solution, but we can't read
- the configuration sooner.
- """
- self.subscriptions = {}
- self.__cfgmgr_ready = cfgmgr_ready
- self.__cfgmgr_ready_called = False
-
- def subscribe(self, group, instance, socket):
- """Add a subscription."""
- target = ( group, instance )
- if target in self.subscriptions:
- logger.debug(TRACE_BASIC, MSGQ_SUBS_APPEND_TARGET, group, instance)
- if socket not in self.subscriptions[target]:
- self.subscriptions[target].append(socket)
- else:
- logger.debug(TRACE_BASIC, MSGQ_SUBS_NEW_TARGET, group, instance)
- self.subscriptions[target] = [ socket ]
- if group == "ConfigManager" and not self.__cfgmgr_ready_called:
- logger.debug(TRACE_BASIC, MSGQ_CFGMGR_SUBSCRIBED)
- self.__cfgmgr_ready_called = True
- self.__cfgmgr_ready()
-
- def unsubscribe(self, group, instance, socket):
- """Remove the socket from the one specific subscription."""
- target = ( group, instance )
- if target in self.subscriptions:
- if socket in self.subscriptions[target]:
- self.subscriptions[target].remove(socket)
- return True
- return False
-
- def unsubscribe_all(self, socket):
- """Remove the socket from all subscriptions."""
- removed_from = []
- for subs, socklist in self.subscriptions.items():
- if socket in socklist:
- socklist.remove(socket)
- removed_from.append(subs)
- return removed_from
-
- def find_sub(self, group, instance):
- """Return an array of sockets which want this specific group,
- instance."""
- target = (group, instance)
- if target in self.subscriptions:
- return self.subscriptions[target]
- else:
- return []
-
- def find(self, group, instance):
- """Return an array of sockets who should get something sent to
- this group, instance pair. This includes wildcard subscriptions."""
- target = (group, instance)
- partone = self.find_sub(group, instance)
- parttwo = self.find_sub(group, CC_INSTANCE_WILDCARD)
- return list(set(partone + parttwo))
-
-class MsgQ:
- """Message Queue class."""
- # did we find a better way to do this?
- SOCKET_FILE = os.path.join("@localstatedir@",
- "@PACKAGE_NAME@",
- "msgq_socket").replace("${prefix}",
- "@prefix@")
-
- def __init__(self, socket_file=None, verbose=False):
- """Initialize the MsgQ master.
-
- The socket_file specifies the path to the UNIX domain socket
- that the msgq process listens on. If it is None, the
- environment variable BIND10_MSGQ_SOCKET_FILE is used. If that
- is not set, it will default to
- @localstatedir@/@PACKAGE_NAME@/msg_socket.
- If verbose is True, then the MsgQ reports
- what it is doing.
- """
-
- if socket_file is None:
- if "BIND10_MSGQ_SOCKET_FILE" in os.environ:
- self.socket_file = os.environ["BIND10_MSGQ_SOCKET_FILE"]
- else:
- self.socket_file = self.SOCKET_FILE
- else:
- self.socket_file = socket_file
-
- self.verbose = verbose
- self.runnable = False
- self.listen_socket = False
- self.sockets = {}
- self.connection_counter = random.random()
- self.hostname = socket.gethostname()
- self.subs = SubscriptionManager(self.cfgmgr_ready)
- self.lnames = {}
- self.fd_to_lname = {}
- self.sendbuffs = {}
- self.running = False
- self.__cfgmgr_ready = None
- self.__cfgmgr_ready_cond = threading.Condition()
- # A lock used when the message queue does anything more complicated.
- # It is mostly a safety measure, the threads doing so should be mostly
- # independent, and the one with config session should be read only,
- # but with threads, one never knows. We use threads for concurrency,
- # not for performance, so we use wide lock scopes to be on the safe
- # side.
- self.__lock = threading.Lock()
- self._session = None
- self.__poller_sock = None
-
- def members_notify(self, event, params):
- """
- Thin wrapper around ccs's notify. Send a notification about change
- of some list that can be requested by the members command.
-
- The event is one of:
- - connected (client connected to MsgQ)
- - disconected (client disconnected from MsgQ)
- - subscribed (client subscribed to a group)
- - unsubscribed (client unsubscribed from a group)
-
- The params is dict containing:
- - client: The lname of the client in question.
- - group (for 'subscribed' and 'unsubscribed' events):
- The group the client subscribed or unsubscribed from.
-
- The notification occurs after the event, so client a subscribing for
- notifications will get a notification about its own subscription, but
- will not get a notification when it unsubscribes.
- """
- # Due to the interaction between threads (and fear it might influence
- # sending stuff), we test this method in msgq_run_test, instead of
- # mocking the ccs.
- if self._session: # Don't send before we have started up
- self._session.notify('cc_members', event, params)
-
- def cfgmgr_ready(self, ready=True):
- """Notify that the config manager is either subscribed, or
- that the msgq is shutting down and it won't connect, but
- anybody waiting for it should stop anyway.
-
- The ready parameter signifies if the config manager is subscribed.
-
- This method can be called multiple times, but second and any
- following call is simply ignored. This means the "abort" version
- of the call can be used on any stop unconditionally, even when
- the config manager already connected.
- """
- with self.__cfgmgr_ready_cond:
- if self.__cfgmgr_ready is not None:
- # This is a second call to this method. In that case it does
- # nothing.
- return
- self.__cfgmgr_ready = ready
- self.__cfgmgr_ready_cond.notify_all()
-
- def wait_cfgmgr(self):
- """Wait for msgq to subscribe.
-
- When this returns, the config manager is either subscribed, or
- msgq gave up waiting for it. Success is signified by the return
- value.
- """
- with self.__cfgmgr_ready_cond:
- # Wait until it either aborts or subscribes
- while self.__cfgmgr_ready is None:
- self.__cfgmgr_ready_cond.wait()
- return self.__cfgmgr_ready
-
- def setup_listener(self):
- """Set up the listener socket. Internal function."""
- logger.debug(TRACE_BASIC, MSGQ_LISTENER_SETUP, self.socket_file)
-
- if os.path.exists(self.socket_file):
- # Rather than just blindly removing the socket file, attempt to
- # connect to the existing socket to see if there is an existing
- # msgq running. Only if that fails do we remove the file and
- # attempt to create a new socket.
- existing_msgq = None
- try:
- existing_msgq = isc.cc.Session(self.socket_file)
- except isc.cc.session.SessionError:
- existing_msgq = None
-
- if existing_msgq:
- existing_msgq.close()
- logger.fatal(MSGQ_ALREADY_RUNNING)
- raise MsgQRunningError("b10-msgq already running")
-
- os.remove(self.socket_file)
-
- try:
- self.listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- self.listen_socket.bind(self.socket_file)
- self.listen_socket.listen(1024)
- except Exception as e:
- # remove the file again if something goes wrong
- # (note this is a catch-all, but we reraise it)
- if os.path.exists(self.socket_file):
- os.remove(self.socket_file)
- self.listen_socket.close()
- logger.fatal(MSGQ_LISTENER_FAILED, self.socket_file, e)
- raise e
-
- def setup_signalsock(self):
- """Create a socket pair used to signal when we want to finish.
- Using a socket is easy and thread/signal safe way to signal
- the termination.
- """
- # The __poller_sock will be the end in the poller. When it is
- # closed, we should shut down.
- (self.__poller_sock, self.__control_sock) = socket.socketpair()
-
- def setup(self):
- """Configure listener socket, polling, etc.
- Raises a socket.error if the socket_file cannot be
- created.
- """
-
- self.setup_signalsock()
- self.setup_listener()
-
- logger.debug(TRACE_START, MSGQ_LISTENER_STARTED);
-
- self.runnable = True
-
- def process_accept(self):
- """Process an accept on the listening socket."""
- newsocket, ipaddr = self.listen_socket.accept()
- # TODO: When we have logging, we might want
- # to add a debug message here that a new connection
- # was made
- self.register_socket(newsocket)
-
- def register_socket(self, newsocket):
- """
- Internal function to insert a socket. Used by process_accept and some
- tests.
- """
- self.sockets[newsocket.fileno()] = newsocket
- lname = self.newlname()
- self.lnames[lname] = newsocket
- self.fd_to_lname[newsocket.fileno()] = lname
-
- logger.debug(TRACE_BASIC, MSGQ_SOCKET_REGISTERED, newsocket.fileno(),
- lname)
-
- self.members_notify('connected', {'client': lname})
-
- def kill_socket(self, fd, sock):
- """Fully close down the socket."""
-
- unsubscribed_from = self.subs.unsubscribe_all(sock)
- lname = self.fd_to_lname[fd]
- del self.fd_to_lname[fd]
- del self.lnames[lname]
- sock.close()
- del self.sockets[fd]
- if fd in self.sendbuffs:
- del self.sendbuffs[fd]
- logger.debug(TRACE_BASIC, MSGQ_SOCK_CLOSE, fd)
- # Filter out just the groups.
- unsubscribed_from_groups = set(map(lambda x: x[0], unsubscribed_from))
- for group in unsubscribed_from_groups:
- self.members_notify('unsubscribed', {
- 'client': lname,
- 'group': group
- })
- self.members_notify('disconnected', {'client': lname})
-
- def __getbytes(self, fd, sock, length, continued):
- """Get exactly the requested bytes, or raise an exception if
- EOF.
-
- continued is set to True if this method is called to complete
- already read data.
- """
- received = b''
- while len(received) < length:
- try:
- data = sock.recv(length - len(received))
-
- except socket.error as err:
- # This case includes ECONNRESET, which seems to happen when
- # the remote client has closed its socket at some subtle
- # timing (it should normally result in receiving empty data).
- # Since we didn't figure out how exactly that could happen,
- # we treat it just like other really-unexpected socket errors.
- raise MsgQReceiveError(str(err))
- if len(data) == 0:
- raise MsgQCloseOnReceive("EOF", continued)
- received += data
- continued = True
- return received
-
- def read_packet(self, fd, sock):
- """Read a correctly formatted packet. Will raise exceptions if
- something fails."""
- lengths = self.__getbytes(fd, sock, 6, False)
- overall_length, routing_length = struct.unpack(">IH", lengths)
- if overall_length < 2:
- raise MsgQReceiveError("overall_length < 2")
- overall_length -= 2
- if routing_length > overall_length:
- raise MsgQReceiveError("routing_length > overall_length")
- if routing_length == 0:
- raise MsgQReceiveError("routing_length == 0")
- data_length = overall_length - routing_length
- # probably need to sanity check lengths here...
- routing = self.__getbytes(fd, sock, routing_length, True)
- if data_length > 0:
- data = self.__getbytes(fd, sock, data_length, True)
- else:
- data = None
- return (routing, data)
-
- def process_packet(self, fd, sock):
- """Process one packet."""
- try:
- routing, data = self.read_packet(fd, sock)
- except (MsgQReceiveError, MsgQCloseOnReceive) as err:
- # If it's MsgQCloseOnReceive and that happens without reading
- # any data, it basically means the remote client has closed the
- # socket, so we log it as debug information. Otherwise, it's
- # a somewhat unexpected event, so we consider it an "error".
- if isinstance(err, MsgQCloseOnReceive) and not err.partial_read:
- logger.debug(TRACE_BASIC, MSGQ_CLOSE_ON_RECV, fd)
- else:
- logger.error(MSGQ_RECV_ERROR, fd, err)
- self.kill_socket(fd, sock)
- return
-
- try:
- routingmsg = isc.cc.message.from_wire(routing)
- except DecodeError as err:
- self.kill_socket(fd, sock)
- logger.error(MSGQ_HDR_DECODE_ERROR, fd, err)
- return
-
- self.process_command(fd, sock, routingmsg, data)
-
- def process_command(self, fd, sock, routing, data):
- """Process a single command. This will split out into one of the
- other functions."""
- logger.debug(TRACE_DETAIL, MSGQ_RECV_HDR, routing)
- cmd = routing[CC_HEADER_TYPE]
- if cmd == CC_COMMAND_SEND:
- self.process_command_send(sock, routing, data)
- elif cmd == CC_COMMAND_SUBSCRIBE:
- self.process_command_subscribe(sock, routing, data)
- elif cmd == CC_COMMAND_UNSUBSCRIBE:
- self.process_command_unsubscribe(sock, routing, data)
- elif cmd == CC_COMMAND_GET_LNAME:
- self.process_command_getlname(sock, routing, data)
- elif cmd == CC_COMMAND_PING:
- # Command for testing purposes
- self.process_command_ping(sock, routing, data)
- elif cmd == CC_COMMAND_STOP:
- self.stop()
- else:
- logger.error(MSGQ_INVALID_CMD, cmd)
-
- def preparemsg(self, env, msg = None):
- if type(env) == dict:
- env = isc.cc.message.to_wire(env)
- if type(msg) == dict:
- msg = isc.cc.message.to_wire(msg)
- length = 2 + len(env);
- if msg:
- length += len(msg)
- ret = struct.pack("!IH", length, len(env))
- ret += env
- if msg:
- ret += msg
- return ret
-
- def sendmsg(self, sock, env, msg = None):
- self.send_prepared_msg(sock, self.preparemsg(env, msg))
-
- def _send_data(self, sock, data):
- """
- Send a piece of data to the given socket. This method is
- essentially "private" to MsgQ, but defined as if it were "protected"
- for easier access from tests.
-
- Parameters:
- sock: The socket to send to
- data: The list of bytes to send
- Returns:
- An integer or None. If an integer (which can be 0), it signals
- the number of bytes sent. If None, the socket appears to have
- been closed on the other end, and it has been killed on this
- side too.
- """
- try:
- # We set the socket nonblocking, MSG_DONTWAIT doesn't exist
- # on some OSes
- sock.setblocking(0)
- return sock.send(data)
- except socket.error as e:
- if e.errno in [ errno.EAGAIN, errno.EWOULDBLOCK, errno.EINTR ]:
- return 0
- elif e.errno in [ errno.EPIPE, errno.ECONNRESET, errno.ENOBUFS ]:
- # EPIPE happens if the remote module has terminated by the time
- # of this send; its severity can vary, but in many cases it
- # shouldn't be critical, so we log it separately as a warning.
- if e.errno == errno.EPIPE:
- logger.warn(MSGQ_CLOSE_ON_SEND, sock.fileno())
- else:
- logger.error(MSGQ_SEND_ERROR, sock.fileno(),
- errno.errorcode[e.errno])
- self.kill_socket(sock.fileno(), sock)
- return None
- else:
- raise e
- finally:
- # And set it back again
- sock.setblocking(1)
-
- def send_prepared_msg(self, sock, msg):
- '''
- Add a message to the queue. If there's nothing waiting, try
- to send it right away.
-
- Return if the socket is still alive. It can return false if the
- socket dies (for example due to EPIPE in the attempt to send).
- Returning true does not guarantee the message will be delivered,
- but returning false means it won't.
- '''
- # Try to send the data, but only if there's nothing waiting
- fileno = sock.fileno()
- if fileno in self.sendbuffs:
- amount_sent = 0
- else:
- amount_sent = self._send_data(sock, msg)
- if amount_sent is None:
- # Socket has been killed, drop the send
- return False
-
- # Still something to send, add it to outgoing queue
- if amount_sent < len(msg):
- now = time.clock()
- # Append it to buffer (but check the data go away)
- if fileno in self.sendbuffs:
- (last_sent, buff) = self.sendbuffs[fileno]
- tdelta = now - last_sent
- if tdelta > 0.1:
- logger.error(MSGQ_SOCKET_TIMEOUT_ERROR, fileno, tdelta)
- self.kill_socket(fileno, sock)
- return False
- buff += msg
- else:
- buff = msg[amount_sent:]
- last_sent = now
- self.sendbuffs[fileno] = (last_sent, buff)
- return True
-
- def _process_write(self, fileno):
- # Try to send some data from the buffer
- (_, msg) = self.sendbuffs[fileno]
- sock = self.sockets[fileno]
- amount_sent = self._send_data(sock, msg)
- if amount_sent is not None:
- # Keep the rest
- msg = msg[amount_sent:]
- if len(msg) == 0:
- # If there's no more, stop requesting for write availability
- del self.sendbuffs[fileno]
- else:
- self.sendbuffs[fileno] = (time.clock(), msg)
-
- def newlname(self):
- """Generate a unique connection identifier for this socket.
- This is done by using an increasing counter and the current
- time."""
- self.connection_counter += 1
- return "%x_%x@%s" % (time.time(), self.connection_counter,
- self.hostname)
-
- def process_command_ping(self, sock, routing, data):
- self.sendmsg(sock, { CC_HEADER_TYPE : CC_COMMAND_PONG }, data)
-
- def process_command_getlname(self, sock, routing, data):
- lname = [ k for k, v in self.lnames.items() if v == sock ][0]
- self.sendmsg(sock, { CC_HEADER_TYPE : CC_COMMAND_GET_LNAME },
- { CC_PAYLOAD_LNAME : lname })
-
- def process_command_send(self, sock, routing, data):
- group = routing[CC_HEADER_GROUP]
- instance = routing[CC_HEADER_INSTANCE]
- to = routing[CC_HEADER_TO]
- if group == None or instance == None:
- # FIXME: Should we log them instead?
- return # ignore invalid packets entirely
-
- if to == CC_TO_WILDCARD:
- sockets = self.subs.find(group, instance)
- else:
- if to in self.lnames:
- sockets = [ self.lnames[to] ]
- else:
- sockets = []
-
- msg = self.preparemsg(routing, data)
-
- if sock in sockets:
- # Don't bounce to self
- sockets.remove(sock)
-
- has_recipient = False
- for socket in sockets:
- if self.send_prepared_msg(socket, msg):
- has_recipient = True
- if not has_recipient and routing.get(CC_HEADER_WANT_ANSWER) and \
- CC_HEADER_REPLY not in routing:
- # We have no recipients. But the sender insists on a reply
- # (and the message isn't a reply itself). We need to send
- # an error to satisfy the request, since there's nobody
- # else who can.
- #
- # We omit the replies on purpose. The recipient might generate
- # the response by copying and mangling the header of incoming
- # message (just like we do below) and would include the want_answer
- # by accident. And we want to avoid loops of errors. Also, it
- # is unclear if the knowledge of undeliverable reply would be
- # of any use to the sender, and it should be much rarer situation.
-
- # The real errors would be positive, 1 most probably. We use
- # negative errors for delivery errors to distinguish them a
- # little. We probably should have a way to provide more data
- # in the error message.
- payload = isc.config.ccsession.create_answer(CC_REPLY_NO_RECPT,
- "No such recipient")
- # We create the header based on the current one. But we don't
- # want to mangle it for the caller, so we get a copy. A shallow
- # one should be enough, we modify the dict only.
- header = routing.copy()
- header[CC_HEADER_REPLY] = routing[CC_HEADER_SEQ]
- # Dummy lname not assigned to clients
- header[CC_HEADER_FROM] = "msgq"
- header[CC_HEADER_TO] = routing[CC_HEADER_FROM]
- # We keep the seq as it is. We don't need to track the message
- # and we will not confuse the sender. The sender would use an
- # unique id for each message, so we won't return one twice to it.
- errmsg = self.preparemsg(header, payload)
- # Send it back.
- self.send_prepared_msg(sock, errmsg)
-
- def process_command_subscribe(self, sock, routing, data):
- group = routing[CC_HEADER_GROUP]
- instance = routing[CC_HEADER_INSTANCE]
- if group == None or instance == None:
- return # ignore invalid packets entirely
- self.subs.subscribe(group, instance, sock)
- lname = self.fd_to_lname[sock.fileno()]
- self.members_notify('subscribed',
- {
- 'client': lname,
- 'group': group
- })
-
- def process_command_unsubscribe(self, sock, routing, data):
- group = routing[CC_HEADER_GROUP]
- instance = routing[CC_HEADER_INSTANCE]
- if group == None or instance == None:
- return # ignore invalid packets entirely
- if self.subs.unsubscribe(group, instance, sock):
- lname = self.fd_to_lname[sock.fileno()]
- self.members_notify('unsubscribed',
- {
- 'client': lname,
- 'group': group
- })
-
- def run(self):
- """Process messages. Forever. Mostly."""
- self.running = True
-
- self.run_select()
-
- def run_select(self):
- while self.running:
- reads = list(self.fd_to_lname.keys())
- if self.listen_socket.fileno() != -1: # Skip in tests
- reads.append(self.listen_socket.fileno())
- if self.__poller_sock and self.__poller_sock.fileno() != -1:
- reads.append(self.__poller_sock.fileno())
- writes = list(self.sendbuffs.keys())
- (read_ready, write_ready) = ([], [])
- try:
- (read_ready, write_ready, _) = select.select(reads, writes,
- []);
- except select.error as err:
- if err.args[0] == errno.EINTR:
- continue # Just try it again if interrupted.
- else:
- logger.fatal(MSGQ_SELECT_ERROR, err)
- break
- with self.__lock:
- write_ready = set(write_ready)
- for fd in read_ready:
- # Do only one operation per loop iteration on the given fd.
- # It could be possible to perform both, but it may have
- # undesired side effects in special situations (like, if the
- # read closes the socket).
- if fd in write_ready:
- write_ready.remove(fd)
- if fd == self.listen_socket.fileno():
- self.process_accept()
- elif self.__poller_sock and fd == \
- self.__poller_sock.fileno():
- # The signal socket. We should terminate now.
- self.running = False
- break
- else:
- self.process_packet(fd, self.sockets[fd])
- for fd in write_ready:
- self._process_write(fd)
-
- def stop(self):
- # Signal it should terminate.
- self.__control_sock.close()
- self.__control_sock = None
- # Abort anything waiting on the condition, just to make sure it's not
- # blocked forever
- self.cfgmgr_ready(False)
-
- def cleanup_signalsock(self):
- """Close the signal sockets. We could do it directly in shutdown,
- but this part is reused in tests.
- """
- if self.__poller_sock:
- self.__poller_sock.close()
- self.__poller_sock = None
- if self.__control_sock:
- self.__control_sock.close()
- self.__control_sock = None
-
- def shutdown(self):
- """Stop the MsgQ master."""
- logger.debug(TRACE_START, MSGQ_SHUTDOWN)
- self.listen_socket.close()
- self.cleanup_signalsock()
- # Close all the sockets too. In real life, there should be none now,
- # as Msgq should be the last one. But some tests don't adhere to this
- # and create a new Msgq for each test, which led to huge socket leaks.
- # Some other threads put some other things in instead of sockets, so
- # we catch whatever exceptions there we can. This should be safe,
- # because in real operation, we will terminate now anyway, implicitly
- # closing anything anyway.
- for sock in self.sockets.values():
- try:
- sock.close()
- except Exception:
- pass
- if os.path.exists(self.socket_file):
- os.remove(self.socket_file)
-
- def config_handler(self, new_config):
- """The configuration handler (run in a separate thread).
- Not tested, currently effectively empty.
- """
- config_logger.debug(TRACE_DETAIL, MSGQ_CONFIG_DATA, new_config)
-
- with self.__lock:
- if not self.running:
- return
-
- # TODO: Any config handling goes here.
-
- return isc.config.create_answer(0)
-
- def command_handler(self, command, args):
- """The command handler (run in a separate thread)."""
- config_logger.debug(TRACE_DETAIL, MSGQ_COMMAND, command, args)
-
- with self.__lock:
- if not self.running:
- return
-
- # TODO: Who does validation? The ModuleCCSession or must we?
-
- if command == 'members':
- # List all members of MsgQ or of a group.
- if args is None:
- args = {}
- group = args.get('group')
- if group:
- return isc.config.create_answer(0,
- list(map(lambda sock: self.fd_to_lname[sock.fileno()],
- self.subs.find(group, ''))))
- else:
- return isc.config.create_answer(0,
- list(self.lnames.keys()))
-
- config_logger.error(MSGQ_COMMAND_UNKNOWN, command)
- return isc.config.create_answer(1, 'unknown command: ' + command)
-
-def signal_handler(msgq, signal, frame):
- if msgq:
- msgq.stop()
-
-def main():
- def check_port(option, opt_str, value, parser):
- """Function to insure that the port we are passed is actually
- a valid port number. Used by OptionParser() on startup."""
- intval = int(value)
- if (intval < 0) or (intval > 65535):
- raise OptionValueError("%s requires a port number (0-65535)" %
- opt_str)
- parser.values.msgq_port = intval
-
- # Parse any command-line options.
- parser = OptionParser(version=VERSION)
- # TODO: Should we remove the option?
- parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
- help="display more about what is going on")
- parser.add_option("-s", "--socket-file", dest="msgq_socket_file",
- type="string", default=None,
- help="UNIX domain socket file the msgq daemon will use")
- (options, args) = parser.parse_args()
-
- # Announce startup.
- logger.debug(TRACE_START, MSGQ_START, VERSION)
-
- msgq = MsgQ(options.msgq_socket_file, options.verbose)
-
- signal.signal(signal.SIGTERM,
- lambda signal, frame: signal_handler(msgq, signal, frame))
-
- # Ignore SIGPIPE. We handle errors writing to children using try in the
- # appropriate places and the delivery of a signal is very heavy handed.
- # Windows doesn't support SIGPIPE so don't try it there.
- if not sys.platform.startswith('win'):
- signal.signal(signal.SIGPIPE, signal.SIG_IGN)
-
- try:
- msgq.setup()
- except Exception as e:
- logger.fatal(MSGQ_START_FAIL, e)
- sys.exit(1)
-
- # We run the processing in a separate thread. This is because we want to
- # connect to the msgq ourself. But the cc library is unfortunately blocking
- # in many places and waiting for the processing part to answer, it would
- # deadlock.
- poller_thread = threading.Thread(target=msgq.run)
- poller_thread.daemon = True
- try:
- poller_thread.start()
- if msgq.wait_cfgmgr():
- # Once we get the config manager, we can read our own config.
- session = isc.config.ModuleCCSession(SPECFILE_LOCATION,
- msgq.config_handler,
- msgq.command_handler,
- None, True,
- msgq.socket_file)
- msgq._session = session
- session.start()
- # And we create a thread that'll just wait for commands and
- # handle them. We don't terminate the thread, we set it to
- # daemon. Once the main thread terminates, it'll just die.
- def run_session():
- while True:
- # As the check_command has internal mutex that is shared
- # with sending part (which includes notify). So we don't
- # want to hold it long-term and block using select.
- fileno = session.get_socket().fileno()
- try:
- (reads, _, _) = select.select([fileno], [], [])
- except select.error as se:
- if se.args[0] != errno.EINTR:
- raise
- session.check_command(True)
- background_thread = threading.Thread(target=run_session)
- background_thread.daemon = True
- background_thread.start()
- poller_thread.join()
- except KeyboardInterrupt:
- pass
-
- msgq.shutdown()
-
- logger.info(MSGQ_EXITING)
-
-if __name__ == "__main__":
- isc.util.traceback_handler.traceback_handler(main)
+++ /dev/null
-{
- "module_spec": {
- "module_name": "Msgq",
- "module_description": "The message queue",
- "config_data": [],
- "commands": [
- {
- "command_name": "members",
- "command_description": "Provide the list of members of a group or of the whole MsgQ if no group is given.",
- "command_args": [
- {
- "item_name": "group",
- "item_optional": true,
- "item_type": "string"
- }
- ]
- }
- ]
- }
-}
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>June 25, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-msgq</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-msgq</refname>
- <refpurpose>message routing daemon for the Command Channel</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2010</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>b10-msgq</command>
- <arg><option>-s <replaceable>file</replaceable></option></arg>
- <arg><option>-v</option></arg>
- <arg><option>--socket-file <replaceable>file</replaceable></option></arg>
- <arg><option>--verbose</option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>
- The <command>b10-msgq</command>
- daemon provides message routing for the Command Channel.
- </para>
-
- <para>
- The Command Channel is a message bus and subscription manager.
- Programs may subscribe to certain groups to receive messages
- for that group.
- Every new connection to <command>b10-msgq</command> is
- assigned a unique identifier -- this is the local name.
- The commands it handles are:
- <itemizedlist>
-
- <listitem><para>
- <command>getlname</command>
- — receive local name.
- </para></listitem>
-
- <listitem><para>
- <command>send</command>
- — send a message to defined subscribers.
- </para></listitem>
-
- <listitem><para>
- <command>subscribe</command>
- — add a subscription. This means it is a listener
- for messages for a specific group.
- </para></listitem>
-
- <listitem><para>
- <command>unsubscribe</command>
- — remove a subscription.
- </para></listitem>
-
- </itemizedlist>
- </para>
-
- <para>
- The <command>b10-msgq</command> daemon may be cleanly stopped by
- sending the SIGTERM signal to the process.
- This shutdown does not notify the subscribers.
- </para>
-
- </refsect1>
-
- <refsect1>
- <title>OPTIONS</title>
-
- <para>The arguments are as follows:</para>
-
- <variablelist>
- <varlistentry>
- <term><option>-s <replaceable>file</replaceable></option>,
- <option>--socket-file <replaceable>file</replaceable></option></term>
- <listitem><para>
- The UNIX domain socket file this daemon will use.
- The default is
- <filename>/usr/local/var/bind10/msg_socket</filename>.
-<!-- @localstatedir@/@PACKAGE_NAME@/msg_socket -->
- </para></listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-v</option>, <option>--verbose</option></term>
- <listitem><para>
- Enabled verbose mode. This enables diagnostic messages to
- STDERR. Displays more about what <command>b10-msgq</command>
- is doing.
-<!-- TODO: is STDERR correct? -->
- </para></listitem>
- </varlistentry>
-
- </variablelist>
-
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-init</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
-<!-- TODO: point to developer docs -->
- </refsect1>
-
- <refsect1>
- <title>AUTHORS</title>
- <para>
- The <command>b10-msgq</command> daemon and Control Channel specification
- were initially designed by Michael Graff of ISC.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- The python version was first coded in December 2009.
- The C version with now deprecated wire format was coded in September
- 2009.
- </para>
- </refsect1>
-
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
-
+++ /dev/null
-# Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-# PERFORMANCE OF THIS SOFTWARE.
-
-# No namespace declaration - these constants go in the global namespace
-# of the ddns messages python module.
-
-# When you add a message to this file, it is a good idea to run
-# <topsrcdir>/tools/reorder_message_file.py to make sure the
-# messages are in the correct order.
-
-% MSGQ_ALREADY_RUNNING Another copy of b10-msgq is already running.
-Only a single instance of b10-msgq should ever be run at one time.
-This instance will now terminate.
-
-% MSGQ_CFGMGR_SUBSCRIBED The config manager subscribed to message queue
-This is a debug message. The message queue has little bit of special handling
-for the configuration manager. This special handling is happening now.
-
-% MSGQ_CLOSE_ON_RECV Reading from socket canceled as it's closed: FD=%1
-A debug message. The msgq daemon was notified of a read event on a
-socket, but its initial read operation failed because the remote
-client has closed its socket. This is possible in a normal operation
-when a module shuts down.
-
-% MSGQ_CLOSE_ON_SEND Sending to socket failed as already closed (okay to ignore on shutdown): FD=%1
-The msgq daemon tries to send some data to a client module, but it
-failed because the socket has been closed. This normally means the
-client terminates (for some reason - either gracefully or as a crash)
-while other modules try to send a message to the terminated module.
-Since msgq doesn't keep track of the status of client modules, this
-can happen and is not really an error for msgq; however, it can still
-be an unexpected event for the BIND 10 system as a whole in that this
-particular message is lost, so it's logged as a warning. If this
-message is logged for a running BIND 10 system, it's suggested to
-check other log messages; there may be an error from other modules
-reporting a missing response message. One common, less critical case
-where this message is logged is during shutdown. The ordering of
-process shutdown is basically arbitrary at this moment, so it's
-possible that some module tries to send a "quitting" message to some
-other module but the latter has already shut down. Such cases are
-generally non critical, but you may want to check other possible error
-messages.
-
-% MSGQ_COMMAND Running command %1 with arguments %2
-Debug message. The message queue received a command and it is running it.
-
-% MSGQ_COMMAND_UNKNOWN Unknown command '%1'
-The message queue received a command from other module, but it doesn't
-recognize it. This is probably either a coding error or inconsistency between
-the message queue version and version of the module.
-
-% MSGQ_CONFIG_DATA Received configuration update for the msgq: %1
-Debug message. The message queue received a configuration update, handling it.
-
-% MSGQ_EXITING exiting
-The msgq daemon is exiting.
-
-% MSGQ_HDR_DECODE_ERROR Error decoding header received from socket %1: %2
-The socket with mentioned file descriptor sent a packet. However, it was not
-possible to decode the routing header of the packet. The packet is ignored.
-This may be caused by a programmer error (one of the components sending invalid
-data) or possibly by incompatible version of msgq and the component (but that's
-unlikely, as the protocol is not changed often).
-
-% MSGQ_INVALID_CMD Received invalid command: %1
-An unknown command listed in the log has been received. It is ignored. This
-indicates either a programmer error (eg. a typo in the command name) or
-incompatible version of a module and message queue daemon.
-
-% MSGQ_LISTENER_FAILED Failed to initialize listener on socket file '%1': %2
-The message queue daemon tried to listen on a file socket (the path is in the
-message), but it failed. The error from the operating system is logged.
-
-% MSGQ_LISTENER_SETUP Starting to listen on socket file '%1'
-Debug message. The listener is trying to open a listening socket.
-
-% MSGQ_LISTENER_STARTED Successfully started to listen
-Debug message. The message queue successfully opened a listening socket and
-waits for incoming connections.
-
-% MSGQ_RECV_ERROR Error reading from socket %1: %2
-There was a low-level error when reading from a socket. The error is logged and
-the corresponding socket is dropped. The errors include receiving
-broken or (non empty but) incomplete data. In either case it usually suggests
-something unexpected happens within the BIND 10 system; it's probably
-better to restart the system, and if it continues it should be
-reported as a bug. One known, probably non critical case is
-the "connection reset by peer" (or its variants) socket error appearing
-on shutdown. It's known this happens when the remote client closes the
-connection as part of shutdown process. Such cases are normally expected
-to be reported as receiving empty data (which we log it at the debug level
-as the MSGQ_CLOSE_ON_RECV message), but for some (yet) unknown reason
-it can also be reported as the system error. At shutdown time it's expected
-that connections are closed, so it's probably safe to ignore these messages
-in such a case. We still log them as an error as we've not figured out
-how exactly that can happen. In future, we may make the shutdown process
-more robust so the msgq daemon can explicitly know when a client shuts down
-more reliably. If and when it's implemented this error message won't appear
-on shutdown unless there's really something unexpected.
-
-% MSGQ_RECV_HDR Received header: %1
-Debug message. This message includes the whole routing header of a packet.
-
-% MSGQ_SELECT_ERROR Error while waiting for events: %1
-A low-level error happened when waiting for events, the error is logged. The
-reason for this varies, but it usually means the system is short on some
-resources.
-
-% MSGQ_SEND_ERROR Error while sending to socket %1: %2
-There was a low-level error when sending data to a socket. The error is logged
-and the corresponding socket is dropped.
-
-% MSGQ_SHUTDOWN Stopping Msgq
-Debug message. The message queue is shutting down.
-
-% MSGQ_SOCKET_REGISTERED Registered a socket descriptor %1 with lname %2
-Debug message. The msgq daemon accepted a session request on the
-shown descriptor of socket and assigned a unique identifier (lname)
-for the client on that socket.
-
-% MSGQ_SOCKET_TIMEOUT_ERROR Killing socket %1 because timeout exceeded (%2)
-Outgoing data was queued up on a socket connected to msgq, but the other
-side is not reading it. It could be deadlocked, or may not be monitoring
-it. Both cases are programming errors and should be corrected. The socket
-is closed on the msgq side.
-
-% MSGQ_SOCK_CLOSE Closing socket fd %1
-Debug message. Closing the mentioned socket.
-
-% MSGQ_START Msgq version %1 starting
-Debug message. The message queue is starting up.
-
-% MSGQ_START_FAIL Error during startup: %1
-There was an error during early startup of the daemon. More concrete error is
-in the log. The daemon terminates as a result.
-
-% MSGQ_SUBS_APPEND_TARGET Appending to existing target for subscription to group '%1' for instance '%2'
-Debug message. Creating a new subscription by appending it to already existing
-data structure.
-
-% MSGQ_SUBS_NEW_TARGET Creating new target for subscription to group '%1' for instance '%2'
-Debug message. Creating a new subscription. Also creating a new data structure
-to hold it.
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-MYPATH_PATH=@abs_top_builddir@/src/bin/msgq
-
-PYTHONPATH=@abs_top_builddir@/src/lib/python/isc/log_messages:@abs_top_builddir@/src/lib/python/isc/cc:@abs_top_srcdir@/src/lib/python:@abs_top_builddir@/src/lib/python:@abs_top_builddir@/src/lib/dns/python/.libs
-export PYTHONPATH
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-SET_ENV_LIBRARY_PATH=@SET_ENV_LIBRARY_PATH@
-if test $SET_ENV_LIBRARY_PATH = yes; then
- @ENV_LIBRARY_PATH@=@abs_top_builddir@/src/lib/dns/.libs:@abs_top_builddir@/src/lib/dns/python/.libs:@abs_top_builddir@/src/lib/cryptolink/.libs:@abs_top_builddir@/src/lib/cc/.libs:@abs_top_builddir@/src/lib/config/.libs:@abs_top_builddir@/src/lib/log/.libs:@abs_top_builddir@/src/lib/acl/.libs:@abs_top_builddir@/src/lib/util/.libs:@abs_top_builddir@/src/lib/util/io/.libs:@abs_top_builddir@/src/lib/exceptions/.libs:@abs_top_builddir@/src/lib/datasrc/.libs:$@ENV_LIBRARY_PATH@
- export @ENV_LIBRARY_PATH@
-fi
-
-B10_FROM_SOURCE=@abs_top_srcdir@
-export B10_FROM_SOURCE
-# TODO: We need to do this feature based (ie. no general from_source)
-# But right now we need a second one because some spec files are
-# generated and hence end up under builddir
-B10_FROM_BUILD=@abs_top_builddir@
-export B10_FROM_BUILD
-
-BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
-export BIND10_MSGQ_SOCKET_FILE
-
-cd ${MYPATH_PATH}
-exec ${PYTHON_EXEC} -O b10-msgq "$@"
+++ /dev/null
-/msgq_test
+++ /dev/null
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = msgq_test.py msgq_run_test.py
-EXTRA_DIST = $(PYTESTS)
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/msgq \
- BIND10_TEST_SOCKET_FILE=$(builddir)/test_msgq_socket.sock \
- B10_FROM_SOURCE=$(abs_top_srcdir) \
- B10_FROM_BUILD=$(abs_top_builddir) \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
-
+++ /dev/null
-# Copyright (C) 2013 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-In this test file, we actually start msgq as a process and test it
-as a whole. It may be considered a system test instead of unit test,
-but apart from the terminology, we don't care much. We need to test
-the message queue works as expected, together with the libraries.
-
-In each test, we first start a timeout (because we do some waits
-for messages and if they wouldn't come, the test could block indefinitely).
-The timeout is long, because it is for the case the test fails.
-
-We then start the msgq and wait for the socket file to appear
-(that should indicate it is ready to receive connections). Then the
-actual test starts. After the test, we kill it and remove the test file.
-
-We also register signal handlers for many signals. Even in the case
-the test is interrupted or crashes, we should ensure the message queue
-itself is terminated.
-"""
-
-import unittest
-import os
-import signal
-import sys
-import subprocess
-import time
-
-import isc.log
-import isc.cc.session
-from isc.cc.proto_defs import *
-
-# Due to problems with too long path on build bots, we place the socket
-# into the top-level build directory. That is ugly, but works.
-SOCKET_PATH = os.path.abspath(os.environ['B10_FROM_BUILD'] + '/msgq.sock')
-MSGQ_PATH = os.environ['B10_FROM_BUILD'] + '/src/bin/msgq/run_msgq.sh'
-TIMEOUT = 15 # Some long time (seconds), for single test.
-
-class MsgqRunTest(unittest.TestCase):
- def setUp(self):
- """
- As described above - check the socket file does not exist.
- Then register signals and timeouts. Finally, launch msgq
- and wait for it to start.
- """
- self.__msgq = None
- self.__opened_connections = []
- # A precondition check
- self.assertFalse(os.path.exists(SOCKET_PATH))
- signal.alarm(TIMEOUT)
- self.__orig_signals = {}
- # Register handlers for many signals. Most of them probably
- # can't happen in python, but we register them anyway just to be
- # safe.
- for sig in [signal.SIGHUP, signal.SIGINT, signal.SIGQUIT,
- signal.SIGILL, signal.SIGTRAP, signal.SIGABRT, signal.SIGBUS,
- signal.SIGFPE, signal.SIGALRM, signal.SIGTERM]:
- self.__orig_signals[sig] = signal.signal(sig, self.__signal)
- # Start msgq
- self.__msgq = subprocess.Popen([MSGQ_PATH, '-s', SOCKET_PATH],
- close_fds=True)
- # Some testing data
- self.__no_recpt = {"result": [-1, "No such recipient"]}
- # Wait for it to become ready (up to the alarm-set timeout)
- connection = None
- while not connection:
- try:
- # If the msgq is ready, this'll succeed. If not, it'll throw
- # session error.
- connection = isc.cc.session.Session(SOCKET_PATH)
- except isc.cc.session.SessionError:
- time.sleep(0.1) # Retry after a short time
- # We have the connection now, that means it works. Close this
- # connection, we won't use it. Each test gets enough new connections
- # of its own.
- connection.close()
-
- def __message(self, data):
- """
- Provide some testing message. The data will be included in it, so
- several different messages can be created.
- """
- return {"Message": "Text", "Data": data}
-
- def tearDown(self):
- """
- Perform cleanup after the test.
- """
- self.__cleanup()
-
- def __signal(self, signal, frame):
- """
- Called from a signal handler. We perform some cleanup, output
- a complain and terminate with error.
- """
- self.__cleanup()
- sys.stderr.write("Test terminating from signal " + str(signal) +
- " in " + str(frame) + "\n")
- sys.exit(1)
-
- def __cleanup(self):
- """
- Kill msgq (if running) and restore original signal handlers.
- """
- # Remove the socket (as we kill, msgq might not clean up)
- for conn in self.__opened_connections:
- conn.close()
- self.__opened_connections = []
- if self.__msgq:
- self.__msgq.kill()
- self.__msgq = None
- if os.path.exists(SOCKET_PATH):
- os.unlink(SOCKET_PATH)
- for sig in self.__orig_signals:
- signal.signal(sig, self.__orig_signals[sig])
- # Cancel timeout (so someone else is not hit by it)
- signal.alarm(0)
-
- def __get_connection(self):
- """
- Create a connection to the daemon and make sure it is properly closed
- at the end of the test.
- """
- connection = isc.cc.session.Session(SOCKET_PATH)
- self.__opened_connections.append(connection)
- return connection
-
- def test_send_direct(self):
- """
- Connect twice to msgq, send a message from one to another using direct
- l-name and see it comes.
- """
- # Create the connections
- conn1 = self.__get_connection()
- conn2 = self.__get_connection()
- # Send the message
- lname1 = conn1.lname
- conn2.group_sendmsg(self.__message(1), "*", to=lname1)
- # Receive the message and see it contains correct data
- (msg, env) = conn1.group_recvmsg(nonblock=False)
- self.assertEqual(self.__message(1), msg)
- # We don't check there are no extra headers, just that none are missing
- # or wrong.
- self.assertEqual(lname1, env[CC_HEADER_TO])
- self.assertEqual(conn2.lname, env[CC_HEADER_FROM])
- self.assertEqual("*", env[CC_HEADER_GROUP])
- self.assertEqual(CC_INSTANCE_WILDCARD, env[CC_HEADER_INSTANCE])
- self.assertEqual(CC_COMMAND_SEND, env[CC_HEADER_TYPE])
- self.assertFalse(env[CC_HEADER_WANT_ANSWER])
-
- def __barrier(self, connections):
- """
- Make sure all previous commands on all supplied connections are
- processed, by sending a ping and waiting for an answer.
- """
- for c in connections:
- c.sendmsg({"type": "ping"})
- for c in connections:
- pong = c.recvmsg(nonblock=False)
- self.assertEqual(({"type": "pong"}, None), pong)
-
- def test_send_group(self):
- """
- Create several connections. First, try to send a message to a (empty)
- group and see an error is bounced back. Then subscribe the others
- to the group and send it again. Send to a different group and see it
- bounced back. Unsubscribe and see it is bounced again.
-
- Then the other connections answer (after unsubscribing, strange, but
- legal). See both answers come.
-
- Then, look there are no more waiting messages.
- """
- conn_a = self.__get_connection()
- conn_b = []
- for i in range(0, 10):
- conn_b.append(self.__get_connection())
- # Send a message to empty group and get an error answer
- seq = conn_a.group_sendmsg(self.__message(1), "group",
- want_answer=True)
- (msg, env) = conn_a.group_recvmsg(nonblock=False, seq=seq)
- self.assertEqual(self.__no_recpt, msg)
- self.assertEqual(conn_a.lname, env[CC_HEADER_TO])
- # Subscribe the two connections
- for c in conn_b:
- c.group_subscribe("group")
- # The subscribe doesn't wait for answer, so make sure it is
- # all processed before continuing.
- self.__barrier(conn_b)
- # Send a message to the group (this time not empty)
- seq = conn_a.group_sendmsg(self.__message(2), "group",
- want_answer=True)
- envs = []
- for c in conn_b:
- (msg, env) = c.group_recvmsg(nonblock=False)
- self.assertEqual(self.__message(2), msg)
- self.assertEqual(conn_a.lname, env[CC_HEADER_FROM])
- # The daemon does not mangle the headers. Is it OK?
- self.assertEqual(CC_TO_WILDCARD, env[CC_HEADER_TO])
- self.assertEqual("group", env[CC_HEADER_GROUP])
- self.assertEqual(CC_INSTANCE_WILDCARD, env[CC_HEADER_INSTANCE])
- self.assertEqual(CC_COMMAND_SEND, env[CC_HEADER_TYPE])
- self.assertTrue(env[CC_HEADER_WANT_ANSWER])
- envs.append(env)
- # Send to non-existing group
- seq_ne = conn_a.group_sendmsg(self.__message(3), "no-group",
- want_answer=True)
- (msg, env) = conn_a.group_recvmsg(nonblock=False, seq=seq_ne)
- self.assertEqual(self.__no_recpt, msg)
- self.assertEqual(conn_a.lname, env[CC_HEADER_TO])
- # Unsubscribe the connections
- for c in conn_b:
- c.group_unsubscribe("group")
- # Synchronize the unsubscriptions
- self.__barrier(conn_b)
- seq_ne = conn_a.group_sendmsg(self.__message(4), "group",
- want_answer=True)
- (msg, env) = conn_a.group_recvmsg(nonblock=False, seq=seq_ne)
- self.assertEqual(self.__no_recpt, msg)
- self.assertEqual(conn_a.lname, env[CC_HEADER_TO])
- # Send answers for the original message that was delivered
- lnames = set()
- for (c, env) in zip(conn_b, envs):
- c.group_reply(env, self.__message("Reply"))
- lnames.add(c.lname)
- # Check the both answers come
- while lnames:
- # While there are still connections we didn't get the answer from
- # (the order is not guaranteed, therefore the juggling with set)
- (msg, env) = conn_a.group_recvmsg(nonblock=False, seq=seq)
- self.assertEqual(self.__message("Reply"), msg)
- lname = env[CC_HEADER_FROM]
- self.assertTrue(lname in lnames)
- lnames.remove(lname)
-
- # The barrier makes the msgq process everything we sent. As the
- # processing is single-threaded in it, any stray message would have
- # arrived before the barrier ends.
- self.__barrier(conn_b)
- self.__barrier([conn_a])
- for c in conn_b:
- self.assertEqual((None, None), c.group_recvmsg())
- self.assertEqual((None, None), conn_a.group_recvmsg())
-
- def test_conn_disconn(self):
- """
- Keep connecting and disconnecting, checking we can still send
- and receive messages.
- """
- conn = self.__get_connection()
- conn.group_subscribe("group")
- for i in range(0, 50):
- new = self.__get_connection()
- new.group_subscribe("group")
- self.__barrier([conn, new])
- new.group_sendmsg(self.__message(i), "group")
- (msg, env) = conn.group_recvmsg(nonblock=False)
- self.assertEqual(self.__message(i), msg)
- conn.close()
- conn = new
-
- def test_notifications(self):
- """
- Check that the MsgQ is actually sending notifications about events.
- We create a socket, subscribe the socket itself and see it receives
- it's own notification.
-
- Testing all the places where notifications happen is task for the
- common unit tests in msgq_test.py.
-
- The test is here, because there might be some trouble with multiple
- threads in msgq (see the note about locking on the module CC session
- when sending message from one thread and listening for commands in the
- other) which would be hard to test using pure unit tests. Testing
- runnig whole msgq tests that implicitly.
- """
- conn = self.__get_connection()
- # Activate the session, pretend to be the config manager.
- conn.group_subscribe('ConfigManager')
- # Answer request for logging config
- (msg, env) = conn.group_recvmsg(nonblock=False)
- self.assertEqual({'command': ['get_config',
- {'module_name': 'Logging'}]},
- msg)
- conn.group_reply(env, {'result': [0, {}]})
- # It sends its spec.
- (msg, env) = conn.group_recvmsg(nonblock=False)
- self.assertEqual('module_spec', msg['command'][0])
- conn.group_reply(env, {'result': [0]})
- # It asks for its own config
- (msg, env) = conn.group_recvmsg(nonblock=False)
- self.assertEqual({'command': ['get_config',
- {'module_name': 'Msgq'}]},
- msg)
- conn.group_reply(env, {'result': [0, {}]})
- # Synchronization - make sure the session is running before
- # we continue, so we get the notification. Similar synchronisation
- # as in b10-init, but we don't have full ccsession here, so we
- # do so manually.
- synchronised = False
- attempts = 100
- while not synchronised and attempts > 0:
- time.sleep(0.1)
- seq = conn.group_sendmsg({'command': ['Are you running?']},
- 'Msgq', want_answer=True)
- msg = conn.group_recvmsg(nonblock=False, seq=seq)
- synchronised = msg[0] != -1
- attempts -= 1
- self.assertTrue(synchronised)
- # The actual test
- conn.group_subscribe('notifications/cc_members')
- (msg, env) = conn.group_recvmsg(nonblock=False)
- self.assertEqual({'notification': ['subscribed', {
- 'client': conn.lname,
- 'group': 'notifications/cc_members'
- }]}, msg)
-
- def test_multiple_invocations(self):
- """
- Check to make sure that an attempt to start a second copy of the MsgQ
- daemon fails.
- """
-
- self.assertTrue (os.path.exists(SOCKET_PATH))
- self.__retcode = subprocess.call([MSGQ_PATH, '-s', SOCKET_PATH])
- self.assertNotEqual(self.__retcode, 0)
-
- # Verify that the socket still exists and works. We re-call
- # test_send_direct as a means of testing that the existing
- # daemon is still behaving correctly.
- self.assertTrue (os.path.exists(SOCKET_PATH))
- self.test_send_direct()
-
-if __name__ == '__main__':
- isc.log.init("msgq-tests")
- isc.log.resetUnitTestRootLogger()
- unittest.main()
+++ /dev/null
-# Copyright (C) 2010-2013 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-import msgq
-from msgq import SubscriptionManager, MsgQ
-
-import unittest
-import os
-import socket
-import select # needed only for #3014. can be removed once it's solved
-import signal
-import sys
-import time
-import errno
-import threading
-import isc.cc
-import collections
-import isc.log
-import struct
-import json
-
-#
-# Currently only the subscription part and some sending is implemented...
-# I'd have to mock out a socket, which, while not impossible, is not trivial.
-#
-
-class TestSubscriptionManager(unittest.TestCase):
- def setUp(self):
- self.__cfgmgr_ready_called = 0
- self.sm = SubscriptionManager(self.cfgmgr_ready)
-
- def cfgmgr_ready(self):
- # Called one more time
- self.__cfgmgr_ready_called += 1
-
- def test_subscription_add_delete_manager(self):
- self.sm.subscribe("a", "*", 'sock1')
- self.assertEqual(self.sm.find_sub("a", "*"), [ 'sock1' ])
-
- def test_subscription_add_delete_other(self):
- self.sm.subscribe("a", "*", 'sock1')
- self.sm.unsubscribe("a", "*", 'sock2')
- self.assertEqual(self.sm.find_sub("a", "*"), [ 'sock1' ])
-
- def test_subscription_add_several_sockets(self):
- socks = [ 's1', 's2', 's3', 's4', 's5' ]
- for s in socks:
- self.sm.subscribe("a", "*", s)
- self.assertEqual(self.sm.find_sub("a", "*"), socks)
-
- def test_unsubscribe(self):
- socks = [ 's1', 's2', 's3', 's4', 's5' ]
- for s in socks:
- self.sm.subscribe("a", "*", s)
- self.assertTrue(self.sm.unsubscribe("a", "*", 's3'))
- # Unsubscribe from group it is not in
- self.assertFalse(self.sm.unsubscribe("a", "*", 's42'))
- self.assertEqual(self.sm.find_sub("a", "*"),
- [ 's1', 's2', 's4', 's5' ])
-
- def test_unsubscribe_all(self):
- self.sm.subscribe('g1', 'i1', 's1')
- self.sm.subscribe('g1', 'i1', 's2')
- self.sm.subscribe('g1', 'i2', 's1')
- self.sm.subscribe('g1', 'i2', 's2')
- self.sm.subscribe('g2', 'i1', 's1')
- self.sm.subscribe('g2', 'i1', 's2')
- self.sm.subscribe('g2', 'i2', 's1')
- self.sm.subscribe('g2', 'i2', 's2')
- self.assertEqual(set([('g1', 'i1'), ('g1', 'i2'), ('g2', 'i1'),
- ('g2', 'i2')]),
- set(self.sm.unsubscribe_all('s1')))
- self.assertEqual(self.sm.find_sub("g1", "i1"), [ 's2' ])
- self.assertEqual(self.sm.find_sub("g1", "i2"), [ 's2' ])
- self.assertEqual(self.sm.find_sub("g2", "i1"), [ 's2' ])
- self.assertEqual(self.sm.find_sub("g2", "i2"), [ 's2' ])
-
- def test_find(self):
- self.sm.subscribe('g1', 'i1', 's1')
- self.sm.subscribe('g1', '*', 's2')
- self.assertEqual(set(self.sm.find("g1", "i1")), set([ 's1', 's2' ]))
-
- def test_find_sub(self):
- self.sm.subscribe('g1', 'i1', 's1')
- self.sm.subscribe('g1', '*', 's2')
- self.assertEqual(self.sm.find_sub("g1", "i1"), [ 's1' ])
-
- def test_open_socket_parameter(self):
- self.assertFalse(os.path.exists("./my_socket_file"))
- msgq = MsgQ("./my_socket_file");
- msgq.setup()
- self.assertTrue(os.path.exists("./my_socket_file"))
- msgq.shutdown();
- self.assertFalse(os.path.exists("./my_socket_file"))
-
- def test_open_socket_environment_variable(self):
- self.assertFalse(os.path.exists("my_socket_file"))
- os.environ["BIND10_MSGQ_SOCKET_FILE"] = "./my_socket_file"
- msgq = MsgQ();
- msgq.setup()
- self.assertTrue(os.path.exists("./my_socket_file"))
- msgq.shutdown();
- self.assertFalse(os.path.exists("./my_socket_file"))
-
- def test_open_socket_default(self):
- env_var = None
- orig_socket_file = None
- if "BIND10_MSGQ_SOCKET_FILE" in os.environ:
- env_var = os.environ["BIND10_MSGQ_SOCKET_FILE"]
- del os.environ["BIND10_MSGQ_SOCKET_FILE"]
- # temporarily replace the class "default" not to be disrupted by
- # any running BIND 10 instance.
- if "BIND10_TEST_SOCKET_FILE" in os.environ:
- MsgQ.SOCKET_FILE = os.environ["BIND10_TEST_SOCKET_FILE"]
- socket_file = MsgQ.SOCKET_FILE
- self.assertFalse(os.path.exists(socket_file))
- msgq = MsgQ();
- try:
- msgq.setup()
- self.assertTrue(os.path.exists(socket_file))
- msgq.shutdown()
- self.assertFalse(os.path.exists(socket_file))
- except socket.error:
- # ok, the install path doesn't exist at all,
- # so we can't check any further
- pass
- if env_var is not None:
- os.environ["BIND10_MSGQ_SOCKET_FILE"] = env_var
- if orig_socket_file is not None:
- MsgQ.SOCKET_FILE = orig_socket_file
-
- def test_open_socket_bad(self):
- msgq = MsgQ("/does/not/exist")
- self.assertRaises(socket.error, msgq.setup)
- # But we can clean up after that.
- msgq.shutdown()
-
- def test_subscribe_cfgmgr(self):
- """Test special handling of the config manager. Once it subscribes,
- the message queue needs to connect and read the config. But not
- before and only once.
- """
- self.assertEqual(0, self.__cfgmgr_ready_called)
- # Not called when something else subscribes
- self.sm.subscribe('SomethingElse', '*', 's1')
- self.assertEqual(0, self.__cfgmgr_ready_called)
- # Called whenever the config manager subscribes
- self.sm.subscribe('ConfigManager', '*', 's2')
- self.assertEqual(1, self.__cfgmgr_ready_called)
- # But not called again when it subscribes again (should not
- # happen in practice, but we make sure anyway)
- self.sm.subscribe('ConfigManager', '*', 's3')
- self.assertEqual(1, self.__cfgmgr_ready_called)
-
-class MsgQTest(unittest.TestCase):
- """
- Tests for the behaviour of MsgQ. This is for the core of MsgQ, other
- subsystems are in separate test fixtures.
- """
- def setUp(self):
- self.__msgq = MsgQ()
-
- def parse_msg(self, msg):
- """
- Parse a binary representation of message to the routing header and the
- data payload. It assumes the message is correctly encoded and the
- payload is not omitted. It'd probably throw in other cases, but we
- don't use it in such situations in this test.
- """
- (length, header_len) = struct.unpack('>IH', msg[:6])
- header = json.loads(msg[6:6 + header_len].decode('utf-8'))
- data = json.loads(msg[6 + header_len:].decode('utf-8'))
- return (header, data)
-
- def test_unknown_command(self):
- """
- Test the command handler returns error when the command is unknown.
- """
- # Fake we are running, to disable test workarounds
- self.__msgq.running = True
- self.assertEqual({'result': [1, "unknown command: unknown"]},
- self.__msgq.command_handler('unknown', {}))
-
- def test_get_members(self):
- """
- Test getting members of a group or of all connected clients.
- """
- # Push two dummy "clients" into msgq (the ugly way, by directly
- # tweaking relevant data structures).
- class Sock:
- def __init__(self, fileno):
- self.fileno = lambda: fileno
- self.__msgq.lnames['first'] = Sock(1)
- self.__msgq.lnames['second'] = Sock(2)
- self.__msgq.fd_to_lname[1] = 'first'
- self.__msgq.fd_to_lname[2] = 'second'
- # Subscribe them to some groups
- self.__msgq.process_command_subscribe(self.__msgq.lnames['first'],
- {'group': 'G1', 'instance': '*'},
- None)
- self.__msgq.process_command_subscribe(self.__msgq.lnames['second'],
- {'group': 'G1', 'instance': '*'},
- None)
- self.__msgq.process_command_subscribe(self.__msgq.lnames['second'],
- {'group': 'G2', 'instance': '*'},
- None)
- # Now query content of some groups through the command handler.
- self.__msgq.running = True # Enable the command handler
- def check_both(result):
- """
- Check the result is successful one and it contains both lnames (in
- any order).
- """
- array = result['result'][1]
- self.assertEqual(set(['first', 'second']), set(array))
- self.assertEqual({'result': [0, array]}, result)
- # Make sure the result can be encoded as JSON
- # (there seems to be types that look like a list but JSON choks
- # on them)
- json.dumps(result)
- # Members of the G1 and G2
- self.assertEqual({'result': [0, ['second']]},
- self.__msgq.command_handler('members',
- {'group': 'G2'}))
- check_both(self.__msgq.command_handler('members', {'group': 'G1'}))
- # We pretend that all the possible groups exist, just that most
- # of them are empty. So requesting for Empty is request for an empty
- # group and should not fail.
- self.assertEqual({'result': [0, []]},
- self.__msgq.command_handler('members',
- {'group': 'Empty'}))
- # Without the name of the group, we just get all the clients.
- check_both(self.__msgq.command_handler('members', {}))
- # Omitting the parameters completely in such case is OK
- check_both(self.__msgq.command_handler('members', None))
-
- def notifications_setup(self):
- """
- Common setup of some notifications tests. Mock several things.
- """
- # Mock the method to send notifications (we don't really want
- # to send them now, just see they'd be sent).
- # Mock the poller, as we don't need it at all (and we don't have
- # real socket to give it now).
- notifications = []
- def send_notification(event, params):
- notifications.append((event, params))
- class FakePoller:
- def register(self, socket, mode):
- pass
- def unregister(self, sock):
- pass
- self.__msgq.members_notify = send_notification
- self.__msgq.poller = FakePoller()
-
- # Create a socket
- class Sock:
- def __init__(self, fileno):
- self.fileno = lambda: fileno
- def close(self):
- pass
- sock = Sock(1)
- return notifications, sock
-
- def test_notifies(self):
- """
- Test the message queue sends notifications about connecting,
- disconnecting and subscription changes.
- """
- notifications, sock = self.notifications_setup()
-
- # We should notify about new cliend when we register it
- self.__msgq.register_socket(sock)
- lname = self.__msgq.fd_to_lname[1] # Steal the lname
- self.assertEqual([('connected', {'client': lname})], notifications)
- del notifications[:]
-
- # A notification should happen for a subscription to a group
- self.__msgq.process_command_subscribe(sock, {'group': 'G',
- 'instance': '*'},
- None)
- self.assertEqual([('subscribed', {'client': lname, 'group': 'G'})],
- notifications)
- del notifications[:]
-
- # As well for unsubscription
- self.__msgq.process_command_unsubscribe(sock, {'group': 'G',
- 'instance': '*'},
- None)
- self.assertEqual([('unsubscribed', {'client': lname, 'group': 'G'})],
- notifications)
- del notifications[:]
-
- # Unsubscription from a group it isn't subscribed to
- self.__msgq.process_command_unsubscribe(sock, {'group': 'H',
- 'instance': '*'},
- None)
- self.assertEqual([], notifications)
-
- # And, finally, for removal of client
- self.__msgq.kill_socket(sock.fileno(), sock)
- self.assertEqual([('disconnected', {'client': lname})], notifications)
-
- def test_notifies_implicit_kill(self):
- """
- Test that the unsubscription notifications are sent before the socket
- is dropped, even in case it does not unsubscribe explicitly.
- """
- notifications, sock = self.notifications_setup()
-
- # Register and subscribe. Notifications for these are in above test.
- self.__msgq.register_socket(sock)
- lname = self.__msgq.fd_to_lname[1] # Steal the lname
- self.__msgq.process_command_subscribe(sock, {'group': 'G',
- 'instance': '*'},
- None)
- del notifications[:]
-
- self.__msgq.kill_socket(sock.fileno(), sock)
- # Now, the notification for unsubscribe should be first, second for
- # the disconnection.
- self.assertEqual([('unsubscribed', {'client': lname, 'group': 'G'}),
- ('disconnected', {'client': lname})
- ], notifications)
-
- def test_undeliverable_errors(self):
- """
- Send several packets through the MsgQ and check it generates
- undeliverable notifications under the correct circumstances.
-
- The test is not exhaustive as it doesn't test all combination
- of existence of the recipient, addressing schemes, want_answer
- header and the reply header. It is not needed, these should
- be mostly independent. That means, for example, if the message
- is a reply and there's no recipient to send it to, the error
- would not be generated no matter if we addressed the recipient
- by lname or group. If we included everything, the test would
- have too many scenarios with little benefit.
- """
- self.__sent_messages = []
- def fake_send_prepared_msg(socket, msg):
- self.__sent_messages.append((socket, msg))
- return True
- self.__msgq.send_prepared_msg = fake_send_prepared_msg
- # These would be real sockets in the MsgQ, but we pass them as
- # parameters only, so we don't need them to be. We use simple
- # integers to tell one from another.
- sender = 1
- recipient = 2
- another_recipiet = 3
- # The routing headers and data to test with.
- routing = {
- 'to': '*',
- 'from': 'sender',
- 'group': 'group',
- 'instance': '*',
- 'seq': 42
- }
- data = {
- "data": "Just some data"
- }
-
- # Some common checking patterns
- def check_error():
- self.assertEqual(1, len(self.__sent_messages))
- self.assertEqual(1, self.__sent_messages[0][0])
- self.assertEqual(({
- 'group': 'group',
- 'instance': '*',
- 'reply': 42,
- 'seq': 42,
- 'from': 'msgq',
- 'to': 'sender',
- 'want_answer': True
- }, {'result': [-1, "No such recipient"]}),
- self.parse_msg(self.__sent_messages[0][1]))
- self.__sent_messages = []
-
- def check_no_message():
- self.assertEqual([], self.__sent_messages)
-
- def check_delivered(rcpt_socket=recipient):
- self.assertEqual(1, len(self.__sent_messages))
- self.assertEqual(rcpt_socket, self.__sent_messages[0][0])
- self.assertEqual((routing, data),
- self.parse_msg(self.__sent_messages[0][1]))
- self.__sent_messages = []
-
- # Send the message. No recipient, but errors are not requested,
- # so none is generated.
- self.__msgq.process_command_send(sender, routing, data)
- check_no_message()
-
- # It should act the same if we explicitly say we do not want replies.
- routing["want_answer"] = False
- self.__msgq.process_command_send(sender, routing, data)
- check_no_message()
-
- # Ask for errors if it can't be delivered.
- routing["want_answer"] = True
- self.__msgq.process_command_send(sender, routing, data)
- check_error()
-
- # If the message is a reply itself, we never generate the errors
- routing["reply"] = 3
- self.__msgq.process_command_send(sender, routing, data)
- check_no_message()
-
- # If there are recipients (but no "reply" header), the error should not
- # be sent and the message should get delivered.
- del routing["reply"]
- self.__msgq.subs.find = lambda group, instance: [recipient]
- self.__msgq.process_command_send(sender, routing, data)
- check_delivered()
-
- # When we send a direct message and the recipient is not there, we get
- # the error too
- routing["to"] = "lname"
- self.__msgq.process_command_send(sender, routing, data)
- check_error()
-
- # But when the recipient is there, it is delivered and no error is
- # generated.
- self.__msgq.lnames["lname"] = recipient
- self.__msgq.process_command_send(sender, routing, data)
- check_delivered()
-
- # If an attempt to send fails, consider it no recipient.
- def fail_send_prepared_msg(socket, msg):
- '''
- Pretend sending a message failed. After one call, return to the
- usual mock, so the errors or other messages can be sent.
- '''
- self.__msgq.send_prepared_msg = fake_send_prepared_msg
- return False
-
- self.__msgq.send_prepared_msg = fail_send_prepared_msg
- self.__msgq.process_command_send(sender, routing, data)
- check_error()
-
- # But if there are more recipients and only one fails, it should
- # be delivered to the other and not considered an error
- self.__msgq.send_prepared_msg = fail_send_prepared_msg
- routing["to"] = '*'
- self.__msgq.subs.find = lambda group, instance: [recipient,
- another_recipiet]
- self.__msgq.process_command_send(sender, routing, data)
- check_delivered(rcpt_socket=another_recipiet)
-
-class DummySocket:
- """
- Dummy socket class.
- This one does nothing at all, but some calls are used.
- It is mainly intended to override the listen socket for msgq, which
- we do not need in these tests.
- """
- def fileno():
- return -1
-
- def close():
- pass
-
-class BadSocket:
- """
- Special socket wrapper class. Once given a socket in its constructor,
- it completely behaves like that socket, except that its send() call
- will only actually send one byte per call, and optionally raise a given
- exception at a given time.
- """
- def __init__(self, real_socket, raise_on_send=0, send_exception=None):
- """
- Parameters:
- real_socket: The actual socket to wrap
- raise_on_send: integer. If higher than 0, and send_exception is
- not None, send_exception will be raised on the
- 'raise_on_send'th call to send().
- send_exception: if not None, this exception will be raised
- (if raise_on_send is not 0)
- """
- self.socket = real_socket
- self.send_count = 0
- self.raise_on_send = raise_on_send
- self.send_exception = send_exception
-
- # completely wrap all calls and member access
- # (except explicitly overridden ones)
- def __getattr__(self, name, *args):
- attr = getattr(self.socket, name)
- if isinstance(attr, collections.Callable):
- def callable_attr(*args):
- return attr.__call__(*args)
- return callable_attr
- else:
- return attr
-
- def send(self, data):
- self.send_count += 1
- if self.send_exception is not None and\
- self.send_count == self.raise_on_send:
- raise self.send_exception
-
- if len(data) > 0:
- return self.socket.send(data[:1])
- else:
- return 0
-
-class MsgQThread(threading.Thread):
- """
- Very simple thread class that runs msgq.run() when started,
- and stores the exception that msgq.run() raises, if any.
- """
- def __init__(self, msgq):
- threading.Thread.__init__(self)
- self.msgq_ = msgq
- self.caught_exception = None
- self.lock = threading.Lock()
-
- def run(self):
- try:
- self.msgq_.run()
- except Exception as exc:
- # Store the exception to make the test fail if necessary
- self.caught_exception = exc
-
- def stop(self):
- self.msgq_.stop()
-
-class SendNonblock(unittest.TestCase):
- """
- Tests that the whole thing will not get blocked if someone does not read.
- """
-
- def terminate_check(self, task, timeout=30):
- """
- Runs task in separate process (task is a function) and checks
- it terminates sooner than timeout.
- """
- task_pid = os.fork()
- if task_pid == 0:
- # Kill the forked process after timeout by SIGALRM
- signal.alarm(timeout)
- # Run the task
- # If an exception happens or we run out of time, we terminate
- # with non-zero
- task()
- # If we got here, then everything worked well and in time
- # In that case, we terminate successfully
- os._exit(0) # needs exit code
- else:
- (pid, status) = os.waitpid(task_pid, 0)
- self.assertEqual(0, status,
- "The task did not complete successfully in time")
-
- def get_msgq_with_sockets(self):
- '''
- Create a message queue and prepare it for use with a socket pair.
- The write end is put into the message queue, so we can check it.
- It returns (msgq, read_end, write_end). It is expected the sockets
- are closed by the caller afterwards.
-
- Also check the sockets are registered correctly (eg. internal data
- structures are there for them).
- '''
- msgq = MsgQ()
- # We do only partial setup, so we don't create the listening socket
- (read, write) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
- msgq.register_socket(write)
- self.assertEqual(1, len(msgq.lnames))
- self.assertEqual(write, msgq.lnames[msgq.fd_to_lname[write.fileno()]])
- return (msgq, read, write)
-
- def infinite_sender(self, sender):
- """
- Sends data until an exception happens. socket.error is caught,
- as it means the socket got closed. Sender is called to actually
- send the data.
- """
- (msgq, read, write) = self.get_msgq_with_sockets()
- # Keep sending while it is not closed by the msgq
- try:
- while True:
- sender(msgq, write)
- except socket.error:
- pass
-
- # Explicitly close temporary socket pair as the Python
- # interpreter expects it. It may not be 100% exception safe,
- # but since this is only for tests we prefer brevity.
- # Actually, the write end is often closed by the sender.
- if write.fileno() != -1:
- # Some of the senders passed here kill the socket internally.
- # So kill it only if not yet done so. If the socket is closed,
- # it gets -1 as fileno().
- msgq.kill_socket(write.fileno(), write)
- self.assertFalse(msgq.lnames)
- self.assertFalse(msgq.fd_to_lname)
- read.close()
-
- def test_infinite_sendmsg(self):
- """
- Tries sending messages (and not reading them) until it either times
- out (in blocking call, wrong) or closes it (correct).
- """
- data = "data"
- for i in range(1, 10):
- data += data
- self.terminate_check(lambda: self.infinite_sender(
- lambda msgq, socket: msgq.sendmsg(socket, {}, {"message" : data})))
-
- def test_infinite_sendprepared(self):
- """
- Tries sending data (and not reading them) until it either times
- out (in blocking call, wrong) or closes it (correct).
- """
- data = b"data"
- for i in range(1, 10):
- data += data
- self.terminate_check(lambda: self.infinite_sender(
- lambda msgq, socket: msgq.send_prepared_msg(socket, data)))
-
- def test_sendprepared_success(self):
- '''
- Test the send_prepared_msg returns success when queueing messages.
- It does so on the first attempt (when it actually tries to send
- something to the socket) and on any attempt that follows and the
- buffer is already full.
- '''
- (msgq, read, write) = self.get_msgq_with_sockets()
- # Now keep sending until we fill in something into the internal
- # buffer.
- while not write.fileno() in msgq.sendbuffs:
- self.assertTrue(msgq.send_prepared_msg(write, b'data'))
- read.close()
- write.close()
-
- def test_sendprepared_epipe(self):
- '''
- Test the send_prepared_msg returns false when we try to queue a
- message and the other side is not there any more. It should be done
- with EPIPE, so not a fatal error.
- '''
- (msgq, read, write) = self.get_msgq_with_sockets()
- # Close one end. It should make a EPIPE on the other.
- read.close()
- # Now it should soft-fail
- self.assertFalse(msgq.send_prepared_msg(write, b'data'))
- write.close()
-
- def send_many(self, data):
- """
- Tries that sending a command many times and getting an answer works.
- """
- msgq = MsgQ()
- # msgq.run needs to compare with the listen_socket, so we provide
- # a replacement
- msgq.listen_socket = DummySocket
- (queue, out) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
- def run():
- length = len(data)
- queue_pid = os.fork()
- if queue_pid == 0:
- signal.alarm(120)
- msgq.setup_signalsock()
- msgq.register_socket(queue)
- msgq.run()
- msgq.cleanup_signalsock()
- else:
- try:
- def killall(signum, frame):
- os.kill(queue_pid, signal.SIGTERM)
- os._exit(1)
- signal.signal(signal.SIGALRM, killall)
- msg = msgq.preparemsg({"type" : "ping"}, data)
- now = time.clock()
- while time.clock() - now < 0.2:
- out.sendall(msg)
- # Check the answer
- (routing, received) = msgq.read_packet(out.fileno(),
- out)
- self.assertEqual({"type" : "pong"},
- isc.cc.message.from_wire(routing))
- self.assertEqual(data, received)
- finally:
- os.kill(queue_pid, signal.SIGTERM)
- self.terminate_check(run)
-
- # Explicitly close temporary socket pair as the Python
- # interpreter expects it. It may not be 100% exception safe,
- # but since this is only for tests we prefer brevity.
- queue.close()
- out.close()
-
- def test_small_sends(self):
- """
- Tests sending small data many times.
- """
- self.send_many(b"data")
-
- def test_large_sends(self):
- """
- Tests sending large data many times.
- """
- data = b"data"
- for i in range(1, 20):
- data = data + data
- self.send_many(data)
-
- def do_send(self, write, read, control_write, control_read,
- expect_arrive=True, expect_send_exception=None):
- """
- Makes a msgq object that is talking to itself,
- run it in a separate thread so we can use and
- test run().
- It is given two sets of connected sockets; write/read, and
- control_write/control_read. The former may be throwing errors
- and mangle data to test msgq. The second is mainly used to
- send msgq the stop command.
- (Note that the terms 'read' and 'write' are from the msgq
- point of view, so the test itself writes to 'control_read')
- Parameters:
- write: a socket that is used to send the data to
- read: a socket that is used to read the data from
- control_write: a second socket for communication with msgq
- control_read: a second socket for communication with msgq
- expect_arrive: if True, the read socket is read from, and the data
- that is read is expected to be the same as the data
- that has been sent to the write socket.
- expect_send_exception: if not None, this is the exception that is
- expected to be raised by msgq
- """
-
- # Some message and envelope data to send and check
- env = b'{"env": "foo"}'
- msg = b'{"msg": "bar"}'
-
- msgq = MsgQ()
- # Don't need a listen_socket
- msgq.listen_socket = DummySocket
- msgq.setup_signalsock()
- msgq.register_socket(write)
- msgq.register_socket(control_write)
- # Queue the message for sending
- msgq.sendmsg(write, env, msg)
-
- # Run it in a thread
- msgq_thread = MsgQThread(msgq)
- # If we're done, just kill it
- msgq_thread.start()
-
- if expect_arrive:
- (recv_env, recv_msg) = msgq.read_packet(read.fileno(),
- read)
- self.assertEqual(env, recv_env)
- self.assertEqual(msg, recv_msg)
-
- # Tell msgq to stop
- msg = msgq.preparemsg({"type" : "stop"})
- control_read.sendall(msg)
-
- # Wait for thread to stop if it hasn't already.
- # Put in a (long) timeout; the thread *should* stop, but if it
- # does not, we don't want the test to hang forever
- msgq_thread.join(60)
- # Fail the test if it didn't stop
- self.assertFalse(msgq_thread.isAlive(), "Thread did not stop")
-
- # Clean up some internals of msgq (usually called as part of
- # shutdown, but we skip that one here)
- msgq.cleanup_signalsock()
-
- # Check the exception from the thread, if any
- # First, if we didn't expect it; reraise it (to make test fail and
- # show the stacktrace for debugging)
- if expect_send_exception is None:
- if msgq_thread.caught_exception is not None:
- raise msgq_thread.caught_exception
- else:
- # If we *did* expect it, fail it there was none
- self.assertIsNotNone(msgq_thread.caught_exception)
-
- def do_send_with_send_error(self, raise_on_send, send_exception,
- expect_answer=True,
- expect_send_exception=None):
- """
- Sets up two connected sockets, wraps the sender socket into a BadSocket
- class, then performs a do_send() test.
- Parameters:
- raise_on_send: the byte at which send_exception should be raised
- (see BadSocket)
- send_exception: the exception to raise (see BadSocket)
- expect_answer: whether the send is expected to complete (and hence
- the read socket should get the message)
- expect_send_exception: the exception msgq is expected to raise when
- send_exception is raised by BadSocket.
- """
- (write, read) = socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)
- (control_write, control_read) = socket.socketpair(socket.AF_UNIX,
- socket.SOCK_STREAM)
- badwrite = BadSocket(write, raise_on_send, send_exception)
- self.do_send(badwrite, read, control_write, control_read,
- expect_answer, expect_send_exception)
- write.close()
- read.close()
- control_write.close()
- control_read.close()
-
- def test_send_raise_recoverable(self):
- """
- Test whether msgq survices a recoverable socket errors when sending.
- Two tests are done: one where the error is raised on the 3rd octet,
- and one on the 23rd.
- """
- for err in [ errno.EAGAIN, errno.EWOULDBLOCK, errno.EINTR ]:
- sockerr = socket.error(err, 'Socket error')
- self.do_send_with_send_error(3, sockerr)
- self.do_send_with_send_error(23, sockerr)
-
- def test_send_raise_nonrecoverable(self):
- """
- Test whether msgq survives socket errors that are nonrecoverable
- (for said socket that is, i.e. EPIPE etc).
- Two tests are done: one where the error is raised on the 3rd octet,
- and one on the 23rd.
- """
- for err in [ errno.EPIPE, errno.ENOBUFS, errno.ECONNRESET ]:
- sockerr = socket.error(err, 'Socket error')
- self.do_send_with_send_error(3, sockerr, False)
- self.do_send_with_send_error(23, sockerr, False)
-
- def otest_send_raise_crash(self):
- """
- Test whether msgq does NOT survive on a general exception.
- Note, perhaps it should; but we'd have to first discuss and decide
- how it should recover (i.e. drop the socket and consider the client
- dead?
- It may be a coding problem in msgq itself, and we certainly don't
- want to ignore those.
- """
- sockerr = Exception("just some general exception")
- self.do_send_with_send_error(3, sockerr, False, sockerr)
- self.do_send_with_send_error(23, sockerr, False, sockerr)
-
-class ThreadTests(unittest.TestCase):
- """Test various things around thread synchronization."""
-
- def setUp(self):
- self.__msgq = MsgQ()
- self.__abort_wait = False
- self.__result = None
- self.__notify_thread = threading.Thread(target=self.__notify)
- self.__wait_thread = threading.Thread(target=self.__wait)
- # Make sure the threads are killed if left behind by the test.
- self.__notify_thread.daemon = True
- self.__wait_thread.daemon = True
-
- def __notify(self):
- """Call the cfgmgr_ready."""
- if self.__abort_wait:
- self.__msgq.cfgmgr_ready(False)
- else:
- self.__msgq.cfgmgr_ready()
-
- def __wait(self):
- """Wait for config manager and store the result."""
- self.__result = self.__msgq.wait_cfgmgr()
-
- def test_wait_cfgmgr(self):
- """One thread signals the config manager subscribed, the other
- waits for it. We then check it terminated correctly.
- """
- self.__notify_thread.start()
- self.__wait_thread.start()
- # Timeout to ensure the test terminates even on failure
- self.__wait_thread.join(60)
- self.assertTrue(self.__result)
-
- def test_wait_cfgmgr_2(self):
- """Same as test_wait_cfgmgr, but starting the threads in reverse order
- (the result should be the same).
- """
- self.__wait_thread.start()
- self.__notify_thread.start()
- # Timeout to ensure the test terminates even on failure
- self.__wait_thread.join(60)
- self.assertTrue(self.__result)
-
- def test_wait_abort(self):
- """Similar to test_wait_cfgmgr, but the config manager is never
- subscribed and it is aborted.
- """
- self.__abort_wait = True
- self.__wait_thread.start()
- self.__notify_thread.start()
- # Timeout to ensure the test terminates even on failure
- self.__wait_thread.join(60)
- self.assertIsNotNone(self.__result)
- self.assertFalse(self.__result)
-
- def __check_ready_and_abort(self):
- """Check that when we first say the config manager is ready and then
- try to abort, it uses the first result.
- """
- self.__msgq.cfgmgr_ready()
- self.__msgq.cfgmgr_ready(False)
- self.__result = self.__msgq.wait_cfgmgr()
-
- def test_ready_and_abort(self):
- """Perform the __check_ready_and_abort test, but in a separate thread,
- so in case something goes wrong with the synchronisation and it
- deadlocks, the test will terminate anyway.
- """
- test_thread = threading.Thread(target=self.__check_ready_and_abort)
- test_thread.daemon = True
- test_thread.start()
- test_thread.join(60)
- self.assertTrue(self.__result)
-
-class SocketTests(unittest.TestCase):
- '''Test cases for micro behaviors related to socket operations.
-
- Some cases are covered as part of other tests, but in this fixture
- we check more details of specific method related to socket operation,
- with the help of mock classes to avoid expensive overhead.
-
- '''
- class MockSocket():
- '''A mock socket used instead of standard socket objects.'''
- def __init__(self):
- self.ex_on_send = None # raised from send() if not None
- self.recv_result = b'test' # dummy data or exception
- self.blockings = [] # history of setblocking() params
- def setblocking(self, on):
- self.blockings.append(on)
- def send(self, data):
- if self.ex_on_send is not None:
- raise self.ex_on_send
- return 10 # arbitrary choice
- def recv(self, len):
- if isinstance(self.recv_result, Exception):
- raise self.recv_result
- ret = self.recv_result
- self.recv_result = b'' # if called again, return empty data
- return ret
- def fileno(self):
- return 42 # arbitrary choice
-
- class LoggerWrapper():
- '''A simple wrapper of logger to inspect log messages.'''
- def __init__(self, logger):
- self.error_called = 0
- self.warn_called = 0
- self.debug_called = 0
- self.orig_logger = logger
- def error(self, *args):
- self.error_called += 1
- self.orig_logger.error(*args)
- def warn(self, *args):
- self.warn_called += 1
- self.orig_logger.warn(*args)
- def debug(self, *args):
- self.debug_called += 1
- self.orig_logger.debug(*args)
-
- def mock_kill_socket(self, fileno, sock):
- '''A replacement of MsgQ.kill_socket method for inspection.'''
- self.__killed_socket = (fileno, sock)
- if fileno in self.__msgq.sockets:
- del self.__msgq.sockets[fileno]
-
- def setUp(self):
- self.__msgq = MsgQ()
- self.__msgq.kill_socket = self.mock_kill_socket
- self.__sock = self.MockSocket()
- self.__data = b'dummy'
- self.__msgq.sockets[42] = self.__sock
- self.__msgq.sendbuffs[42] = (None, b'testdata')
- self.__sock_error = socket.error()
- self.__killed_socket = None
- self.__logger = self.LoggerWrapper(msgq.logger)
- msgq.logger = self.__logger
- self.__orig_select = msgq.select.select
-
- def tearDown(self):
- msgq.logger = self.__logger.orig_logger
- msgq.select.select = self.__orig_select
-
- def test_send_data(self):
- # Successful case: _send_data() returns the hardcoded value, and
- # setblocking() is called twice with the expected parameters
- self.assertEqual(10, self.__msgq._send_data(self.__sock, self.__data))
- self.assertEqual([0, 1], self.__sock.blockings)
- self.assertIsNone(self.__killed_socket)
-
- def test_send_data_interrupt(self):
- '''send() is interrupted. send_data() returns 0, sock isn't killed.'''
- expected_blockings = []
- for eno in [errno.EAGAIN, errno.EWOULDBLOCK, errno.EINTR]:
- self.__sock_error.errno = eno
- self.__sock.ex_on_send = self.__sock_error
- self.assertEqual(0, self.__msgq._send_data(self.__sock,
- self.__data))
- expected_blockings.extend([0, 1])
- self.assertEqual(expected_blockings, self.__sock.blockings)
- self.assertIsNone(self.__killed_socket)
-
- def test_send_data_error(self):
- '''Unexpected error happens on send(). The socket is killed.
-
- If the error is EPIPE, it's logged at the warn level; otherwise
- an error message is logged.
-
- '''
- expected_blockings = []
- expected_errors = 0
- expected_warns = 0
- for eno in [errno.EPIPE, errno.ECONNRESET, errno.ENOBUFS]:
- self.__sock_error.errno = eno
- self.__sock.ex_on_send = self.__sock_error
- self.__killed_socket = None # clear any previuos value
- self.assertEqual(None, self.__msgq._send_data(self.__sock,
- self.__data))
- self.assertEqual((42, self.__sock), self.__killed_socket)
- expected_blockings.extend([0, 1])
- self.assertEqual(expected_blockings, self.__sock.blockings)
-
- if eno == errno.EPIPE:
- expected_warns += 1
- else:
- expected_errors += 1
- self.assertEqual(expected_errors, self.__logger.error_called)
- self.assertEqual(expected_warns, self.__logger.warn_called)
-
- def test_process_packet(self):
- '''Check some failure cases in handling an incoming message.'''
- expected_errors = 0
- expected_debugs = 0
-
- # if socket.recv() fails due to socket.error, it will be logged
- # as error and the socket will be killed regardless of errno.
- for eno in [errno.ENOBUFS, errno.ECONNRESET]:
- self.__sock_error.errno = eno
- self.__sock.recv_result = self.__sock_error
- self.__killed_socket = None # clear any previuos value
- self.__msgq.process_packet(42, self.__sock)
- self.assertEqual((42, self.__sock), self.__killed_socket)
- expected_errors += 1
- self.assertEqual(expected_errors, self.__logger.error_called)
- self.assertEqual(expected_debugs, self.__logger.debug_called)
-
- # if socket.recv() returns empty data, the result depends on whether
- # there's any preceding data; in the second case below, at least
- # 6 bytes of data will be expected, and the second call to our faked
- # recv() returns empty data. In that case it will be logged as error.
- for recv_data in [b'', b'short']:
- self.__sock.recv_result = recv_data
- self.__killed_socket = None
- self.__msgq.process_packet(42, self.__sock)
- self.assertEqual((42, self.__sock), self.__killed_socket)
- if len(recv_data) == 0:
- expected_debugs += 1
- else:
- expected_errors += 1
- self.assertEqual(expected_errors, self.__logger.error_called)
- self.assertEqual(expected_debugs, self.__logger.debug_called)
-
- def test_do_select(self):
- """
- Check the behaviour of the run_select method.
-
- In particular, check that we skip writing to the sockets we read,
- because a read may have side effects (like closing the socket) and
- we want to prevent strange behavior.
- """
- self.__read_called = []
- self.__write_called = []
- self.__reads = None
- self.__writes = None
- def do_read(fd, socket):
- self.__read_called.append(fd)
- self.__msgq.running = False
- def do_write(fd):
- self.__write_called.append(fd)
- self.__msgq.running = False
- self.__msgq.process_packet = do_read
- self.__msgq._process_write = do_write
- self.__msgq.fd_to_lname = {42: 'lname', 44: 'other', 45: 'unused'}
- # The do_select does index it, but just passes the value. So reuse
- # the dict to safe typing in the test.
- self.__msgq.sockets = self.__msgq.fd_to_lname
- self.__msgq.sendbuffs = {42: 'data', 43: 'data'}
- def my_select(reads, writes, errors):
- self.__reads = reads
- self.__writes = writes
- self.assertEqual([], errors)
- return ([42, 44], [42, 43], [])
- msgq.select.select = my_select
- self.__msgq.listen_socket = DummySocket
-
- self.__msgq.running = True
- self.__msgq.run_select()
-
- self.assertEqual([42, 44], self.__read_called)
- self.assertEqual([43], self.__write_called)
- self.assertEqual({42, 44, 45}, set(self.__reads))
- self.assertEqual({42, 43}, set(self.__writes))
-
-if __name__ == '__main__':
- isc.log.resetUnitTestRootLogger()
- unittest.main()
+++ /dev/null
-/b10-stats
-/b10-stats-httpd
-/stats.py
-/stats_httpd.py
-/b10-stats-httpd.8
-/b10-stats.8
+++ /dev/null
-SUBDIRS = tests
-
-pkglibexecdir = $(libexecdir)/@PACKAGE@
-
-pkglibexec_SCRIPTS = b10-stats b10-stats-httpd
-
-b10_statsdir = $(pkgdatadir)
-b10_stats_DATA = stats.spec stats-httpd.spec
-b10_stats_DATA += stats-httpd-xml.tpl stats-httpd-xsd.tpl stats-httpd-xsl.tpl
-
-nodist_pylogmessage_PYTHON = $(PYTHON_LOGMSGPKG_DIR)/work/stats_messages.py
-nodist_pylogmessage_PYTHON += $(PYTHON_LOGMSGPKG_DIR)/work/stats_httpd_messages.py
-pylogmessagedir = $(pyexecdir)/isc/log_messages/
-
-CLEANFILES = b10-stats stats.pyc
-CLEANFILES += b10-stats-httpd stats_httpd.pyc
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/stats_messages.py
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/stats_messages.pyc
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/stats_httpd_messages.py
-CLEANFILES += $(PYTHON_LOGMSGPKG_DIR)/work/stats_httpd_messages.pyc
-
-man_MANS = b10-stats.8 b10-stats-httpd.8
-DISTCLEANFILES = $(man_MANS)
-EXTRA_DIST = $(man_MANS) b10-stats.xml b10-stats-httpd.xml
-EXTRA_DIST += stats.spec stats-httpd.spec
-EXTRA_DIST += stats-httpd-xml.tpl stats-httpd-xsd.tpl stats-httpd-xsl.tpl
-EXTRA_DIST += stats_messages.mes stats_httpd_messages.mes
-
-if GENERATE_DOCS
-
-b10-stats.8: b10-stats.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-stats.xml
-
-b10-stats-httpd.8: b10-stats-httpd.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-stats-httpd.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 BIND 10 > $@
-
-endif
-
-$(PYTHON_LOGMSGPKG_DIR)/work/stats_messages.py : stats_messages.mes
- $(top_builddir)/src/lib/log/compiler/message \
- -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/stats_messages.mes
-
-$(PYTHON_LOGMSGPKG_DIR)/work/stats_httpd_messages.py : stats_httpd_messages.mes
- $(top_builddir)/src/lib/log/compiler/message \
- -d $(PYTHON_LOGMSGPKG_DIR)/work -p $(srcdir)/stats_httpd_messages.mes
-
-# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
-b10-stats: stats.py $(PYTHON_LOGMSGPKG_DIR)/work/stats_messages.py
- $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" stats.py >$@
- chmod a+x $@
-
-b10-stats-httpd: stats_httpd.py $(PYTHON_LOGMSGPKG_DIR)/work/stats_httpd_messages.py
- $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" stats_httpd.py >$@
- chmod a+x $@
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2011-2012 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>February 28, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-stats-httpd</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-stats-httpd</refname>
- <refpurpose>BIND 10 HTTP server for HTTP/XML interface of statistics</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2011-2012</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis> <command>b10-stats-httpd</command>
- <arg><option>-v</option></arg>|<arg><option>--verbose</option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>
- <command>b10-stats-httpd</command> is a standalone HTTP server. It is
- intended for HTTP/XML interface for statistics module. This server
- process runs as a process separated from the process of the BIND 10 Stats
- daemon (<command>b10-stats</command>). The server is initially executed
- by the b10-init process and eventually
- exited by it. The server is intended to serve requests by HTTP
- clients like web browsers and third-party modules. When the server is
- asked, it requests BIND 10 statistics data or its schema from
- <command>b10-stats</command> which sends the data back in Python
- dictionary format, and the server converts it into XML format. The server
- sends it to the HTTP client. The server can send three types of documents,
- which are XML (Extensible Markup Language), XSD (XML Schema definition)
- and XSL (Extensible Stylesheet Language). The XML document is the
- statistics data of BIND 10, the XSD document is the data schema of it,
- and the XSL document is the style sheet to be showed for the web
- browsers. There is different URL for each document. But please note that
- you would be redirected to the URL of XML document if you request the URL
- of the root document. For example, you would be redirected to
- http://127.0.0.1:8000/bind10/statistics/xml if you request
- http://127.0.0.1:8000/. Please see the manual and the spec file
- of <command>b10-stats</command> for more details about the items of BIND
- 10 statistics. The server uses CC session in communication
- with <command>b10-stats</command>. CC session is provided
- by <command>b10-msgq</command> which is started
- by <command>b10-init</command> in advance. The server is implemented by
- HTTP-server libraries included in Python 3. The server obtains the
- configuration from the config manager (<command>b10-cfgmgr</command>) in
- runtime. Please see below for more details about this spec file and
- configuration of the server.
- </para>
- </refsect1>
-
-<!-- TODO: this is too verbose; move some of this into the guide instead -->
-
- <refsect1>
- <title>OPTIONS</title>
- <para>The argument is as follow:</para>
- <variablelist>
- <varlistentry>
- <term><option>-v</option>, <option>--verbose</option></term>
- <listitem>
- <para>
- <command>b10-stats-httpd</command> switches to verbose mode and sends
- verbose messages to STDOUT.
- </para>
- </listitem>
- </varlistentry>
- </variablelist>
- </refsect1>
-
- <refsect1>
- <title>FILES</title>
- <para>
- <filename>/usr/local/share/bind10/stats-httpd.spec</filename>
- <!--TODO: The filename should be computed from prefix-->
- — the spec file of <command>b10-stats-httpd</command>. This file
- contains configurable settings
- of <command>b10-stats-httpd</command>. This setting can be configured in
- runtime via
- <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>. Please
- see the manual
- of <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum> about
- how to configure the settings.
- </para>
- <para>
- <filename>/usr/local/share/bind10/stats-httpd-xml.tpl</filename>
- <!--TODO: The filename should be computed from prefix-->
- — the template file of XML document.
- </para>
- <para>
- <filename>/usr/local/share/bind10/stats-httpd-xsd.tpl</filename>
- <!--TODO: The filename should be computed from prefix-->
- — the template file of XSD document.
- </para>
- <para>
- <filename>/usr/local/share/bind10/stats-httpd-xsl.tpl</filename>
- <!--TODO: The filename should be computed from prefix-->
- — the template file of XSL document.
- </para>
- </refsect1>
-
- <refsect1>
- <title>CONFIGURATION AND COMMANDS</title>
- <para>
- The configurable setting in
- <filename>stats-httpd.spec</filename> is:
- </para>
- <variablelist>
- <varlistentry>
- <term><varname>listen_on</varname></term>
- <listitem>
- <para>
- a list of pairs of address and port for
- <command>b10-stats-httpd</command> to listen HTTP requests on. The
- pair consists of the <varname>address</varname> string
- and <varname>port</varname> number. The default setting is the list
- of address 127.0.0.1 port 8000. If the server is started by the
- default setting being left, for example, the URL for XML document
- is http://127.0.0.1:8000/bind10/statistics/xml. And also IPv6
- addresses can be configured and they works in the runtime
- environment for dual stack. You can change the settings
- through <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>.
- </para>
- </listitem>
- </varlistentry>
- </variablelist>
- <para>
- The commands in <filename>stats-httpd.spec</filename> are:
- </para>
- <variablelist>
- <varlistentry>
- <term><command>status</command></term>
- <listitem>
- <para>
- shows the status of <command>b10-stats-httpd</command> with its
- PID.
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term><command>shutdown</command></term>
- <listitem>
- <para>
- exits the <command>b10-stats-httpd</command> process.
- This has an optional <varname>pid</varname> argument to
- select the process ID to stop.
- (Note that the b10-init process may restart this service
- if configured.)
- </para>
- </listitem>
- </varlistentry>
- </variablelist>
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-cfgmgr</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-init</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-msgq</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-stats</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- <command>b10-stats-httpd</command> was designed and implemented by Naoki
- Kambe of JPRS in March 2011.
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2010-2012 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>June 20, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-stats</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-stats</refname>
- <refpurpose>BIND 10 statistics module</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2010-2012</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>b10-stats</command>
- <arg><option>-v</option></arg>
- <arg><option>--verbose</option></arg>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>
- The <command>b10-stats</command> daemon collects statistics data
- from each BIND 10 module. Its statistics information may be
- reported via <command>bindctl</command> or
- <command>b10-stats-httpd</command>. It is started by
- <command>b10-init</command> and communicates by using the
- Command Channel by <command>b10-msgq</command> with other
- modules like <command>b10-init</command>, <command>b10-auth</command>
- and so on. <command>b10-stats</command> periodically requests statistics
- data from each module. The interval time can be configured
- via <command>bindctl</command>. <command>b10-stats</command> cannot
- accept any command from other modules for updating statistics data. The
- stats module collects data and
- aggregates it. <command>b10-stats</command> invokes an internal
- command for <command>b10-init</command> after its initial
- starting to make sure it collects statistics data from
- <command>b10-init</command>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>OPTIONS</title>
- <para>The arguments are as follows:</para>
- <variablelist>
- <varlistentry>
- <term><option>-v</option>, <option>--verbose</option></term>
- <listitem>
- <para>
- This enables maximum debug logging.
- </para>
- </listitem>
- </varlistentry>
- </variablelist>
- </refsect1>
-
- <refsect1>
- <title>CONFIGURATION AND COMMANDS</title>
-
- <para>
- The only configurable setting in <filename>stats.spec</filename>
- is:
- </para>
-
- <variablelist>
- <varlistentry>
- <term><varname>poll-interval</varname></term>
- <listitem>
- <para>
- is a time interval in seconds at which <command>b10-stats</command>
- requests each module to return its statistics data. The default is 60
- seconds. Polling can be disabled by setting it to 0. The type of the
- value should be an unsigned integer. Negative integers are ignored.
- </para>
- </listitem>
- </varlistentry>
- </variablelist>
-
-<!-- TODO: formating -->
- <para>
- The configuration commands are:
- </para>
-
- <para>
- <command>show</command> will send the statistics data
- in JSON format.
- By default, it outputs all the statistics data it has collected.
- An optional item name may be specified to receive individual output.
- </para>
-
- <para>
- <command>showschema</command> will send the schema of the statistics data
- in JSON format. The output is equivalent to the statistics part
- of <filename>stats.spec</filename>.
- </para>
-
- <para>
- <command>shutdown</command> will shutdown the
- <command>b10-stats</command> process.
- This has an optional <varname>pid</varname> argument to
- select the process ID to stop.
- (Note that the b10-init process may restart this service
- if configured.)
- </para>
-
- <para>
- <command>status</command> simply indicates that the daemon is
- running.
- </para>
-
- </refsect1>
-
- <refsect1>
- <title>STATISTICS DATA</title>
-
- <para>
- The <command>b10-stats</command> daemon contains these
- <quote>Stats</quote> statistics:
- </para>
-
- <variablelist>
-
-
- <varlistentry>
- <term>boot_time</term>
- <listitem><simpara>The date and time when this daemon was
- started in ISO 8601 format.
- This is a constant which can't be reset except by restarting
- <command>b10-stats</command>.
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>last_update_time</term>
- <listitem><simpara>The date and time (in ISO 8601 format)
- when this daemon last received data from another component.
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>lname</term>
- <listitem><simpara>This is the name used for the
- <command>b10-msgq</command> command-control channel.
- (This is a constant which can't be reset except by restarting
- <command>b10-stats</command>.)
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>report_time</term>
- <listitem><simpara>The latest report date and time in
- ISO 8601 format.</simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>start_time</term>
- <listitem><simpara>This is the date and time (in ISO 8601 format)
- when this daemon started collecting data.
- </simpara></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>timestamp</term>
- <listitem><simpara>The current date and time represented in
- seconds since UNIX epoch (1970-01-01T00:00:00Z) with
- precision (delimited with a period) up to
- one hundred thousandth of second.</simpara></listitem>
- </varlistentry>
-
- </variablelist>
-
- <para>
- See other manual pages for explanations for their statistics
- that are kept track by <command>b10-stats</command>.
- </para>
-
- </refsect1>
-
- <refsect1>
- <title>FILES</title>
- <para><filename>/usr/local/share/bind10/stats.spec</filename>
- <!--TODO: The filename should be computed from prefix-->
- — This is a spec file for <command>b10-stats</command>. It
- contains commands for <command>b10-stats</command>. They can be
- invoked
- via <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-init</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>b10-stats-httpd</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citerefentry>
- <refentrytitle>bindctl</refentrytitle><manvolnum>1</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- The <command>b10-stats</command> daemon was initially designed
- and implemented by Naoki Kambe of JPRS in October 2010.
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<?xml-stylesheet type="text/xsl" href="$xsl_url_path"?>
-$xml_string
\ No newline at end of file
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<schema targetNamespace="$xsd_namespace"
- xmlns="http://www.w3.org/2001/XMLSchema"
- xmlns:bind10="$xsd_namespace">
- <annotation>
- <documentation>XML schema of statistics data in BIND 10</documentation>
- </annotation>
- <element name="statistics">
- <annotation><documentation>A set of statistics data</documentation></annotation>
- <complexType>
- <sequence>
- <element name="item" maxOccurs="unbounded" minOccurs="1">
- <complexType>
- <attribute name="identifier" type="string" use="required">
- <annotation>
- <appinfo>Identifier</appinfo>
- <documentation>Identifier of item</documentation>
- </annotation>
- </attribute>
- <attribute name="value" type="string" use="optional">
- <annotation>
- <appinfo>Value</appinfo>
- <documentation>Value of item</documentation>
- </annotation>
- </attribute>
- <attribute name="owner" type="string" use="required">
- <annotation>
- <appinfo>Owner</appinfo>
- <documentation>Owner module name</documentation>
- </annotation>
- </attribute>
- <attribute name="uri" type="anyURI" use="required">
- <annotation>
- <appinfo>URI</appinfo>
- <documentation>URI of item</documentation>
- </annotation>
- </attribute>
- <attribute name="name" type="string" use="required">
- <annotation>
- <appinfo>Name</appinfo>
- <documentation>Name of item</documentation>
- </annotation>
- </attribute>
- <attribute name="type" use="required">
- <annotation>
- <appinfo>Type</appinfo>
- <documentation>Type of item</documentation>
- </annotation>
- <simpleType>
- <restriction base="token">
- <enumeration value="boolean"/>
- <enumeration value="integer"/>
- <enumeration value="real"/>
- <enumeration value="string"/>
- <enumeration value="map"/>
- <enumeration value="list"/>
- <enumeration value="named_set"/>
- <enumeration value="any"/>
- </restriction>
- </simpleType>
- </attribute>
- <attribute name="description" type="string" use="optional">
- <annotation>
- <appinfo>Description</appinfo>
- <documentation>Description of item</documentation>
- </annotation>
- </attribute>
- <attribute name="title" type="string" use="optional">
- <annotation>
- <appinfo>Title</appinfo>
- <documentation>Title of item</documentation>
- </annotation>
- </attribute>
- <attribute name="optional" type="boolean" use="optional">
- <annotation>
- <appinfo>Optional</appinfo>
- <documentation>The item is optional or not</documentation>
- </annotation>
- </attribute>
- <attribute name="default" type="string" use="optional">
- <annotation>
- <appinfo>Default</appinfo>
- <documentation>Default of item</documentation>
- </annotation>
- </attribute>
- <attribute name="format" type="string" use="optional">
- <annotation>
- <appinfo>Format</appinfo>
- <documentation>Format of item value</documentation>
- </annotation>
- </attribute>
- </complexType>
- </element>
- </sequence>
- </complexType>
- </element>
-</schema>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<xsl:stylesheet version="1.0"
- xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml"
- xmlns:bind10="$xsd_namespace">
- <xsl:output method="html" encoding="UTF-8"
- doctype-public="-//W3C//DTD HTML 4.01 Transitional//EN"
- doctype-system=" http://www.w3.org/TR/html4/loose.dtd " />
- <xsl:template match="/">
- <html lang="en">
- <head>
- <title>BIND 10 Statistics</title>
- <style type="text/css"><![CDATA[
-table {
- border: 1px #000000 solid;
- border-collapse: collapse;
-}
-td, th {
- padding: 3px 20px;
- border: 1px #000000 solid;
-}
-td.title {
- text-decoration:underline;
-}
-]]>
- </style>
- </head>
- <body>
- <h1>BIND 10 Statistics</h1>
- <xsl:apply-templates />
- </body>
- </html>
- </xsl:template>
- <xsl:template match="bind10:statistics">
- <table>
- <tr>
- <th>Identifier</th><th>Value</th><th>Description</th>
- </tr>
- <xsl:for-each select="item">
- <tr>
- <td>
- <xsl:element name="a">
- <xsl:attribute name="href"><xsl:value-of select="@uri" /></xsl:attribute>
- <xsl:value-of select="@identifier" />
- </xsl:element>
- </td>
- <td>
- <xsl:if test="@value"><xsl:value-of select="@value" /></xsl:if>
- </td>
- <td>
- <xsl:if test="@description"><xsl:value-of select="@description" /></xsl:if>
- </td>
- </tr>
- </xsl:for-each>
- </table>
- </xsl:template>
-</xsl:stylesheet>
+++ /dev/null
-{
- "module_spec": {
- "module_name": "StatsHttpd",
- "module_description": "Stats HTTP daemon",
- "config_data": [
- {
- "item_name": "listen_on",
- "item_type": "list",
- "item_optional": false,
- "item_default": [
- {
- "address": "127.0.0.1",
- "port": 8000
- }
- ],
- "list_item_spec": {
- "item_name": "address",
- "item_type": "map",
- "item_optional": false,
- "item_default": {},
- "map_item_spec": [
- {
- "item_name": "address",
- "item_type": "string",
- "item_optional": true,
- "item_default": "127.0.0.1",
- "item_description": "listen-on address for HTTP"
- },
- {
- "item_name": "port",
- "item_type": "integer",
- "item_optional": true,
- "item_default": 8000,
- "item_description": "listen-on port for HTTP"
- }
- ]
- },
- "item_description": "http listen-on address and port"
- }
- ],
- "commands": [
- {
- "command_name": "status",
- "command_description": "Status of the stats httpd",
- "command_args": []
- },
- {
- "command_name": "shutdown",
- "command_description": "Shut down the stats httpd",
- "command_args": [
- {
- "item_name": "pid",
- "item_type": "integer",
- "item_optional": true
- }
- ]
- }
- ]
- }
-}
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2010-2013 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-Statistics daemon in BIND 10
-
-"""
-import sys; sys.path.append ('@@PYTHONPATH@@')
-import os
-from time import time, strftime, gmtime
-from optparse import OptionParser, OptionValueError
-import errno
-import select
-
-import isc.cc
-import isc.config
-import isc.util.process
-import isc.util.traceback_handler
-import isc.log
-from isc.log_messages.stats_messages import *
-
-isc.log.init("b10-stats", buffer=True)
-logger = isc.log.Logger("stats")
-
-# Some constants for debug levels.
-DBG_STATS_MESSAGING = logger.DBGLVL_COMMAND
-
-# This is for boot_time of Stats
-_BASETIME = gmtime()
-
-# for setproctitle
-isc.util.process.rename()
-
-# If B10_FROM_SOURCE is set in the environment, we use data files
-# from a directory relative to that, otherwise we use the ones
-# installed on the system
-if "B10_FROM_SOURCE" in os.environ:
- SPECFILE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
- "src" + os.sep + "bin" + os.sep + "stats" + os.sep + "stats.spec"
-else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- SPECFILE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@" + os.sep + "stats.spec"
- SPECFILE_LOCATION = SPECFILE_LOCATION.replace("${datarootdir}", DATAROOTDIR)\
- .replace("${prefix}", PREFIX)
-
-def get_timestamp():
- """
- get current timestamp
- """
- return time()
-
-def get_datetime(gmt=None):
- """
- get current datetime
- """
- if not gmt: gmt = gmtime()
- return strftime("%Y-%m-%dT%H:%M:%SZ", gmt)
-
-def get_spec_defaults(spec):
- """
- extracts the default values of the items from spec specified in
- arg, and returns the dict-type variable which is a set of the item
- names and the default values
- """
- if type(spec) is not list: return {}
- def _get_spec_defaults(spec):
- item_type = spec['item_type']
- if item_type == "integer":
- return int(spec.get('item_default', 0))
- elif item_type == "real":
- return float(spec.get('item_default', 0.0))
- elif item_type == "boolean":
- return bool(spec.get('item_default', False))
- elif item_type == "string":
- return str(spec.get('item_default', ""))
- elif item_type == "list":
- return spec.get(
- "item_default",
- [ _get_spec_defaults(spec["list_item_spec"]) ])
- elif item_type == "map":
- return spec.get(
- "item_default",
- dict([ (s["item_name"], _get_spec_defaults(s)) for s in spec["map_item_spec"] ]) )
- elif item_type == "named_set":
- # in a named_set type, it returns {} as a default value
- return spec.get("item_default", {})
- else:
- return spec.get("item_default", None)
- return dict([ (s['item_name'], _get_spec_defaults(s)) for s in spec ])
-
-def _accum(a, b):
- """If the first arg is dict or list type, two values
- would be merged and accumlated. This is for internal use."""
-
- # If both of args are dict or list type, two
- # values are merged.
- if type(a) is dict and type(b) is dict:
- return dict([ (k, _accum(v, b[k])) \
- if k in b else (k, v) \
- for (k, v) in a.items() ] \
- + [ (k, v) \
- for (k, v) in b.items() \
- if k not in a ])
- elif type(a) is list and type(b) is list:
- return [ _accum(a[i], b[i]) \
- if len(b) > i else a[i] \
- for i in range(len(a)) ] \
- + [ b[i] \
- for i in range(len(b)) \
- if len(a) <= i ]
- # If both of args are integer or float type, two
- # values are added.
- elif (type(a) is int or type(a) is float) \
- and (type(b) is int or type(b) is float):
- return a + b
-
- # If both of args are string type,
- # values are compared and bigger one is returned.
- elif type(a) is str and type(b) is str:
- if a < b: return b
- return a
-
- # If the first arg is None type, the second value is returned.
- elif a is None:
- return b
-
- # Nothing matches above, the first arg is returned
- return a
-
-def merge_oldnew(old, new):
- """
- Merges two arguments. If old data contains the corresponding name
- against new data, the value of the name is replaced with the
- corresponding value in new data. Otherwise, the new date are added
- at same name or same id. Both old data and new data should be same
- data type. This method returns the merged result.
- """
- # If the first arg is dict or list type, two values
- # would be merged
- if type(old) is dict and type(new) is dict:
- return dict([ (k, merge_oldnew(old[k], v)) \
- if k in old else (k, v) \
- for (k, v) in new.items() ] \
- + [ (k, v) \
- for (k, v) in old.items() \
- if k not in new ])
- elif type(old) is list and type(new) is list:
- return [ merge_oldnew(old[i], new[i]) \
- if len(old) > i else new[i] \
- for i in range(len(new)) ] \
- + [ old[i] \
- for i in range(len(old)) \
- if len(new) <= i ]
- else:
- return new
-
-class Callback():
- """
- A Callback handler class
- """
- def __init__(self, command=None, args=(), kwargs={}):
- self.command = command
- self.args = args
- self.kwargs = kwargs
-
- def __call__(self, *args, **kwargs):
- if not args: args = self.args
- if not kwargs: kwargs = self.kwargs
- if self.command: return self.command(*args, **kwargs)
-
-class StatsError(Exception):
- """Exception class for Stats class"""
- pass
-
-class InitSessionTimeout(isc.cc.session.SessionTimeout):
- """SessionTimeout Exception in the session between the stats module and the
- init module"""
- pass
-
-class Stats:
- """
- Main class of stats module
- """
- def __init__(self, module_ccsession_class=isc.config.ModuleCCSession):
- '''Constructor
-
- module_ccsession_class is parameterized so that test can specify
- a mocked class to test the behavior without involving network I/O.
- In other cases this parameter shouldn't be specified.
-
- '''
- self.running = False
- # create ModuleCCSession object
- self.mccs = module_ccsession_class(SPECFILE_LOCATION,
- self.config_handler,
- self.command_handler)
- self.cc_session = self.mccs._session
- # get module spec
- self.module_name = self.mccs.get_module_spec().get_module_name()
- self.modules = {}
- self.statistics_data = {}
- # statistics data by each mid
- self.statistics_data_bymid = {}
- # get commands spec
- self.commands_spec = self.mccs.get_module_spec().get_commands_spec()
- # add event handler related command_handler of ModuleCCSession
- self.callbacks = {}
- for cmd in self.commands_spec:
- # add prefix "command_"
- name = "command_" + cmd["command_name"]
- try:
- callback = getattr(self, name)
- kwargs = get_spec_defaults(cmd["command_args"])
- self.callbacks[name] = Callback(command=callback, kwargs=kwargs)
- except AttributeError:
- raise StatsError(STATS_UNKNOWN_COMMAND_IN_SPEC, cmd["command_name"])
- self.config = {}
- self.mccs.start()
- # setup my config
- self.config = dict([
- (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
- for itm in self.mccs.get_module_spec().get_config_spec()
- ])
- # set a absolute timestamp polling at next time
- self.next_polltime = get_timestamp() + self.get_interval()
-
- self._init_statistics_data()
-
- def _init_statistics_data(self):
- """initialized Statistics data.
-
- This method is a dedicated subroutine of __int__(), but extracted
- so tests can override it to avoid blocking network operation.
-
- """
- self.update_modules()
- self.update_statistics_data(
- self.module_name,
- self.cc_session.lname,
- {'lname': self.cc_session.lname,
- 'boot_time': get_datetime(_BASETIME),
- 'last_update_time': get_datetime()})
- # define the variable of the last time of polling
- self._lasttime_poll = 0.0
-
- def get_interval(self):
- """return the current value of 'poll-interval'"""
- return self.config['poll-interval']
-
- def _get_multi_module_list(self):
- """Returns a module list which is running as multiple modules."""
- # It counts the number of instances of same module by
- # examining the third value from the array result of
- # 'show_processes' of Init
- try:
- value = self.mccs.rpc_call('show_processes', 'Init')
- except isc.config.RPCRecipientMissing:
- # This has been SessionTimeout before, so we keep the original
- # behavior.
- raise
- except isc.config.RPCError:
- # TODO: Is it OK to just pass? As part of refactoring, preserving
- # the original behaviour.
- value = None
- except isc.cc.session.SessionTimeout as e:
- raise InitSessionTimeout(e)
- modules = []
- if type(value) is list:
- # NOTE: For example, the "show_processes" command
- # of Init is assumed to return the response in this
- # format:
- # [
- # ...
- # [
- # 20061,
- # "b10-auth",
- # "Auth"
- # ],
- # [
- # 20103,
- # "b10-auth-2",
- # "Auth"
- # ]
- # ...
- # ]
- # If multiple instances of the same module are
- # running, the address names of them, which are at the
- # third element, must be also same. Thus, the value of
- # the third element of each outer element is read here
- # for counting multiple instances. This is a
- # workaround for counting the instances. This should
- # be fixed in another proper way in the future
- # release.
- modules = [ v[2] if type(v) is list and len(v) > 2 \
- else None for v in value ]
- return modules
-
- def _query_statistics(self, modules):
- """Queries each module statistics and returns sequences to use
- for receiving that answers based on the argument 'modules' which
- is a list of modules running as multiple modules"""
- # start requesting each module to collect statistics data
- sequences = []
- for (module_name, data) in self.get_statistics_data().items():
- # skip if module_name is 'Stats'
- if module_name == self.module_name:
- continue
- logger.debug(DBG_STATS_MESSAGING, STATS_SEND_STATISTICS_REQUEST,
- module_name)
- cmd = isc.config.ccsession.create_command(
- "getstats", None) # no argument
- # Not using rpc_call here. We first send a bunch of commands, then
- # collect all the answers. This eliminates some of the round-trip
- # times. Unfortunately, rpc_call is not flexible enough to allow
- # this, though the future rpc_call_async could.
- seq = self.cc_session.group_sendmsg(cmd, module_name,
- want_answer=True)
- sequences.append((module_name, seq))
- cnt = modules.count(module_name)
- if cnt > 1:
- sequences = sequences + [ (module_name, seq) \
- for i in range(cnt-1) ]
- return sequences
-
- def _collect_statistics(self, sequences):
- """Based on sequences which is a list of values returned from
- group_sendmsg(), collects statistics data from each module. If
- a SessionTimeout exception is raised when collecting from the
- module, skips it and goes to collect from the next module."""
- # start receiving statistics data
- _statistics_data = []
- for (module_name, seq) in sequences:
- try:
- answer, env = self.cc_session.group_recvmsg(False, seq)
- if answer:
- rcode, args = isc.config.ccsession.parse_answer(answer)
- if rcode == 0:
- _statistics_data.append(
- (module_name, env['from'], args))
- # skip this module if SessionTimeout raised
- except isc.cc.session.SessionTimeout:
- logger.warn(STATS_SKIP_COLLECTING, module_name)
- return _statistics_data
-
- def _refresh_statistics(self, statistics_data):
- """Refreshes statistics_data into internal statistics data to
- display, which is a list of a tuple of module_name, lname, and
- args"""
- # update statistics data
- _statistics_data = statistics_data[:]
- self.update_modules()
- while len(_statistics_data) > 0:
- (_module_name, _lname, _args) = _statistics_data.pop(0)
- if not self.update_statistics_data(_module_name, _lname, _args):
- self.update_statistics_data(
- self.module_name,
- self.cc_session.lname,
- {'last_update_time': get_datetime()})
-
- def do_polling(self):
- """Polls modules for statistics data. Return nothing. First
- search multiple instances of same module. Second requests
- each module to invoke 'getstats'. Finally updates internal
- statistics data every time it gets from each instance."""
- try:
- modules = self._get_multi_module_list()
- sequences = self._query_statistics(modules)
- _statistics_data = self._collect_statistics(sequences)
- self._refresh_statistics(_statistics_data)
- except InitSessionTimeout:
- logger.warn(STATS_SKIP_POLLING)
- # if successfully done or skipped, set the last time of polling
- self._lasttime_poll = get_timestamp()
-
- def _check_command(self, nonblock=False):
- """check invoked command by waiting for 'poll-interval' seconds
-
- This is a dedicated subroutine of start(), but extracted and defined
- as a 'protected' method so that tests can replace it.
-
- """
- # backup original timeout
- orig_timeout = self.cc_session.get_timeout()
- # set cc-session timeout to half of a second(500ms)
- self.cc_session.set_timeout(500)
- try:
- answer, env = self.cc_session.group_recvmsg(nonblock)
- self.mccs.check_command_without_recvmsg(answer, env)
- except isc.cc.session.SessionTimeout:
- pass # waited for poll-interval seconds
- # restore timeout
- self.cc_session.set_timeout(orig_timeout)
-
- def start(self):
- """
- Start stats module
- """
- logger.info(STATS_STARTING)
-
- try:
- self.running = True
- while self.running:
- self._check_command()
- now = get_timestamp()
- intval = self.get_interval()
- if intval > 0 and now >= self.next_polltime:
- # decide the next polling timestamp
- self.next_polltime += intval
- # adjust next time
- if self.next_polltime < now:
- self.next_polltime = now
- self.do_polling()
- finally:
- self.mccs.send_stopping()
-
- def config_handler(self, new_config):
- """
- handle a configure from the cc channel
- """
- logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_NEW_CONFIG,
- new_config)
- errors = []
- if not self.mccs.get_module_spec().\
- validate_config(False, new_config, errors):
- return isc.config.ccsession.create_answer(
- 1, ", ".join(errors))
-
- if 'poll-interval' in new_config \
- and new_config['poll-interval'] < 0:
- return isc.config.ccsession.create_answer(
- 1, "Negative integer ignored")
-
- self.config.update(new_config)
- if 'poll-interval' in self.config:
- # update next polling timestamp
- self.next_polltime = get_timestamp() + self.get_interval()
- return isc.config.create_answer(0)
-
- def command_handler(self, command, kwargs):
- """
- handle commands from the cc channel
- """
- name = 'command_' + command
- if name in self.callbacks:
- callback = self.callbacks[name]
- if kwargs:
- return callback(**kwargs)
- else:
- return callback()
- else:
- logger.error(STATS_RECEIVED_UNKNOWN_COMMAND, command)
- return isc.config.create_answer(1, "Unknown command: '"+str(command)+"'")
-
- def update_modules(self):
- """
- updates information of each module. This method gets each
- module's information from the config manager and sets it into
- self.modules. If its getting from the config manager fails, it
- raises StatsError.
- """
- modules = {}
- try:
- value = self.mccs.rpc_call(isc.config.ccsession. \
- COMMAND_GET_STATISTICS_SPEC,
- 'ConfigManager')
- except isc.config.RPCError as e:
- raise StatsError("Updating module spec fails: " + str(e))
- for mod in value:
- spec = { "module_name" : mod }
- if value[mod] and type(value[mod]) is list:
- spec["statistics"] = value[mod]
- modules[mod] = isc.config.module_spec.ModuleSpec(spec)
- modules[self.module_name] = self.mccs.get_module_spec()
- self.modules = modules
-
- def get_statistics_data(self, owner=None, name=None):
- """
- returns statistics data which stats module has of each
- module. If it can't find specified statistics data, it raises
- StatsError.
- """
- self.update_modules()
- if owner and name:
- try:
- return {owner:{name:self.statistics_data[owner][name]}}
- except KeyError:
- pass
- elif owner:
- try:
- return {owner: self.statistics_data[owner]}
- except KeyError:
- pass
- elif name:
- pass
- else:
- return self.statistics_data
- raise StatsError("No statistics data found: "
- + "owner: " + str(owner) + ", "
- + "name: " + str(name))
-
- def update_statistics_data(self, owner=None, mid=None, data=None):
- """
- change statistics data of specified module into specified
- data. It updates information of each module first, and it
- updates statistics data. If specified data is invalid for
- statistics spec of specified owner, it returns a list of error
- messages. If there is no error or if neither owner nor data is
- specified in args, it returns None. The 'mid' argument is an
- identifier of the sender module in order for stats to identify which
- instance sends statistics data in the situation that multiple
- instances are working.
- """
- # Note:
- # The fix of #1751 is for multiple instances working. It is
- # assumed here that they send different statistics data with
- # each sender module id (mid). Stats should save their statistics data
- # by mid. The statistics data, which is the existing variable, is
- # preserved by accumlating from statistics data by the mid. This
- # is an ad-hoc fix because administrators can not see
- # statistics by each instance via bindctl or HTTP/XML. These
- # interfaces aren't changed in this fix.
-
- def _accum_bymodule(statistics_data_bymid):
- """This is an internal method for the superordinate
- method. It accumulates statistics data of each module id
- by module. It returns a accumulated result."""
- # FIXME: A issue might happen when consolidating
- # statistics of the multiple instances. If they have
- # different statistics data which are not for adding each
- # other, this might happen: If these are integer or float,
- # these are added each other. If these are string , these
- # are compared and consolidated into bigger one. If one
- # of them is None type , these might be consolidated
- # into not None-type one. Otherwise these are overwritten
- # into one of them.
- ret = {}
- for data in statistics_data_bymid.values():
- ret.update(_accum(data, ret))
- return ret
-
- # Firstly, it gets default statistics data in each spec file.
- statistics_data = {}
- for (name, module) in self.modules.items():
- value = get_spec_defaults(module.get_statistics_spec())
- if module.validate_statistics(True, value):
- statistics_data[name] = value
- self.statistics_data = statistics_data
-
- # If the "owner" and "data" arguments in this function are
- # specified, then the variable of statistics data of each module id
- # would be updated.
- errors = []
- if owner and data:
- _data = self.statistics_data_bymid.copy()
- try:
- for (_key, _val) in data.items():
- if self.modules[owner].validate_statistics(
- False, {_key: _val}, errors):
- if owner not in _data:
- _data[owner] = { mid: { _key: _val } }
- elif mid not in _data[owner]:
- _data[owner][mid] = { _key: _val }
- else:
- # merge recursively old value and new
- # value each other
- _data[owner][mid] = \
- merge_oldnew(_data[owner][mid], {_key: _val})
- continue
- # the key string might be a "xx/yy/zz[0]"
- # type. try it.
- if _key.find('/') >= 0 or \
- isc.cc.data.identifier_has_list_index(_key):
- # remove the last error
- if errors: errors.pop()
- # try updata and check validation in adavance
- __data = _data.copy()
- if owner not in __data:
- __data[owner] = {}
- if mid not in __data[owner]:
- __data[owner][mid] = {}
- # use the isc.cc.data.set method
- try:
- isc.cc.data.set(__data[owner][mid], _key, _val)
- if self.modules[owner].validate_statistics(
- False, __data[owner][mid], errors):
- _data = __data
- except Exception as e:
- errors.append("%s: %s" % (e.__class__.__name__, e))
- except KeyError:
- errors.append("unknown module name: " + owner)
- if not errors:
- self.statistics_data_bymid = _data
-
- # Just consolidate statistics data of each module without
- # removing that of modules which have been already dead
- mlist = [ k for k in self.statistics_data_bymid.keys() ]
- for m in mlist:
- if self.statistics_data_bymid[m]:
- if m in self.statistics_data:
- # propagate the default values by times of
- # instances
- _len = len(self.statistics_data_bymid[m])
- for i in range(0, _len - 1):
- self.statistics_data[m] = _accum(
- self.statistics_data[m],
- self.statistics_data[m])
- # replace the default values with summaries of the
- # collected values of each module. But the default
- # values which are not included in collected
- # values are not replaced.
- self.statistics_data[m] = merge_oldnew(
- self.statistics_data[m],
- _accum_bymodule(self.statistics_data_bymid[m]))
-
- if errors:
- logger.warn(
- STATS_RECEIVED_INVALID_STATISTICS_DATA, owner)
- return errors
-
- def command_status(self):
- """
- handle status command
- """
- logger.debug(DBG_STATS_MESSAGING, STATS_RECEIVED_STATUS_COMMAND)
- return isc.config.create_answer(
- 0, "Stats is up. (PID " + str(os.getpid()) + ")")
-
- def command_shutdown(self, pid=None):
- """
- handle shutdown command
-
- The pid argument is ignored, it is here to match the signature.
- """
- logger.info(STATS_RECEIVED_SHUTDOWN_COMMAND)
- self.running = False
- return isc.config.create_answer(0)
-
- def command_show(self, owner=None, name=None):
- """
- handle show command
- """
- # decide if polling should be done based on the the last time of
- # polling. If more than one second has passed since the last
- # request to each module, the stats module requests each module
- # statistics data and then shows the latest result. Otherwise,
- # the stats module just shows statistics data which it has.
- if get_timestamp() - self._lasttime_poll > 1.0:
- self.do_polling()
- if owner or name:
- logger.debug(DBG_STATS_MESSAGING,
- STATS_RECEIVED_SHOW_NAME_COMMAND,
- str(owner)+", "+str(name))
- else:
- logger.debug(DBG_STATS_MESSAGING,
- STATS_RECEIVED_SHOW_ALL_COMMAND)
- errors = self.update_statistics_data(
- self.module_name,
- self.cc_session.lname,
- {'timestamp': get_timestamp(),
- 'report_time': get_datetime()}
- )
- if errors:
- raise StatsError("stats spec file is incorrect: "
- + ", ".join(errors))
- try:
- return isc.config.create_answer(
- 0, self.get_statistics_data(owner, name))
- except StatsError:
- return isc.config.create_answer(
- 1, "specified arguments are incorrect: " \
- + "owner: " + str(owner) + ", name: " + str(name))
-
- def command_showschema(self, owner=None, name=None):
- """
- handle show command
- """
- if owner or name:
- logger.debug(DBG_STATS_MESSAGING,
- STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND,
- str(owner)+", "+str(name))
- else:
- logger.debug(DBG_STATS_MESSAGING,
- STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND)
- self.update_modules()
- schema = {}
- schema_byname = {}
- for mod in self.modules:
- spec = self.modules[mod].get_statistics_spec()
- schema_byname[mod] = {}
- if spec:
- schema[mod] = spec
- for item in spec:
- schema_byname[mod][item['item_name']] = item
- if owner:
- try:
- if name:
- return isc.config.create_answer(0, {owner:[schema_byname[owner][name]]})
- else:
- return isc.config.create_answer(0, {owner:schema[owner]})
- except KeyError:
- pass
- else:
- if name:
- return isc.config.create_answer(1, "module name is not specified")
- else:
- return isc.config.create_answer(0, schema)
- return isc.config.create_answer(
- 1, "specified arguments are incorrect: " \
- + "owner: " + str(owner) + ", name: " + str(name))
-
-def main():
- try:
- parser = OptionParser()
- parser.add_option(
- "-v", "--verbose", dest="verbose", action="store_true",
- help="enable maximum debug logging")
- (options, args) = parser.parse_args()
- if options.verbose:
- isc.log.init("b10-stats", "DEBUG", 99, buffer=True)
- stats = Stats()
- stats.start()
- except OptionValueError as ove:
- logger.fatal(STATS_BAD_OPTION_VALUE, ove)
- sys.exit(1)
- except isc.cc.session.SessionError as se:
- logger.fatal(STATS_CC_SESSION_ERROR, se)
- sys.exit(1)
- except StatsError as se:
- logger.fatal(STATS_START_ERROR, se)
- sys.exit(1)
- except KeyboardInterrupt as kie:
- logger.info(STATS_STOPPED_BY_KEYBOARD)
-
- logger.info(STATS_EXITING)
-
-if __name__ == "__main__":
- isc.util.traceback_handler.traceback_handler(main)
+++ /dev/null
-{
- "module_spec": {
- "module_name": "Stats",
- "module_description": "Stats daemon",
- "config_data": [
- { "item_name": "poll-interval",
- "item_type": "integer",
- "item_optional": true,
- "item_default": 60
- }
- ],
- "commands": [
- {
- "command_name": "status",
- "command_description": "Show status of the stats daemon",
- "command_args": []
- },
- {
- "command_name": "shutdown",
- "command_description": "Shut down the stats module",
- "command_args": [
- {
- "item_name": "pid",
- "item_type": "integer",
- "item_optional": true
- }
- ]
- },
- {
- "command_name": "show",
- "command_description": "Show the specified/all statistics data",
- "command_args": [
- {
- "item_name": "owner",
- "item_type": "string",
- "item_optional": true,
- "item_default": "",
- "item_description": "module name of the owner of the statistics data"
- },
- {
- "item_name": "name",
- "item_type": "string",
- "item_optional": true,
- "item_default": "",
- "item_description": "statistics item name of the owner"
- }
- ]
- },
- {
- "command_name": "showschema",
- "command_description": "show the specified/all statistics shema",
- "command_args": [
- {
- "item_name": "owner",
- "item_type": "string",
- "item_optional": true,
- "item_default": "",
- "item_description": "module name of the owner of the statistics data"
- },
- {
- "item_name": "name",
- "item_type": "string",
- "item_optional": true,
- "item_default": "",
- "item_description": "statistics item name of the owner"
- }
- ]
- }
- ],
- "statistics": [
- {
- "item_name": "report_time",
- "item_type": "string",
- "item_optional": false,
- "item_default": "1970-01-01T00:00:00Z",
- "item_title": "Report time",
- "item_description": "A date time when stats module reports",
- "item_format": "date-time"
- },
- {
- "item_name": "boot_time",
- "item_type": "string",
- "item_optional": false,
- "item_default": "1970-01-01T00:00:00Z",
- "item_title": "Boot time",
- "item_description": "A date time when the stats module starts initially or when the stats module restarts",
- "item_format": "date-time"
- },
- {
- "item_name": "last_update_time",
- "item_type": "string",
- "item_optional": false,
- "item_default": "1970-01-01T00:00:00Z",
- "item_title": "Last update time",
- "item_description": "The latest date time when the stats module receives from other modules like auth server or b10-init process and so on",
- "item_format": "date-time"
- },
- {
- "item_name": "timestamp",
- "item_type": "real",
- "item_optional": false,
- "item_default": 0.0,
- "item_title": "Timestamp",
- "item_description": "A current time stamp since epoch time (1970-01-01T00:00:00Z)"
- },
- {
- "item_name": "lname",
- "item_type": "string",
- "item_optional": false,
- "item_default": "",
- "item_title": "Local Name",
- "item_description": "A localname of stats module given via CC protocol"
- }
- ]
- }
-}
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2011-2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-A standalone HTTP server for HTTP/XML interface of statistics in BIND 10
-
-"""
-import sys; sys.path.append ('@@PYTHONPATH@@')
-import os
-import time
-import errno
-import select
-from optparse import OptionParser, OptionValueError
-import http.server
-import socket
-import string
-import xml.etree.ElementTree
-import urllib.parse
-import re
-
-import isc.cc
-import isc.config
-import isc.util.process
-import isc.util.traceback_handler
-from isc.util.address_formatter import AddressFormatter
-
-import isc.log
-from isc.log_messages.stats_httpd_messages import *
-
-isc.log.init("b10-stats-httpd", buffer=True)
-logger = isc.log.Logger("stats-httpd")
-
-# Some constants for debug levels.
-DBG_STATSHTTPD_INIT = logger.DBGLVL_START_SHUT
-DBG_STATSHTTPD_MESSAGING = logger.DBGLVL_COMMAND
-
-# If B10_FROM_SOURCE is set in the environment, we use data files
-# from a directory relative to that, otherwise we use the ones
-# installed on the system
-if "B10_FROM_SOURCE" in os.environ:
- BASE_LOCATION = os.environ["B10_FROM_SOURCE"] + os.sep + \
- "src" + os.sep + "bin" + os.sep + "stats"
-else:
- PREFIX = "@prefix@"
- DATAROOTDIR = "@datarootdir@"
- BASE_LOCATION = "@datadir@" + os.sep + "@PACKAGE@"
- BASE_LOCATION = BASE_LOCATION.replace("${datarootdir}", DATAROOTDIR).replace("${prefix}", PREFIX)
-SPECFILE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd.spec"
-XML_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xml.tpl"
-XSD_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsd.tpl"
-XSL_TEMPLATE_LOCATION = BASE_LOCATION + os.sep + "stats-httpd-xsl.tpl"
-
-# These variables are paths part of URL.
-# eg. "http://${address}" + XXX_URL_PATH
-XML_URL_PATH = '/bind10/statistics/xml'
-XSD_URL_PATH = '/bind10/statistics.xsd'
-XSL_URL_PATH = '/bind10/statistics.xsl'
-# TODO: This should be considered later.
-XSD_NAMESPACE = 'http://bind10.isc.org/bind10'
-XMLNS_XSI = "http://www.w3.org/2001/XMLSchema-instance"
-
-# constant parameter of XML
-XML_ROOT_ELEMENT = 'bind10:statistics'
-XML_ROOT_ATTRIB = { 'xsi:schemaLocation' : '%s %s' % (XSD_NAMESPACE, XSD_URL_PATH),
- 'xmlns:bind10' : XSD_NAMESPACE,
- 'xmlns:xsi' : XMLNS_XSI }
-
-# Assign this process name
-isc.util.process.rename()
-
-def item_name_list(element, identifier):
- """Return a list of strings. The strings are string expression of
- the first argument element which is dict type. The second argument
- identifier is a string for specifying the strings which are
- returned from this method as a list. For example, if we specify as
-
- item_name_list({'a': {'aa': [0, 1]}, 'b': [0, 1]}, 'a/aa'),
-
- then it returns
-
- ['a/aa', 'a/aa[0]', 'a/aa[1]'].
-
- If an empty string is specified in the second argument, all
- possible strings are returned as a list. In that example if we
- specify an empty string in the second argument, then it returns
-
- ['a', 'a/aa', 'a/aa[0]', 'a/aa[1]', 'b', 'b[0]', 'b[1]'].
-
- The key name of element which is in the first argument is sorted.
- Even if we specify as
-
- item_name_list({'xx': 0, 'a': 1, 'x': 2}, ''),
-
- then it returns
-
- ['a', 'x', 'xx'].
-
- This method internally invokes isc.cc.data.find(). The arguments
- of this method are passed to isc.cc.data.find(). So an exception
- DataNotFoundError or DataTypeError might be raised via
- isc.cc.data.find() depending on the arguments. See details of
- isc.cc.data.find() for more information about exceptions"""
- elem = isc.cc.data.find(element, identifier)
- ret = []
- ident = identifier
- if ident:
- ret.append(ident)
- if type(elem) is dict:
- if ident:
- ident = ident + '/'
- for key in sorted(elem.keys()):
- idstr = '%s%s' % (ident, key)
- ret += item_name_list(element, idstr)
- elif type(elem) is list:
- for i in range(0, len(elem)):
- idstr = '%s[%d]' % (ident, i)
- ret += item_name_list(element, idstr)
- return ret
-
-class HttpHandler(http.server.BaseHTTPRequestHandler):
- """HTTP handler class for HttpServer class. The class inherits the super
- class http.server.BaseHTTPRequestHandler. It implements do_GET()
- and do_HEAD() and overrides log_message()"""
- def do_GET(self):
- body = self.send_head()
- if body is not None:
- self.wfile.write(body.encode())
-
- def do_HEAD(self):
- self.send_head()
-
- def send_head(self):
- try:
- req_path = self.path
- req_path = urllib.parse.urlsplit(req_path).path
- req_path = urllib.parse.unquote(req_path)
- body = None
- # In case that the requested path (req_path),
- # e.g. /bind10/statistics/Auth/, is started with
- # XML_URL_PATH + '/'
- if req_path.find('%s/' % XML_URL_PATH) == 0:
- # remove './' from the path if there is
- req_path = os.path.normpath(req_path)
- # get the strings tailing after XML_URL_PATH
- req_path = req_path.lstrip('%s/' % XML_URL_PATH)
- # remove empty dir names from the path if there are
- path_dirs = req_path.split('/')
- path_dirs = [ d for d in filter(None, path_dirs) ]
- req_path = '/'.join(path_dirs)
- # pass the complete requested path,
- # e.g. Auth/queries.upd, to xml_handler()
- body = self.server.xml_handler(req_path)
- # In case that the requested path (req_path) is exactly
- # matched with XSD_URL_PATH
- elif req_path == XSD_URL_PATH:
- body = self.server.xsd_handler()
- # In case that the requested path (req_path) is exactly
- # matched with XSL_URL_PATH
- elif req_path == XSL_URL_PATH:
- body = self.server.xsl_handler()
- else:
- if 'Host' in self.headers.keys() and \
- ( req_path == '/' or req_path == XML_URL_PATH ):
- # redirect to XML URL only when requested with '/' or XML_URL_PATH
- toloc = "http://%s%s/" % (self.headers.get('Host'), XML_URL_PATH)
- self.send_response(302)
- self.send_header("Location", toloc)
- self.end_headers()
- return None
- else:
- # Couldn't find HOST
- self.send_error(404)
- return None
- except StatsHttpdDataError as err:
- # Couldn't find neither specified module name nor
- # specified item name
- self.send_error(404)
- logger.error(STATSHTTPD_SERVER_DATAERROR, err)
- return None
- except Exception as err:
- self.send_error(500)
- logger.error(STATSHTTPD_SERVER_ERROR, err)
- return None
- else:
- self.send_response(200)
- self.send_header("Content-type", "text/xml")
- self.send_header("Content-Length", len(body))
- self.send_header("Last-Modified", self.date_time_string(time.time()))
- self.end_headers()
- return body
-
- def log_message(self, format, *args):
- """overrides the parent method log_message()
- to use the bind10 logging framework.
- """
- logger.debug(DBG_STATSHTTPD_MESSAGING, STATSHTTPD_HTTPLOG,
- self.address_string(),
- format%args)
-
-class HttpServerError(Exception):
- """Exception class for HttpServer class. It is intended to be
- passed from the HttpServer object to the StatsHttpd object."""
- pass
-
-class HttpServer(http.server.HTTPServer):
- """HTTP Server class. The class inherits the super
- http.server.HTTPServer. Some parameters are specified as
- arguments, which are xml_handler, xsd_handler, xsl_handler, and
- log_writer. These all are parameters which the StatsHttpd object
- has. The handler parameters are references of functions which
- return body of each document. The last parameter log_writer is
- reference of writer function to just write to
- sys.stderr.write. They are intended to be referred by HttpHandler
- object."""
- def __init__(self, server_address, handler,
- xml_handler, xsd_handler, xsl_handler, log_writer):
- self.server_address = server_address
- self.xml_handler = xml_handler
- self.xsd_handler = xsd_handler
- self.xsl_handler = xsl_handler
- self.log_writer = log_writer
- http.server.HTTPServer.__init__(self, server_address, handler)
-
-class StatsHttpdError(Exception):
- """Exception class for StatsHttpd class. It is intended to be
- thrown from the the StatsHttpd object to the HttpHandler object or
- main routine."""
- pass
-
-class StatsHttpdDataError(Exception):
- """Exception class for StatsHttpd class. The reason seems to be
- due to the data. It is intended to be thrown from the the
- StatsHttpd object to the HttpHandler object or main routine."""
- pass
-
-class StatsHttpd:
- """The main class of HTTP server of HTTP/XML interface for
- statistics module. It handles HTTP requests, and command channel
- and config channel CC session. It uses select.select function
- while waiting for clients requests."""
- def __init__(self):
- self.running = False
- self.poll_intval = 0.5
- self.write_log = sys.stderr.write
- self.mccs = None
- self.httpd = []
- self.open_mccs()
- self.config = {}
- self.load_config()
- self.http_addrs = []
- self.mccs.start()
- try:
- self.open_httpd()
- except HttpServerError:
- # if some exception, e.g. address in use, is raised, then it closes mccs and httpd
- self.close_mccs()
- raise
-
- def open_mccs(self):
- """Opens a ModuleCCSession object"""
- # create ModuleCCSession
- logger.debug(DBG_STATSHTTPD_INIT, STATSHTTPD_STARTING_CC_SESSION)
- self.mccs = isc.config.ModuleCCSession(
- SPECFILE_LOCATION, self.config_handler, self.command_handler)
- self.cc_session = self.mccs._session
-
- def close_mccs(self):
- """Closes a ModuleCCSession object"""
- if self.mccs is None:
- return
- self.mccs.send_stopping()
-
- logger.debug(DBG_STATSHTTPD_INIT, STATSHTTPD_CLOSING_CC_SESSION)
- self.mccs.close()
- self.mccs = None
-
- def load_config(self, new_config={}):
- """Loads configuration from spec file or new configuration
- from the config manager"""
- # load config
- if len(self.config) == 0:
- self.config = dict([
- (itm['item_name'], self.mccs.get_value(itm['item_name'])[0])
- for itm in self.mccs.get_module_spec().get_config_spec()
- ])
- self.config.update(new_config)
- # set addresses and ports for HTTP
- addrs = []
- if 'listen_on' in self.config:
- for cf in self.config['listen_on']:
- if 'address' in cf and 'port' in cf:
- addrs.append((cf['address'], cf['port']))
- self.http_addrs = addrs
-
- def open_httpd(self):
- """Opens sockets for HTTP. Iterating each HTTP address to be
- configured in spec file"""
- for addr in self.http_addrs:
- self.httpd.append(self._open_httpd(addr))
-
- def _open_httpd(self, server_address):
- httpd = None
- try:
- # get address family for the server_address before
- # creating HttpServer object. If a specified address is
- # not numerical, gaierror may be thrown.
- address_family = socket.getaddrinfo(
- server_address[0], server_address[1], 0,
- socket.SOCK_STREAM, socket.IPPROTO_TCP, socket.AI_NUMERICHOST
- )[0][0]
- HttpServer.address_family = address_family
- httpd = HttpServer(
- server_address, HttpHandler,
- self.xml_handler, self.xsd_handler, self.xsl_handler,
- self.write_log)
- logger.info(STATSHTTPD_STARTED,
- AddressFormatter(server_address, address_family))
- return httpd
- except (socket.gaierror, socket.error,
- OverflowError, TypeError) as err:
- if httpd:
- httpd.server_close()
- raise HttpServerError(
- "Invalid address %s, port %s: %s: %s" %
- (server_address[0], server_address[1],
- err.__class__.__name__, err))
-
- def close_httpd(self):
- """Closes sockets for HTTP"""
- while len(self.httpd)>0:
- ht = self.httpd.pop()
- logger.info(STATSHTTPD_CLOSING,
- AddressFormatter(ht.server_address))
- ht.server_close()
-
- def start(self):
- """Starts StatsHttpd objects to run. Waiting for client
- requests by using select.select functions"""
- self.running = True
- while self.running:
- try:
- (rfd, wfd, xfd) = select.select(
- self.get_sockets(), [], [], self.poll_intval)
- except select.error as err:
- # select.error exception is caught only in the case of
- # EINTR, or in other cases it is just thrown.
- if err.args[0] == errno.EINTR:
- (rfd, wfd, xfd) = ([], [], [])
- else:
- raise
- # FIXME: This module can handle only one request at a
- # time. If someone sends only part of the request, we block
- # waiting for it until we time out.
- # But it isn't so big issue for administration purposes.
- for fd in rfd + xfd:
- if fd == self.mccs.get_socket():
- self.mccs.check_command(nonblock=False)
- continue
- for ht in self.httpd:
- if fd == ht.socket:
- ht.handle_request()
- break
- self.stop()
-
- def stop(self):
- """Stops the running StatsHttpd objects. Closes CC session and
- HTTP handling sockets"""
- logger.info(STATSHTTPD_SHUTDOWN)
- self.close_httpd()
- self.close_mccs()
- self.running = False
-
- def get_sockets(self):
- """Returns sockets to select.select"""
- sockets = []
- if self.mccs is not None:
- sockets.append(self.mccs.get_socket())
- if len(self.httpd) > 0:
- for ht in self.httpd:
- sockets.append(ht.socket)
- return sockets
-
- def config_handler(self, new_config):
- """Config handler for the ModuleCCSession object. It resets
- addresses and ports to listen HTTP requests on."""
- logger.debug(DBG_STATSHTTPD_MESSAGING, STATSHTTPD_HANDLE_CONFIG,
- new_config)
- errors = []
- if not self.mccs.get_module_spec().\
- validate_config(False, new_config, errors):
- return isc.config.ccsession.create_answer(
- 1, ", ".join(errors))
- # backup old config
- old_config = self.config.copy()
- self.load_config(new_config)
- # If the http sockets aren't opened or
- # if new_config doesn't have 'listen_on', it returns
- if len(self.httpd) == 0 or 'listen_on' not in new_config:
- return isc.config.ccsession.create_answer(0)
- self.close_httpd()
- try:
- self.open_httpd()
- except HttpServerError as err:
- logger.error(STATSHTTPD_SERVER_INIT_ERROR, err)
- # restore old config
- self.load_config(old_config)
- self.open_httpd()
- return isc.config.ccsession.create_answer(1, str(err))
- else:
- return isc.config.ccsession.create_answer(0)
-
- def command_handler(self, command, args):
- """Command handler for the ModuleCCSesson object. It handles
- "status" and "shutdown" commands."""
- if command == "status":
- logger.debug(DBG_STATSHTTPD_MESSAGING,
- STATSHTTPD_RECEIVED_STATUS_COMMAND)
- return isc.config.ccsession.create_answer(
- 0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")")
- elif command == "shutdown":
- logger.debug(DBG_STATSHTTPD_MESSAGING,
- STATSHTTPD_RECEIVED_SHUTDOWN_COMMAND)
- self.running = False
- return isc.config.ccsession.create_answer(0)
- else:
- logger.debug(DBG_STATSHTTPD_MESSAGING,
- STATSHTTPD_RECEIVED_UNKNOWN_COMMAND, command)
- return isc.config.ccsession.create_answer(
- 1, "Unknown command: " + str(command))
-
- def get_stats_data(self, owner=None, name=None):
- """Requests statistics data to the Stats daemon and returns
- the data which obtains from it. The first argument is the
- module name which owns the statistics data, the second
- argument is one name of the statistics items which the the
- module owns. The second argument cannot be specified when the
- first argument is not specified. It returns the statistics
- data of the specified module or item. When the session timeout
- or the session error is occurred, it raises
- StatsHttpdError. When the stats daemon returns none-zero
- value, it raises StatsHttpdDataError."""
- param = {}
- if owner is None and name is None:
- param = None
- if owner is not None:
- param['owner'] = owner
- if name is not None:
- param['name'] = name
- try:
- return self.mccs.rpc_call('show', 'Stats', params=param)
- except (isc.cc.session.SessionTimeout,
- isc.cc.session.SessionError,
- isc.config.RPCRecipientMissing) as err:
- raise StatsHttpdError("%s: %s" %
- (err.__class__.__name__, err))
- except isc.config.RPCError as e:
- raise StatsHttpdDataError("Stats module: %s" % str(e))
-
- def get_stats_spec(self, owner=None, name=None):
- """Requests statistics data to the Stats daemon and returns
- the data which obtains from it. The first argument is the
- module name which owns the statistics data, the second
- argument is one name of the statistics items which the the
- module owns. The second argument cannot be specified when the
- first argument is not specified. It returns the statistics
- specification of the specified module or item. When the
- session timeout or the session error is occurred, it raises
- StatsHttpdError. When the stats daemon returns none-zero
- value, it raises StatsHttpdDataError."""
- param = {}
- if owner is None and name is None:
- param = None
- if owner is not None:
- param['owner'] = owner
- if name is not None:
- param['name'] = name
- try:
- return self.mccs.rpc_call('showschema', 'Stats', params=param)
- except isc.config.RPCError as e:
- raise StatsHttpdDataError("Stats module: %s" % str(e))
- except (isc.cc.session.SessionTimeout,
- isc.cc.session.SessionError) as err:
- raise StatsHttpdError("%s: %s" %
- (err.__class__.__name__, err))
-
-
- def xml_handler(self, path=''):
- """Requests the specified statistics data and specification by
- using the functions get_stats_data and get_stats_spec
- respectively and loads the XML template file and returns the
- string of the XML document.The argument is a path in the
- requested URI. For example, the path is assumed to be like
- ${module_name}/${top_level_item_name}/${second_level_item_name}/..."""
-
- dirs = [ d for d in path.split("/") if len(d) > 0 ]
- module_name = None
- item_name = None
- if len(dirs) > 0:
- module_name = dirs[0]
- if len(dirs) > 1:
- item_name = dirs[1]
- # removed an index string when list-type value is
- # requested. Because such a item name can be accept by the
- # stats module currently.
- item_name = re.sub('\[\d+\]$', '', item_name)
- stats_spec = self.get_stats_spec(module_name, item_name)
- stats_data = self.get_stats_data(module_name, item_name)
- path_list = []
- try:
- path_list = item_name_list(stats_data, path)
- except isc.cc.data.DataNotFoundError as err:
- raise StatsHttpdDataError(
- "%s: %s" % (err.__class__.__name__, err))
- item_list = []
- for path in path_list:
- dirs = path.split("/")
- if len(dirs) < 2: continue
- item = {}
- item['identifier'] = path
- value = isc.cc.data.find(stats_data, path)
- if type(value) is bool:
- value = str(value).lower()
- if type(value) is dict or type(value) is list:
- value = None
- if value is not None:
- item['value'] = str(value)
- owner = dirs[0]
- item['owner'] = owner
- item['uri'] = urllib.parse.quote('%s/%s' % (XML_URL_PATH, path))
- item_path = '/'.join(dirs[1:])
- spec = isc.config.find_spec_part(stats_spec[owner], item_path)
- for key in ['name', 'type', 'description', 'title', \
- 'optional', 'default', 'format']:
- value = spec.get('item_%s' % key)
- if type(value) is bool:
- value = str(value).lower()
- if type(value) is dict or type(value) is list:
- value = None
- if value is not None:
- item[key] = str(value)
- item_list.append(item)
- xml_elem = xml.etree.ElementTree.Element(
- XML_ROOT_ELEMENT, attrib=XML_ROOT_ATTRIB)
- for item in item_list:
- item_elem = xml.etree.ElementTree.Element('item', attrib=item)
- xml_elem.append(item_elem)
- # The coding conversion is tricky. xml..tostring() of Python 3.2
- # returns bytes (not string) regardless of the coding, while
- # tostring() of Python 3.1 returns a string. To support both
- # cases transparently, we first make sure tostring() returns
- # bytes by specifying utf-8 and then convert the result to a
- # plain string (code below assume it).
- # FIXME: Non-ASCII characters might be lost here. Consider how
- # the whole system should handle non-ASCII characters.
- xml_string = str(xml.etree.ElementTree.tostring(xml_elem, encoding='utf-8'),
- encoding='us-ascii')
- self.xml_body = self.open_template(XML_TEMPLATE_LOCATION).substitute(
- xml_string=xml_string, xsl_url_path=XSL_URL_PATH)
- return self.xml_body
-
- def xsd_handler(self):
- """Loads the XSD template file, replaces the variable strings,
- and returns the string of the XSD document."""
- self.xsd_body = self.open_template(XSD_TEMPLATE_LOCATION).substitute(
- xsd_namespace=XSD_NAMESPACE)
- return self.xsd_body
-
- def xsl_handler(self):
- """Loads the XSL template file, replaces the variable strings,
- and returns the string of the XSL document."""
- self.xsl_body = self.open_template(XSL_TEMPLATE_LOCATION).substitute(
- xsd_namespace=XSD_NAMESPACE)
- return self.xsl_body
-
- def open_template(self, file_name):
- """It opens a template file, and it loads all lines to a
- string variable and returns string. Template object includes
- the variable. Limitation of a file size isn't needed there. XXXX"""
- lines = None
- try:
- with open(file_name, 'r') as f:
- lines = "".join(f.readlines())
- except IOError as err:
- raise StatsHttpdDataError(
- "%s: %s" % (err.__class__.__name__, err))
- return string.Template(lines)
-
-def main():
- try:
- parser = OptionParser()
- parser.add_option(
- "-v", "--verbose", dest="verbose", action="store_true",
- help="enable maximum debug logging")
- (options, args) = parser.parse_args()
- if options.verbose:
- isc.log.init("b10-stats-httpd", "DEBUG", 99, buffer=True)
- stats_httpd = StatsHttpd()
- stats_httpd.start()
- except OptionValueError as ove:
- logger.fatal(STATSHTTPD_BAD_OPTION_VALUE, ove)
- sys.exit(1)
- except isc.cc.session.SessionError as se:
- logger.fatal(STATSHTTPD_CC_SESSION_ERROR, se)
- sys.exit(1)
- except HttpServerError as hse:
- logger.fatal(STATSHTTPD_START_SERVER_INIT_ERROR, hse)
- sys.exit(1)
- except KeyboardInterrupt as kie:
- logger.info(STATSHTTPD_STOPPED_BY_KEYBOARD)
-
- logger.info(STATSHTTPD_EXITING)
-
-if __name__ == "__main__":
- isc.util.traceback_handler.traceback_handler(main)
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-# PERFORMANCE OF THIS SOFTWARE.
-
-# No namespace declaration - these constants go in the global namespace
-# of the stats_httpd_messages python module.
-
-% STATSHTTPD_BAD_OPTION_VALUE bad command line argument: %1
-The stats-httpd module was called with a bad command-line argument
-and will not start.
-
-% STATSHTTPD_CC_SESSION_ERROR error connecting to message bus: %1
-The stats-httpd module was unable to connect to the BIND 10 command
-and control bus. A likely problem is that the message bus daemon
-(b10-msgq) is not running. The stats-httpd module will now shut down.
-
-% STATSHTTPD_CLOSING closing %1
-The stats-httpd daemon will stop listening for requests on the given
-address and port number.
-
-% STATSHTTPD_CLOSING_CC_SESSION stopping cc session
-Debug message indicating that the stats-httpd module is disconnecting
-from the command and control bus.
-
-% STATSHTTPD_EXITING exiting
-The stats HTTP server is exiting.
-
-% STATSHTTPD_HANDLE_CONFIG reading configuration: %1
-The stats-httpd daemon has received new configuration data and will now
-process it. The (changed) data is printed.
-
-% STATSHTTPD_HTTPLOG %1 %2
-Debug HTTP log message. These are the messages logged by the http server
-instance. For most logs, the message shows HTTP client and query
-information like HTTP method, URI, and status code, but the http server
-can also log other information, such as extended status reports.
-
-% STATSHTTPD_RECEIVED_SHUTDOWN_COMMAND shutdown command received
-A shutdown command was sent to the stats-httpd module, and it will
-now shut down.
-
-% STATSHTTPD_RECEIVED_STATUS_COMMAND received command to return status
-A status command was sent to the stats-httpd module, and it will
-respond with 'Stats Httpd is up.' and its PID.
-
-% STATSHTTPD_RECEIVED_UNKNOWN_COMMAND received unknown command: %1
-An unknown command has been sent to the stats-httpd module. The
-stats-httpd module will respond with an error, and the command will
-be ignored.
-
-% STATSHTTPD_SERVER_DATAERROR HTTP server data error: %1
-An internal error occurred while handling an HTTP request. An HTTP 404
-response will be sent back, and the specific error is printed. This
-is an error condition that likely points the specified data
-corresponding to the requested URI is incorrect.
-
-% STATSHTTPD_SERVER_ERROR HTTP server error: %1
-An internal error occurred while handling an HTTP request. An HTTP 500
-response will be sent back, and the specific error is printed. This
-is an error condition that likely points to a module that is not
-responding correctly to statistics requests.
-
-% STATSHTTPD_SERVER_INIT_ERROR HTTP server initialization error: %1
-There was a problem initializing the HTTP server in the stats-httpd
-module upon receiving its configuration data. The most likely cause
-is a port binding problem or a bad configuration value. The specific
-error is printed in the message. The new configuration is ignored,
-and an error is sent back.
-
-% STATSHTTPD_SHUTDOWN shutting down
-The stats-httpd daemon is shutting down.
-
-% STATSHTTPD_STARTED listening on %1
-The stats-httpd daemon will now start listening for requests on the
-given address and port number.
-
-% STATSHTTPD_STARTING_CC_SESSION starting cc session
-Debug message indicating that the stats-httpd module is connecting to
-the command and control bus.
-
-% STATSHTTPD_START_SERVER_INIT_ERROR HTTP server initialization error: %1
-There was a problem initializing the HTTP server in the stats-httpd
-module upon startup. The most likely cause is that it was not able
-to bind to the listening port. The specific error is printed, and the
-module will shut down.
-
-% STATSHTTPD_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
-There was a keyboard interrupt signal to stop the stats-httpd
-daemon. The daemon will now shut down.
-
-% STATSHTTPD_UNKNOWN_CONFIG_ITEM unknown configuration item: %1
-The stats-httpd daemon received a configuration update from the
-configuration manager. However, one of the items in the
-configuration is unknown. The new configuration is ignored, and an
-error is sent back. As possible cause is that there was an upgrade
-problem, and the stats-httpd version is out of sync with the rest of
-the system.
+++ /dev/null
-# Copyright (C) 2011, 2012 Internet Systems Consortium, Inc. ("ISC")
-#
-# Permission to use, copy, modify, and/or distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
-# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
-# AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
-# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
-# OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
-# PERFORMANCE OF THIS SOFTWARE.
-
-# No namespace declaration - these constants go in the global namespace
-# of the stats_messages python module.
-
-% STATS_BAD_OPTION_VALUE bad command line argument: %1
-The stats module was called with a bad command-line argument and will
-not start.
-
-% STATS_CC_SESSION_ERROR error connecting to message bus: %1
-The stats module was unable to connect to the BIND 10 command and
-control bus. A likely problem is that the message bus daemon
-(b10-msgq) is not running. The stats module will now shut down.
-
-% STATS_EXITING exiting
-The stats module process is exiting.
-
-% STATS_RECEIVED_INVALID_STATISTICS_DATA received invalid statistics data from %1
-Invalid statistics data has been received from the module while
-polling and it has been discarded.
-
-% STATS_RECEIVED_NEW_CONFIG received new configuration: %1
-This debug message is printed when the stats module has received a
-configuration update from the configuration manager.
-
-% STATS_RECEIVED_SHOWSCHEMA_ALL_COMMAND received command to show all statistics schema
-The stats module received a command to show all statistics schemas of all modules.
-
-% STATS_RECEIVED_SHOWSCHEMA_NAME_COMMAND received command to show statistics schema for %1
-The stats module received a command to show the specified statistics schema of the specified module.
-
-% STATS_RECEIVED_SHOW_ALL_COMMAND received command to show all statistics
-The stats module received a command to show all statistics that it has
-collected.
-
-% STATS_RECEIVED_SHOW_NAME_COMMAND received command to show statistics for %1
-The stats module received a command to show the statistics that it has
-collected for the given item.
-
-% STATS_RECEIVED_SHUTDOWN_COMMAND shutdown command received
-A shutdown command was sent to the stats module and it will now shut down.
-
-% STATS_RECEIVED_STATUS_COMMAND received command to return status
-A status command was sent to the stats module. It will return a
-response indicating that it is running normally.
-
-% STATS_RECEIVED_UNKNOWN_COMMAND received unknown command: %1
-An unknown command has been sent to the stats module. The stats module
-will respond with an error and the command will be ignored.
-
-% STATS_SEND_STATISTICS_REQUEST requesting %1 to send statistics
-This debug message is printed when a request is sent to the module
-to send its data to the stats module.
-
-% STATS_SKIP_COLLECTING skipped collecting statistics from %1
-The stats module temporarily encountered an internal messaging bus error while
-collecting statistics from the module, then it skipped collecting statistics
-from it and will try to collect from the next module. The lack of statistics
-will be recovered at the next polling round.
-
-% STATS_SKIP_POLLING skipped polling statistics to modules
-The stats module temporarily encountered an internal messaging bus error while
-collecting initial information to collect statistics from the init module, then
-it skipped polling statistics and will try to do next time.
-
-% STATS_STARTING starting
-The stats module will be now starting.
-
-% STATS_START_ERROR stats module error: %1
-An internal error occurred while starting the stats module. The stats
-module will be now shutting down.
-
-% STATS_STOPPED_BY_KEYBOARD keyboard interrupt, shutting down
-There was a keyboard interrupt signal to stop the stats module. The
-daemon will now shut down.
-
-% STATS_UNKNOWN_COMMAND_IN_SPEC unknown command in specification file: %1
-The specification file for the stats module contains a command that
-is unknown in the implementation. The most likely cause is an
-installation problem, where the specification file stats.spec is
-from a different version of BIND 10 than the stats module itself.
-Please check your installation.
+++ /dev/null
-SUBDIRS = testdata .
-
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = stats_test.py stats-httpd_test.py
-EXTRA_DIST = $(PYTESTS) test_utils.py
-CLEANFILES = test_utils.pyc
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/stats:$(abs_top_builddir)/src/bin/stats/tests:$(abs_top_builddir)/src/bin/msgq:$(abs_top_builddir)/src/lib/python/isc/config \
- B10_FROM_SOURCE=$(abs_top_srcdir) \
- BIND10_MSGQ_SOCKET_FILE=$(abs_top_builddir)/msgq_socket \
- CONFIG_TESTDATA_PATH=$(abs_top_srcdir)/src/bin/stats/tests/testdata \
- B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-# Copyright (C) 2011-2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-In each of these tests we start several virtual components. They are
-not the real components, no external processes are started. They are
-just simple mock objects running each in its own thread and pretending
-to be bind10 modules. This helps testing the stats http server in a
-close to real environment.
-"""
-
-import unittest
-import os
-import imp
-import socket
-import errno
-import select
-import string
-import time
-import threading
-import http.client
-import xml.etree.ElementTree
-import random
-import urllib.parse
-import sys
-# load this module for xml validation with xsd. For this test, an
-# installation of lxml is required in advance. See http://lxml.de/.
-try:
- from lxml import etree as lxml_etree
-except ImportError:
- lxml_etree = None
-
-import isc
-import isc.log
-import stats_httpd
-import stats
-from test_utils import ThreadingServerManager, SignalHandler, \
- MyStatsHttpd, CONST_BASETIME
-from isc.testutils.ccsession_mock import MockModuleCCSession
-from isc.config import RPCRecipientMissing, RPCError
-
-# This test suite uses xml.etree.ElementTree.XMLParser via
-# xml.etree.ElementTree.parse. On the platform where expat isn't
-# installed, ImportError is raised and it's failed. Check expat is
-# available before the test invocation. Skip this test if it's
-# unavailable.
-try:
- # ImportError raised if xpat is unavailable
- xml_parser = xml.etree.ElementTree.XMLParser()
-except ImportError:
- xml_parser = None
-
-# set XML Namespaces for testing
-XMLNS_XSL = "http://www.w3.org/1999/XSL/Transform"
-XMLNS_XHTML = "http://www.w3.org/1999/xhtml"
-XMLNS_XSD = "http://www.w3.org/2001/XMLSchema"
-XMLNS_XSI = stats_httpd.XMLNS_XSI
-
-DUMMY_DATA = {
- 'Init' : {
- "boot_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)
- },
- 'Auth' : {
- "queries.tcp": 6,
- "queries.udp": 4,
- "queries.perzone": [{
- "zonename": "test1.example",
- "queries.tcp": 10,
- "queries.udp": 8
- }, {
- "zonename": "test2.example",
- "queries.tcp": 8,
- "queries.udp": 6
- }],
- "nds_queries.perzone": {
- "test10.example": {
- "queries.tcp": 10,
- "queries.udp": 8
- },
- "test20.example": {
- "queries.tcp": 8,
- "queries.udp": 6
- }
- }
- },
- 'Stats' : {
- "report_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
- "boot_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
- "last_update_time": time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
- "lname": "4d70d40a_c@host",
- "timestamp": time.mktime(CONST_BASETIME)
- }
- }
-
-# Bad practice: this should be localized
-stats._BASETIME = CONST_BASETIME
-stats.get_timestamp = lambda: time.mktime(CONST_BASETIME)
-stats.get_datetime = lambda x=None: time.strftime("%Y-%m-%dT%H:%M:%SZ", CONST_BASETIME)
-
-def get_availaddr(address='127.0.0.1', port=8001):
- """returns a tuple of address and port which is available to
- listen on the platform. The first argument is a address for
- search. The second argument is a port for search. If a set of
- address and port is failed on the search for the availability, the
- port number is increased and it goes on the next trial until the
- available set of address and port is looked up. If the port number
- reaches over 65535, it may stop the search and raise a
- OverflowError exception."""
- while True:
- for addr in socket.getaddrinfo(
- address, port, 0,
- socket.SOCK_STREAM, socket.IPPROTO_TCP):
- sock = socket.socket(addr[0], socket.SOCK_STREAM)
- try:
- sock.bind((address, port))
- return (address, port)
- except socket.error:
- continue
- finally:
- if sock: sock.close()
- # This address and port number are already in use.
- # next port number is added
- port = port + 1
-
-def is_ipv6_enabled(address='::1', port=8001):
- """checks IPv6 enabled on the platform. address for check is '::1'
- and port for check is random number between 8001 and
- 65535. Retrying is 3 times even if it fails. The built-in socket
- module provides a 'has_ipv6' parameter, but it's not used here
- because there may be a situation where the value is True on an
- environment where the IPv6 config is disabled."""
- for p in random.sample(range(port, 65535), 3):
- try:
- sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
- sock.bind((address, p))
- return True
- except socket.error:
- continue
- finally:
- if sock: sock.close()
- return False
-
-class TestItemNameList(unittest.TestCase):
-
- def test_item_name_list(self):
- # for a one-element list
- self.assertEqual(['a'],
- stats_httpd.item_name_list({'a':1}, 'a'))
- # for a dict under a dict
- self.assertEqual(['a','a/b'],
- stats_httpd.item_name_list({'a':{'b':1}}, 'a'))
- self.assertEqual(['a/b'],
- stats_httpd.item_name_list({'a':{'b':1}}, 'a/b'))
- self.assertEqual(['a','a/b','a/b/c'],
- stats_httpd.item_name_list({'a':{'b':{'c':1}}}, 'a'))
- self.assertEqual(['a/b','a/b/c'],
- stats_httpd.item_name_list({'a':{'b':{'c':1}}},
- 'a/b'))
- self.assertEqual(['a/b/c'],
- stats_httpd.item_name_list({'a':{'b':{'c':1}}},
- 'a/b/c'))
- # for a list under a dict
- self.assertEqual(['a[2]'],
- stats_httpd.item_name_list({'a':[1,2,3]}, 'a[2]'))
- self.assertEqual(['a', 'a[0]', 'a[1]', 'a[2]'],
- stats_httpd.item_name_list({'a':[1,2,3]}, 'a'))
- self.assertEqual(['a', 'a[0]', 'a[1]', 'a[2]'],
- stats_httpd.item_name_list({'a':[1,2,3]}, ''))
- # for a list under a dict under a dict
- self.assertEqual(['a', 'a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
- stats_httpd.item_name_list({'a':{'b':[1,2,3]}}, 'a'))
- self.assertEqual(['a', 'a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
- stats_httpd.item_name_list({'a':{'b':[1,2,3]}}, ''))
- self.assertEqual(['a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
- stats_httpd.item_name_list({'a':{'b':[1,2,3]}}, 'a/b'))
- # for a mixed case of the above
- self.assertEqual(['a', 'a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]', 'a/c'],
- stats_httpd.item_name_list(
- {'a':{'b':[1,2,3], 'c':1}}, 'a'))
- self.assertEqual(['a/b', 'a/b[0]', 'a/b[1]', 'a/b[2]'],
- stats_httpd.item_name_list(
- {'a':{'b':[1,2,3], 'c':1}}, 'a/b'))
- self.assertEqual(['a/c'],
- stats_httpd.item_name_list(
- {'a':{'b':[1,2,3], 'c':1}}, 'a/c'))
- # for specifying a wrong identifier which is not found in
- # element
- self.assertRaises(isc.cc.data.DataNotFoundError,
- stats_httpd.item_name_list, {'x':1}, 'a')
- # for specifying a string in element and an empty string in
- # identifier
- self.assertEqual([],
- stats_httpd.item_name_list('a', ''))
- # for specifying empty strings in element and identifier
- self.assertEqual([],
- stats_httpd.item_name_list('', ''))
- # for specifying wrong element, which is an non-empty string,
- # and an non-empty string in identifier
- self.assertRaises(isc.cc.data.DataTypeError,
- stats_httpd.item_name_list, 'a', 'a')
- # for specifying None in element and identifier
- self.assertRaises(isc.cc.data.DataTypeError,
- stats_httpd.item_name_list, None, None)
- # for specifying non-dict in element
- self.assertRaises(isc.cc.data.DataTypeError,
- stats_httpd.item_name_list, [1,2,3], 'a')
- self.assertRaises(isc.cc.data.DataTypeError,
- stats_httpd.item_name_list, [1,2,3], '')
- # for checking key names sorted which consist of element
- num = 11
- keys = [ 'a', 'aa', 'b' ]
- keys.sort(reverse=True)
- dictlist = dict([ (k, list(range(num))) for k in keys ])
- keys.sort()
- ans = []
- for k in keys:
- ans += [k] + [ '%s[%d]' % (k, i) for i in range(num) ]
- self.assertEqual(ans,
- stats_httpd.item_name_list(dictlist, ''))
-
-class TestHttpHandler(unittest.TestCase):
- """Tests for HttpHandler class"""
- def setUp(self):
- # set the signal handler for deadlock
- self.sig_handler = SignalHandler(self.fail)
- DUMMY_DATA['Stats']['lname'] = 'test-lname'
- (self.address, self.port) = get_availaddr()
- self.stats_httpd_server = ThreadingServerManager(MyStatsHttpd,
- (self.address,
- self.port))
- self.stats_httpd = self.stats_httpd_server.server
- self.stats_httpd_server.run()
- self.client = http.client.HTTPConnection(self.address, self.port)
- self.client._http_vsn_str = 'HTTP/1.0\n'
- self.client.connect()
-
- def tearDown(self):
- self.client.close()
- self.stats_httpd_server.shutdown()
- # reset the signal handler
- self.sig_handler.reset()
-
- @unittest.skipUnless(xml_parser, "skipping the test using XMLParser")
- def test_do_GET(self):
- self.assertTrue(type(self.stats_httpd.httpd) is list)
- self.assertEqual(len(self.stats_httpd.httpd), 1)
- self.assertEqual((self.address, self.port), self.stats_httpd.http_addrs[0])
-
- def check_XML_URL_PATH(path=''):
- url_path = '%s/%s' % (stats_httpd.XML_URL_PATH, path)
- url_path = urllib.parse.quote(url_path)
- self.client.putrequest('GET', url_path)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.getheader("Content-type"), "text/xml")
- self.assertGreater(int(response.getheader("Content-Length")), 0)
- self.assertEqual(response.status, 200)
- xml_doctype = response.readline().decode()
- xsl_doctype = response.readline().decode()
- self.assertGreater(len(xml_doctype), 0)
- self.assertGreater(len(xsl_doctype), 0)
- root = xml.etree.ElementTree.parse(response).getroot()
- self.assertGreater(root.tag.find('statistics'), 0)
- schema_loc = '{%s}schemaLocation' % XMLNS_XSI
- # check the path of XSD
- self.assertEqual(root.attrib[schema_loc],
- stats_httpd.XSD_NAMESPACE + ' '
- + stats_httpd.XSD_URL_PATH)
- # check the path of XSL
- self.assertTrue(xsl_doctype.startswith(
- '<?xml-stylesheet type="text/xsl" href="' +
- stats_httpd.XSL_URL_PATH
- + '"?>'))
- # check whether the list of 'identifier' attributes in
- # root is same as the list of item names in DUMMY_DATA
- id_list = [ elm.attrib['identifier'] for elm in root ]
- item_list = [ it for it in \
- stats_httpd.item_name_list(DUMMY_DATA, path) \
- if len(it.split('/')) > 1 ]
- self.assertEqual(id_list, item_list)
- for elem in root:
- attr = elem.attrib
- value = isc.cc.data.find(DUMMY_DATA, attr['identifier'])
- # No 'value' attribute should be found in the 'item'
- # element when datatype of the value is list or dict.
- if type(value) is list or type(value) is dict:
- self.assertFalse('value' in attr)
- # The value of the 'value' attribute should be checked
- # after casting it to string type if datatype of the
- # value is int or float. Because attr['value'] returns
- # string type even if its value is int or float.
- elif type(value) is int or type(value) is float:
- self.assertEqual(attr['value'], str(value))
- else:
- self.assertEqual(attr['value'], value)
-
- # URL is '/bind10/statistics/xml'
- check_XML_URL_PATH()
- for path in stats_httpd.item_name_list(DUMMY_DATA, ''):
- check_XML_URL_PATH(path)
-
- def check_XSD_URL_PATH():
- url_path = stats_httpd.XSD_URL_PATH
- url_path = urllib.parse.quote(url_path)
- self.client.putrequest('GET', url_path)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.getheader("Content-type"), "text/xml")
- self.assertGreater(int(response.getheader("Content-Length")), 0)
- self.assertEqual(response.status, 200)
- root = xml.etree.ElementTree.parse(response).getroot()
- url_xmlschema = '{%s}' % XMLNS_XSD
- self.assertGreater(root.tag.find('schema'), 0)
- self.assertTrue(hasattr(root, 'attrib'))
- self.assertTrue('targetNamespace' in root.attrib)
- self.assertEqual(root.attrib['targetNamespace'],
- stats_httpd.XSD_NAMESPACE)
-
- # URL is '/bind10/statistics/xsd'
- check_XSD_URL_PATH()
-
- def check_XSL_URL_PATH():
- url_path = stats_httpd.XSL_URL_PATH
- url_path = urllib.parse.quote(url_path)
- self.client.putrequest('GET', url_path)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.getheader("Content-type"), "text/xml")
- self.assertGreater(int(response.getheader("Content-Length")), 0)
- self.assertEqual(response.status, 200)
- root = xml.etree.ElementTree.parse(response).getroot()
- url_trans = '{%s}' % XMLNS_XSL
- url_xhtml = '{%s}' % XMLNS_XHTML
- self.assertEqual(root.tag, url_trans + 'stylesheet')
-
- # URL is '/bind10/statistics/xsl'
- check_XSL_URL_PATH()
-
- # 302 redirect
- self.client._http_vsn_str = 'HTTP/1.1'
- self.client.putrequest('GET', '/')
- self.client.putheader('Host', self.address)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 302)
- self.assertEqual(response.getheader('Location'),
- "http://%s:%d%s/" % (self.address, self.port, stats_httpd.XML_URL_PATH))
-
- # 404 NotFound (random path)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', '/path/to/foo/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', '/bind10/foo')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', '/bind10/statistics/foo')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + 'Auth') # with no slash
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
-
- # 200 ok
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/#foo')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/?foo=bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
-
- # 404 NotFound (too long path)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Init/boot_time/a')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
-
- # 404 NotFound (nonexistent module name)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Foo')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XSD_URL_PATH + '/Foo')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XSL_URL_PATH + '/Foo')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
-
- # 404 NotFound (nonexistent item name)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Foo/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XSD_URL_PATH + '/Foo/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XSL_URL_PATH + '/Foo/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
-
- # 404 NotFound (existent module but nonexistent item name)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/Auth/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XSD_URL_PATH + '/Auth/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
- self.client._http_vsn_str = 'HTTP/1.0'
- self.client.putrequest('GET', stats_httpd.XSL_URL_PATH + '/Auth/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
-
- def test_do_GET_failed1(self):
- # failure case (Stats is down, so rpc_call() results in an exception)
- # Note: this should eventually be RPCRecipientMissing.
- self.stats_httpd._rpc_answers.append(
- isc.cc.session.SessionTimeout('timeout'))
-
- # request XML
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 500)
-
- # request XSD
- self.client.putrequest('GET', stats_httpd.XSD_URL_PATH)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
-
- # request XSL
- self.client.putrequest('GET', stats_httpd.XSL_URL_PATH)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
-
- def test_do_GET_failed2(self):
- # failure case(Stats replies an error)
- self.stats_httpd._rpc_answers.append(
- RPCError(1, "specified arguments are incorrect: I have an error."))
-
- # request XML
- self.client.putrequest('GET', stats_httpd.XML_URL_PATH + '/')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
-
- # request XSD
- self.stats_httpd._rpc_answers.append(
- RPCError(1, "specified arguments are incorrect: I have an error."))
- self.client.putrequest('GET', stats_httpd.XSD_URL_PATH)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
-
- # request XSL
- self.stats_httpd._rpc_answers.append(
- RPCError(1, "specified arguments are incorrect: I have an error."))
- self.client.putrequest('GET', stats_httpd.XSL_URL_PATH)
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
-
- def test_do_HEAD(self):
- self.client.putrequest('HEAD', stats_httpd.XML_URL_PATH + '/')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 200)
-
- self.client.putrequest('HEAD', '/path/to/foo/bar')
- self.client.endheaders()
- response = self.client.getresponse()
- self.assertEqual(response.status, 404)
-
- @unittest.skipUnless(lxml_etree, "skipping XML validation with XSD")
- def test_xml_validation_with_xsd(self):
- """Tests for XML validation with XSD. If lxml is not
- installed, this tests would be skipped."""
- def request_xsd():
- url_path = stats_httpd.XSD_URL_PATH
- url_path = urllib.parse.quote(url_path)
- self.client.putrequest('GET', url_path)
- self.client.endheaders()
- xsd_doc = self.client.getresponse()
- xsd_doc = lxml_etree.parse(xsd_doc)
- return lxml_etree.XMLSchema(xsd_doc)
-
- def request_xmldoc(path=''):
- url_path = '%s/%s' % (stats_httpd.XML_URL_PATH, path)
- url_path = urllib.parse.quote(url_path)
- self.client.putrequest('GET', url_path)
- self.client.endheaders()
- xml_doc = self.client.getresponse()
- return lxml_etree.parse(xml_doc)
-
- # request XSD and XML
- xsd = request_xsd()
- xml_doc = request_xmldoc()
- # do validation
- self.assertTrue(xsd.validate(xml_doc))
-
- # validate each paths in DUMMY_DATA
- for path in stats_httpd.item_name_list(DUMMY_DATA, ''):
- # request XML
- xml_doc = request_xmldoc(path)
- # do validation
- self.assertTrue(xsd.validate(xml_doc))
-
-class TestHttpServerError(unittest.TestCase):
- """Tests for HttpServerError exception"""
- def test_raises(self):
- try:
- raise stats_httpd.HttpServerError('Nothing')
- except stats_httpd.HttpServerError as err:
- self.assertEqual(str(err), 'Nothing')
-
-class TestHttpServer(unittest.TestCase):
- """Tests for HttpServer class"""
- def setUp(self):
- # set the signal handler for deadlock
- self.sig_handler = SignalHandler(self.fail)
-
- def tearDown(self):
- if hasattr(self, "stats_httpd"):
- self.stats_httpd.stop()
- # reset the signal handler
- self.sig_handler.reset()
-
- def test_httpserver(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.assertEqual(type(self.stats_httpd.httpd), list)
- self.assertEqual(len(self.stats_httpd.httpd), 1)
- for httpd in self.stats_httpd.httpd:
- self.assertTrue(isinstance(httpd, stats_httpd.HttpServer))
-
-class TestStatsHttpdError(unittest.TestCase):
- """Tests for StatsHttpdError exception"""
-
- def test_raises1(self):
- try:
- raise stats_httpd.StatsHttpdError('Nothing')
- except stats_httpd.StatsHttpdError as err:
- self.assertEqual(str(err), 'Nothing')
-
- def test_raises2(self):
- try:
- raise stats_httpd.StatsHttpdDataError('Nothing')
- except stats_httpd.StatsHttpdDataError as err:
- self.assertEqual(str(err), 'Nothing')
-
-class TestStatsHttpd(unittest.TestCase):
- """Tests for StatsHttpd class"""
-
- def setUp(self):
- # set the signal handler for deadlock
- self.sig_handler = SignalHandler(self.fail)
- # checking IPv6 enabled on this platform
- self.ipv6_enabled = is_ipv6_enabled()
- # instantiation of StatsHttpd indirectly calls gethostbyaddr(), which
- # can block for an uncontrollable period, leading many undesirable
- # results. We should rather eliminate the reliance, but until we
- # can make such fundamental cleanup we replace it with a faked method;
- # in our test scenario the return value doesn't matter.
- self.__gethostbyaddr_orig = socket.gethostbyaddr
- socket.gethostbyaddr = lambda x: ('test.example.', [], None)
-
- # Some tests replace this library function. Keep the original for
- # restor
- self.__orig_select_select = select.select
-
- def tearDown(self):
- socket.gethostbyaddr = self.__gethostbyaddr_orig
- if hasattr(self, "stats_httpd"):
- self.stats_httpd.stop()
- # reset the signal handler
- self.sig_handler.reset()
-
- # restore original of replaced library
- select.select = self.__orig_select_select
-
- def test_init(self):
- server_address = get_availaddr()
- self.stats_httpd = MyStatsHttpd(server_address)
- self.assertEqual(self.stats_httpd.running, False)
- self.assertEqual(self.stats_httpd.poll_intval, 0.5)
- self.assertNotEqual(len(self.stats_httpd.httpd), 0)
- self.assertIsNotNone(self.stats_httpd.mccs)
- self.assertIsNotNone(self.stats_httpd.cc_session)
- # The real CfgMgr would return 'version', but our test mock omits it,
- # so the len(config) should be 1
- self.assertEqual(len(self.stats_httpd.config), 1)
- self.assertTrue('listen_on' in self.stats_httpd.config)
- self.assertEqual(len(self.stats_httpd.config['listen_on']), 1)
- self.assertTrue('address' in self.stats_httpd.config['listen_on'][0])
- self.assertTrue('port' in self.stats_httpd.config['listen_on'][0])
- self.assertTrue(server_address in set(self.stats_httpd.http_addrs))
- self.assertEqual('StatsHttpd', self.stats_httpd.mccs.\
- get_module_spec().get_module_name())
-
- def test_init_hterr(self):
- """Test the behavior of StatsHttpd constructor when open_httpd fails.
-
- We specifically check the following two:
- - close_mccs() is called (so stats-httpd tells ConfigMgr it's shutting
- down)
- - the constructor results in HttpServerError exception.
-
- """
- self.__mccs_closed = False
- def call_checker():
- self.__mccs_closed = True
- class FailingStatsHttpd(MyStatsHttpd):
- def open_httpd(self):
- raise stats_httpd.HttpServerError
- def close_mccs(self):
- call_checker()
- self.assertRaises(stats_httpd.HttpServerError, FailingStatsHttpd)
- self.assertTrue(self.__mccs_closed)
-
- def test_openclose_mccs(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- mccs = self.stats_httpd.mccs
- self.assertFalse(self.stats_httpd.mccs.stopped)
- self.assertFalse(self.stats_httpd.mccs.closed)
- self.stats_httpd.close_mccs()
- self.assertTrue(mccs.stopped)
- self.assertTrue(mccs.closed)
- self.assertIsNone(self.stats_httpd.mccs)
- self.stats_httpd.open_mccs()
- self.assertIsNotNone(self.stats_httpd.mccs)
- self.stats_httpd.mccs = None
- self.assertIsNone(self.stats_httpd.mccs)
- self.assertIsNone(self.stats_httpd.close_mccs())
-
- def test_mccs(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.assertIsNotNone(self.stats_httpd.mccs.get_socket())
- self.assertTrue(
- isinstance(self.stats_httpd.mccs.get_socket(), socket.socket))
- self.assertIsNotNone(self.stats_httpd.cc_session)
- statistics_spec = self.stats_httpd.get_stats_spec()
- for mod in DUMMY_DATA:
- self.assertTrue(mod in statistics_spec)
- for cfg in statistics_spec[mod]:
- self.assertTrue('item_name' in cfg)
- self.assertTrue(cfg['item_name'] in DUMMY_DATA[mod])
- self.assertTrue(len(statistics_spec[mod]), len(DUMMY_DATA[mod]))
- self.stats_httpd.close_mccs()
- self.assertIsNone(self.stats_httpd.mccs)
-
- def test_httpd(self):
- # dual stack (addresses is ipv4 and ipv6)
- if self.ipv6_enabled:
- server_addresses = (get_availaddr('::1'), get_availaddr())
- self.stats_httpd = MyStatsHttpd(*server_addresses)
- for ht in self.stats_httpd.httpd:
- self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
- self.assertTrue(ht.address_family in set([socket.AF_INET,
- socket.AF_INET6]))
- self.assertTrue(isinstance(ht.socket, socket.socket))
- ht.socket.close() # to silence warning about resource leak
- self.stats_httpd.close_mccs() # ditto
-
- # dual stack (address is ipv6)
- if self.ipv6_enabled:
- server_addresses = get_availaddr('::1')
- self.stats_httpd = MyStatsHttpd(server_addresses)
- for ht in self.stats_httpd.httpd:
- self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
- self.assertEqual(ht.address_family, socket.AF_INET6)
- self.assertTrue(isinstance(ht.socket, socket.socket))
- ht.socket.close()
- self.stats_httpd.close_mccs() # ditto
-
- # dual/single stack (address is ipv4)
- server_addresses = get_availaddr()
- self.stats_httpd = MyStatsHttpd(server_addresses)
- for ht in self.stats_httpd.httpd:
- self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
- self.assertEqual(ht.address_family, socket.AF_INET)
- self.assertTrue(isinstance(ht.socket, socket.socket))
- ht.socket.close()
- self.stats_httpd.close_mccs()
-
- def test_httpd_anyIPv4(self):
- # any address (IPv4)
- server_addresses = get_availaddr(address='0.0.0.0')
- self.stats_httpd = MyStatsHttpd(server_addresses)
- for ht in self.stats_httpd.httpd:
- self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
- self.assertEqual(ht.address_family,socket.AF_INET)
- self.assertTrue(isinstance(ht.socket, socket.socket))
-
- def test_httpd_anyIPv6(self):
- # any address (IPv6)
- if self.ipv6_enabled:
- server_addresses = get_availaddr(address='::')
- self.stats_httpd = MyStatsHttpd(server_addresses)
- for ht in self.stats_httpd.httpd:
- self.assertTrue(isinstance(ht, stats_httpd.HttpServer))
- self.assertEqual(ht.address_family,socket.AF_INET6)
- self.assertTrue(isinstance(ht.socket, socket.socket))
-
- def test_httpd_failed(self):
- # existent hostname
- self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd,
- get_availaddr(address='localhost'))
-
- # nonexistent hostname
- self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd,
- ('my.host.domain', 8000))
-
- # over flow of port number
- self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd,
- ('127.0.0.1', 80000))
-
- # negative
- self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd,
- ('127.0.0.1', -8000))
-
- # alphabet
- self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd,
- ('127.0.0.1', 'ABCDE'))
-
- # Address already in use
- server_addresses = get_availaddr()
- server = MyStatsHttpd(server_addresses)
- self.assertRaises(stats_httpd.HttpServerError, MyStatsHttpd,
- server_addresses)
-
- def __faked_select(self, ex=None):
- """A helper subroutine for tests using faked select.select.
-
- See test_running() for basic features. If ex is not None,
- it's assumed to be an exception object and will be raised on the
- first call.
-
- """
- self.assertTrue(self.stats_httpd.running)
- self.__call_count += 1
- if ex is not None and self.__call_count == 1:
- raise ex
- if self.__call_count == 2:
- self.stats_httpd.running = False
- assert self.__call_count <= 2 # safety net to avoid infinite loop
- return ([], [], [])
-
- def test_running(self):
- # Previous version of this test checks the result of "status" and
- # "shutdown" commands; however, they are more explicitly tested
- # in specific tests. In this test we only have to check:
- # - start() will set 'running' to True
- # - as long as 'running' is True, it keeps calling select.select
- # - when running becomes False, it exists from the loop and calls
- # stop()
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.assertFalse(self.stats_httpd.running)
-
- # In this test we'll call select.select() 2 times: on the first call
- # stats_httpd.running should be True; on the second call the faked
- # select() will set it to False.
- self.__call_count = 0
- select.select = lambda r, w, x, t: self.__faked_select()
- self.stats_httpd.start()
- self.assertFalse(self.stats_httpd.running)
- self.assertIsNone(self.stats_httpd.mccs) # stop() clears .mccs
-
- def test_running_fail(self):
- # A failure case of start(): we close the (real but dummy) socket for
- # the CC session. This breaks the select-loop due to exception
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.stats_httpd.mccs.get_socket().close()
- self.assertRaises(ValueError, self.stats_httpd.start)
-
- def test_failure_with_a_select_error (self):
- """checks select.error is raised if the exception except
- errno.EINTR is raised while it's selecting"""
- def raise_select_except(*args):
- raise select.error('dummy error')
- select.select = raise_select_except
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.assertRaises(select.error, self.stats_httpd.start)
-
- def test_nofailure_with_errno_EINTR(self):
- """checks no exception is raised if errno.EINTR is raised
- while it's selecting"""
- self.__call_count = 0
- select.select = lambda r, w, x, t: self.__faked_select(
- select.error(errno.EINTR))
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.stats_httpd.start() # shouldn't leak the exception
- self.assertFalse(self.stats_httpd.running)
- self.assertIsNone(self.stats_httpd.mccs)
-
- def test_open_template(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- # successful conditions
- tmpl = self.stats_httpd.open_template(
- stats_httpd.XML_TEMPLATE_LOCATION)
- self.assertTrue(isinstance(tmpl, string.Template))
- opts = dict(
- xml_string="<dummy></dummy>",
- xsl_url_path="/path/to/")
- lines = tmpl.substitute(opts)
- for n in opts:
- self.assertGreater(lines.find(opts[n]), 0)
- tmpl = self.stats_httpd.open_template(
- stats_httpd.XSD_TEMPLATE_LOCATION)
- self.assertTrue(isinstance(tmpl, string.Template))
- opts = dict(xsd_namespace="http://host/path/to/")
- lines = tmpl.substitute(opts)
- for n in opts:
- self.assertGreater(lines.find(opts[n]), 0)
- tmpl = self.stats_httpd.open_template(
- stats_httpd.XSL_TEMPLATE_LOCATION)
- self.assertTrue(isinstance(tmpl, string.Template))
- opts = dict(xsd_namespace="http://host/path/to/")
- lines = tmpl.substitute(opts)
- for n in opts:
- self.assertGreater(lines.find(opts[n]), 0)
- # unsuccessful condition
- self.assertRaises(
- stats_httpd.StatsHttpdDataError,
- self.stats_httpd.open_template, '/path/to/foo/bar')
-
- def test_commands(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.assertEqual(self.stats_httpd.command_handler("status", None),
- isc.config.ccsession.create_answer(
- 0, "Stats Httpd is up. (PID " + str(os.getpid()) + ")"))
- self.stats_httpd.running = True
- self.assertEqual(self.stats_httpd.command_handler("shutdown", None),
- isc.config.ccsession.create_answer(0))
- self.assertFalse(self.stats_httpd.running)
- self.assertEqual(
- self.stats_httpd.command_handler("__UNKNOWN_COMMAND__", None),
- isc.config.ccsession.create_answer(
- 1, "Unknown command: __UNKNOWN_COMMAND__"))
-
- def test_config(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- self.assertEqual(
- self.stats_httpd.config_handler(dict(_UNKNOWN_KEY_=None)),
- isc.config.ccsession.create_answer(
- 1, "unknown item _UNKNOWN_KEY_"))
-
- addresses = get_availaddr()
- self.assertEqual(
- self.stats_httpd.config_handler(
- dict(listen_on=[dict(address=addresses[0],port=addresses[1])])),
- isc.config.ccsession.create_answer(0))
- self.assertTrue("listen_on" in self.stats_httpd.config)
- for addr in self.stats_httpd.config["listen_on"]:
- self.assertTrue("address" in addr)
- self.assertTrue("port" in addr)
- self.assertTrue(addr["address"] == addresses[0])
- self.assertTrue(addr["port"] == addresses[1])
-
- if self.ipv6_enabled:
- addresses = get_availaddr("::1")
- self.assertEqual(
- self.stats_httpd.config_handler(
- dict(listen_on=[dict(address=addresses[0],port=addresses[1])])),
- isc.config.ccsession.create_answer(0))
- self.assertTrue("listen_on" in self.stats_httpd.config)
- for addr in self.stats_httpd.config["listen_on"]:
- self.assertTrue("address" in addr)
- self.assertTrue("port" in addr)
- self.assertTrue(addr["address"] == addresses[0])
- self.assertTrue(addr["port"] == addresses[1])
-
- addresses = get_availaddr()
- self.assertEqual(
- self.stats_httpd.config_handler(
- dict(listen_on=[dict(address=addresses[0],port=addresses[1])])),
- isc.config.ccsession.create_answer(0))
- self.assertTrue("listen_on" in self.stats_httpd.config)
- for addr in self.stats_httpd.config["listen_on"]:
- self.assertTrue("address" in addr)
- self.assertTrue("port" in addr)
- self.assertTrue(addr["address"] == addresses[0])
- self.assertTrue(addr["port"] == addresses[1])
- (ret, arg) = isc.config.ccsession.parse_answer(
- self.stats_httpd.config_handler(
- dict(listen_on=[dict(address="1.2.3.4",port=543210)]))
- )
- self.assertEqual(ret, 1)
-
- @unittest.skipUnless(xml_parser, "skipping the test using XMLParser")
- def test_xml_handler(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- module_name = 'Dummy'
- stats_spec = \
- { module_name :
- [{
- "item_name": "foo",
- "item_type": "string",
- "item_optional": False,
- "item_default": "bar",
- "item_description": "foo is bar",
- "item_title": "Foo"
- },
- {
- "item_name": "foo2",
- "item_type": "list",
- "item_optional": False,
- "item_default": [
- {
- "zonename" : "test1",
- "queries.udp" : 1,
- "queries.tcp" : 2
- },
- {
- "zonename" : "test2",
- "queries.udp" : 3,
- "queries.tcp" : 4
- }
- ],
- "item_title": "Foo bar",
- "item_description": "Foo bar",
- "list_item_spec": {
- "item_name": "foo2-1",
- "item_type": "map",
- "item_optional": False,
- "item_default": {},
- "map_item_spec": [
- {
- "item_name": "foo2-1-1",
- "item_type": "string",
- "item_optional": False,
- "item_default": "",
- "item_title": "Foo2 1 1",
- "item_description": "Foo bar"
- },
- {
- "item_name": "foo2-1-2",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Foo2 1 2",
- "item_description": "Foo bar"
- },
- {
- "item_name": "foo2-1-3",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Foo2 1 3",
- "item_description": "Foo bar"
- }
- ]
- }
- }]
- }
- stats_data = \
- { module_name : { 'foo':'bar',
- 'foo2': [
- {
- "foo2-1-1" : "bar1",
- "foo2-1-2" : 10,
- "foo2-1-3" : 9
- },
- {
- "foo2-1-1" : "bar2",
- "foo2-1-2" : 8,
- "foo2-1-3" : 7
- }
- ] } }
- self.stats_httpd.get_stats_spec = lambda x,y: stats_spec
- self.stats_httpd.get_stats_data = lambda x,y: stats_data
- xml_string = self.stats_httpd.xml_handler()
- stats_xml = xml.etree.ElementTree.fromstring(xml_string)
- schema_loc = '{%s}schemaLocation' % XMLNS_XSI
- self.assertEqual(stats_xml.attrib[schema_loc],
- stats_httpd.XML_ROOT_ATTRIB['xsi:schemaLocation'])
- stats_data = stats_data[module_name]
- stats_spec = stats_spec[module_name]
- names = stats_httpd.item_name_list(stats_data, '')
- for i in range(0, len(names)):
- self.assertEqual('%s/%s' % (module_name, names[i]), stats_xml[i].attrib['identifier'])
- value = isc.cc.data.find(stats_data, names[i])
- if type(value) is int:
- value = str(value)
- if type(value) is dict or type(value) is list:
- self.assertFalse('value' in stats_xml[i].attrib)
- else:
- self.assertEqual(value, stats_xml[i].attrib['value'])
- self.assertEqual(module_name, stats_xml[i].attrib['owner'])
- self.assertEqual(urllib.parse.quote('%s/%s/%s' % (stats_httpd.XML_URL_PATH,
- module_name, names[i])),
- stats_xml[i].attrib['uri'])
- spec = isc.config.find_spec_part(stats_spec, names[i])
- self.assertEqual(spec['item_name'], stats_xml[i].attrib['name'])
- self.assertEqual(spec['item_type'], stats_xml[i].attrib['type'])
- self.assertEqual(spec['item_description'], stats_xml[i].attrib['description'])
- self.assertEqual(spec['item_title'], stats_xml[i].attrib['title'])
- self.assertEqual(str(spec['item_optional']).lower(), stats_xml[i].attrib['optional'])
- default = spec['item_default']
- if type(default) is int:
- default = str(default)
- if type(default) is dict or type(default) is list:
- self.assertFalse('default' in stats_xml[i].attrib)
- else:
- self.assertEqual(default, stats_xml[i].attrib['default'])
- self.assertFalse('item_format' in spec)
- self.assertFalse('format' in stats_xml[i].attrib)
-
- @unittest.skipUnless(xml_parser, "skipping the test using XMLParser")
- def test_xsd_handler(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- xsd_string = self.stats_httpd.xsd_handler()
- stats_xsd = xml.etree.ElementTree.fromstring(xsd_string)
- ns = '{%s}' % XMLNS_XSD
- stats_xsd = stats_xsd[1].find('%scomplexType/%ssequence/%selement' % ((ns,)*3))
- self.assertEqual('item', stats_xsd.attrib['name'])
- stats_xsd = stats_xsd.find('%scomplexType' % ns)
- type_types = ('boolean', 'integer', 'real', 'string', 'map', \
- 'list', 'named_set', 'any')
- attribs = [('identifier', 'string', 'required'),
- ('value', 'string', 'optional'),
- ('owner', 'string', 'required'),
- ('uri', 'anyURI', 'required'),
- ('name', 'string', 'required'),
- ('type', type_types, 'required'),
- ('description', 'string', 'optional'),
- ('title', 'string', 'optional'),
- ('optional', 'boolean', 'optional'),
- ('default', 'string', 'optional'),
- ('format', 'string', 'optional')]
- for i in range(0, len(attribs)):
- self.assertEqual(attribs[i][0], stats_xsd[i].attrib['name'])
- if attribs[i][0] == 'type':
- stats_xsd_types = \
- stats_xsd[i].find('%ssimpleType/%srestriction' % \
- ((ns,)*2))
- for j in range(0, len(attribs[i][1])):
- self.assertEqual(attribs[i][1][j], \
- stats_xsd_types[j].attrib['value'])
- else:
- self.assertEqual(attribs[i][1], stats_xsd[i].attrib['type'])
- self.assertEqual(attribs[i][2], stats_xsd[i].attrib['use'])
-
- @unittest.skipUnless(xml_parser, "skipping the test using XMLParser")
- def test_xsl_handler(self):
- self.stats_httpd = MyStatsHttpd(get_availaddr())
- xsl_string = self.stats_httpd.xsl_handler()
- stats_xsl = xml.etree.ElementTree.fromstring(xsl_string)
- nst = '{%s}' % XMLNS_XSL
- nsx = '{%s}' % XMLNS_XHTML
- self.assertEqual("bind10:statistics", stats_xsl[2].attrib['match'])
- stats_xsl = stats_xsl[2].find('%stable' % nsx)
- self.assertEqual('item', stats_xsl[1].attrib['select'])
- stats_xsl = stats_xsl[1].find('%str' % nsx)
- self.assertEqual('@uri', stats_xsl[0].find(
- '%selement/%sattribute/%svalue-of' % ((nst,)*3)).attrib['select'])
- self.assertEqual('@identifier', stats_xsl[0].find(
- '%selement/%svalue-of' % ((nst,)*2)).attrib['select'])
- self.assertEqual('@value', stats_xsl[1].find('%sif' % nst).attrib['test'])
- self.assertEqual('@value', stats_xsl[1].find('%sif/%svalue-of' % ((nst,)*2)).attrib['select'])
- self.assertEqual('@description', stats_xsl[2].find('%sif' % nst).attrib['test'])
- self.assertEqual('@description', stats_xsl[2].find('%sif/%svalue-of' % ((nst,)*2)).attrib['select'])
-
-class Z_TestOSEnv(unittest.TestCase):
- def test_for_without_B10_FROM_SOURCE(self):
- # Note: this test is sensitive due to its substantial side effect of
- # reloading. For exmaple, it affects tests that tweak module
- # attributes (such as test_init_hterr). It also breaks logging
- # setting for unit tests. To minimize these effects, we use
- # workaround: make it very likely to run at the end of the tests
- # by naming the test class "Z_".
-
- # just lets it go through the code without B10_FROM_SOURCE env
- # variable
- if "B10_FROM_SOURCE" in os.environ:
- tmppath = os.environ["B10_FROM_SOURCE"]
- os.environ.pop("B10_FROM_SOURCE")
- imp.reload(stats_httpd)
- os.environ["B10_FROM_SOURCE"] = tmppath
- imp.reload(stats_httpd)
-
-if __name__ == "__main__":
- isc.log.resetUnitTestRootLogger()
- unittest.main()
+++ /dev/null
-# Copyright (C) 2010-2013 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-In each of these tests we start several virtual components. They are
-not the real components, no external processes are started. They are
-just simple mock objects running each in its own thread and pretending
-to be bind10 modules. This helps testing the stats module in a close
-to real environment.
-"""
-
-import unittest
-import os
-import io
-import time
-import imp
-import sys
-
-import stats
-import isc.log
-from test_utils import MyStats
-from isc.config.ccsession import create_answer
-
-class TestUtilties(unittest.TestCase):
- items = [
- { 'item_name': 'test_int1', 'item_type': 'integer', 'item_default': 12345 },
- { 'item_name': 'test_real1', 'item_type': 'real', 'item_default': 12345.6789 },
- { 'item_name': 'test_bool1', 'item_type': 'boolean', 'item_default': True },
- { 'item_name': 'test_str1', 'item_type': 'string', 'item_default': 'ABCD' },
- { 'item_name': 'test_list1', 'item_type': 'list', 'item_default': [1,2,3],
- 'list_item_spec' : { 'item_name': 'number', 'item_type': 'integer' } },
- { 'item_name': 'test_map1', 'item_type': 'map', 'item_default': {'a':1,'b':2,'c':3},
- 'map_item_spec' : [ { 'item_name': 'a', 'item_type': 'integer'},
- { 'item_name': 'b', 'item_type': 'integer'},
- { 'item_name': 'c', 'item_type': 'integer'} ] },
- { 'item_name': 'test_int2', 'item_type': 'integer' },
- { 'item_name': 'test_real2', 'item_type': 'real' },
- { 'item_name': 'test_bool2', 'item_type': 'boolean' },
- { 'item_name': 'test_str2', 'item_type': 'string' },
- { 'item_name': 'test_list2', 'item_type': 'list',
- 'list_item_spec' : { 'item_name': 'number', 'item_type': 'integer' } },
- { 'item_name': 'test_map2', 'item_type': 'map',
- 'map_item_spec' : [ { 'item_name': 'A', 'item_type': 'integer'},
- { 'item_name': 'B', 'item_type': 'integer'},
- { 'item_name': 'C', 'item_type': 'integer'} ] },
- { 'item_name': 'test_none', 'item_type': 'none' },
- { 'item_name': 'test_list3', 'item_type': 'list', 'item_default': ["one","two","three"],
- 'list_item_spec' : { 'item_name': 'number', 'item_type': 'string' } },
- { 'item_name': 'test_map3', 'item_type': 'map', 'item_default': {'a':'one','b':'two','c':'three'},
- 'map_item_spec' : [ { 'item_name': 'a', 'item_type': 'string'},
- { 'item_name': 'b', 'item_type': 'string'},
- { 'item_name': 'c', 'item_type': 'string'} ] },
- {
- 'item_name': 'test_named_set',
- 'item_type': 'named_set',
- 'item_default': { },
- 'named_set_item_spec': {
- 'item_name': 'name',
- 'item_type': 'map',
- 'item_default': { },
- 'map_item_spec': [
- {
- 'item_name': 'number1',
- 'item_type': 'integer'
- },
- {
- 'item_name': 'number2',
- 'item_type': 'integer'
- }
- ]
- }
- }
- ]
-
- def setUp(self):
- self.const_timestamp = 1308730448.965706
- self.const_timetuple = (2011, 6, 22, 8, 14, 8, 2, 173, 0)
- self.const_datetime = '2011-06-22T08:14:08Z'
- self.__orig_time = stats.time
- self.__orig_gmtime = stats.gmtime
- stats.time = lambda : self.const_timestamp
- stats.gmtime = lambda : self.const_timetuple
-
- def tearDown(self):
- stats.time = self.__orig_time
- stats.gmtime = self.__orig_gmtime
-
- def test_get_spec_defaults(self):
- self.assertEqual(
- stats.get_spec_defaults(self.items), {
- 'test_int1' : 12345 ,
- 'test_real1' : 12345.6789 ,
- 'test_bool1' : True ,
- 'test_str1' : 'ABCD' ,
- 'test_list1' : [1,2,3] ,
- 'test_map1' : {'a':1,'b':2,'c':3},
- 'test_int2' : 0 ,
- 'test_real2' : 0.0,
- 'test_bool2' : False,
- 'test_str2' : "",
- 'test_list2' : [0],
- 'test_map2' : { 'A' : 0, 'B' : 0, 'C' : 0 },
- 'test_none' : None,
- 'test_list3' : [ "one", "two", "three" ],
- 'test_map3' : { 'a' : 'one', 'b' : 'two', 'c' : 'three' },
- 'test_named_set' : {} })
- self.assertEqual(stats.get_spec_defaults(None), {})
- self.assertRaises(KeyError, stats.get_spec_defaults, [{'item_name':'Foo'}])
-
- def test_get_timestamp(self):
- self.assertEqual(stats.get_timestamp(), self.const_timestamp)
-
- def test_get_datetime(self):
- self.assertEqual(stats.get_datetime(), self.const_datetime)
- self.assertNotEqual(stats.get_datetime(
- (2011, 6, 22, 8, 23, 40, 2, 173, 0)), self.const_datetime)
-
- def test__accum(self):
- self.assertEqual(stats._accum(None, None), None)
- self.assertEqual(stats._accum(None, "b"), "b")
- self.assertEqual(stats._accum("a", None), "a")
- self.assertEqual(stats._accum(1, 2), 3)
- self.assertEqual(stats._accum(0.5, 0.3), 0.8)
- self.assertEqual(stats._accum(1, 0.3), 1.3)
- self.assertEqual(stats._accum(0.5, 2), 2.5)
- self.assertEqual(stats._accum('aa','bb'), 'bb')
- self.assertEqual(stats._accum('1970-01-01T09:00:00Z','2012-08-09T09:33:31Z'),
- '2012-08-09T09:33:31Z')
- self.assertEqual(stats._accum(
- [1, 2, 3], [4, 5]), [5, 7, 3])
- self.assertEqual(stats._accum(
- [4, 5], [1, 2, 3]), [5, 7, 3])
- self.assertEqual(stats._accum(
- [1, 2, 3], [None, 5, 6]), [1, 7, 9])
- self.assertEqual(stats._accum(
- [None, 5, 6], [1, 2, 3]), [1, 7, 9])
- self.assertEqual(stats._accum(
- [1, 2, 3], [None, None, None, None]), [1,2,3,None])
- self.assertEqual(stats._accum(
- [[1,2],3],[[],5,6]), [[1,2],8,6])
- self.assertEqual(stats._accum(
- {'one': 1, 'two': 2, 'three': 3},
- {'one': 4, 'two': 5}),
- {'one': 5, 'two': 7, 'three': 3})
- self.assertEqual(stats._accum(
- {'one': 1, 'two': 2, 'three': 3},
- {'four': 4, 'five': 5}),
- {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5})
- self.assertEqual(stats._accum(
- {'one': [1, 2], 'two': [3, None, 5], 'three': [None, 3, None]},
- {'one': [2], 'two': [4, 5], 'three': [None, None, None], 'four': 'FOUR'}),
- {'one':[3,2], 'two':[7,5,5], 'three':[None,3,None], 'four': 'FOUR'})
- self.assertEqual(stats._accum(
- [ {'one': 1, 'two': 2, 'three': 3}, {'four': 4, 'five': 5, 'six': 6} ],
- [ {}, {'four': 1, 'five': 2, 'six': 3} ]),
- [ {'one': 1, 'two': 2, 'three': 3}, {'four': 5, 'five': 7, 'six': 9} ])
-
- def test_merge_oldnre(self):
- self.assertEqual(stats.merge_oldnew(1, 2), 2)
- self.assertEqual(stats.merge_oldnew(0.5, 0.3), 0.3)
- self.assertEqual(stats.merge_oldnew('aa','bb'), 'bb')
- self.assertEqual(stats.merge_oldnew(
- [1, 2, 3], [4, 5]), [4, 5, 3])
- self.assertEqual(stats.merge_oldnew(
- [4, 5], [1, 2, 3]), [1, 2, 3])
- self.assertEqual(stats.merge_oldnew(
- [1, 2, 3], [None, 5, 6]), [None, 5, 6])
- self.assertEqual(stats.merge_oldnew(
- [None, 5, 6], [1, 2, 3]), [1, 2, 3])
- self.assertEqual(stats.merge_oldnew(
- [1, 2, 3], [None, None, None, None]), [None, None, None, None])
- self.assertEqual(stats.merge_oldnew(
- [[1,2],3],[[],5,6]), [[1,2],5,6])
- self.assertEqual(stats.merge_oldnew(
- {'one': 1, 'two': 2, 'three': 3},
- {'one': 4, 'two': 5}),
- {'one': 4, 'two': 5, 'three': 3})
- self.assertEqual(stats.merge_oldnew(
- {'one': 1, 'two': 2, 'three': 3},
- {'four': 4, 'five': 5}),
- {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5})
- self.assertEqual(stats.merge_oldnew(
- {'one': [1, 2], 'two': [3, None, 5], 'three': [None, 3, None]},
- {'one': [2], 'two': [4, 5], 'three': [None, None, None], 'four': 'FOUR'}),
- {'one':[2,2], 'two':[4,5,5], 'three':[None,None,None], 'four': 'FOUR'})
- self.assertEqual(stats.merge_oldnew(
- [ {'one': 1, 'two': 2, 'three': 3}, {'four': 4, 'five': 5, 'six': 6} ],
- [ {}, {'four': 1, 'five': 2, 'six': 3} ]),
- [ {'one': 1, 'two': 2, 'three': 3}, {'four': 1, 'five': 2, 'six': 3} ])
-
-class TestCallback(unittest.TestCase):
- def setUp(self):
- self.dummy_func = lambda *x, **y : (x, y)
- self.dummy_args = (1,2,3)
- self.dummy_kwargs = {'a':1,'b':2,'c':3}
- self.cback1 = stats.Callback(
- command=self.dummy_func,
- args=self.dummy_args,
- kwargs=self.dummy_kwargs
- )
- self.cback2 = stats.Callback(
- args=self.dummy_args,
- kwargs=self.dummy_kwargs
- )
- self.cback3 = stats.Callback(
- command=self.dummy_func,
- kwargs=self.dummy_kwargs
- )
- self.cback4 = stats.Callback(
- command=self.dummy_func,
- args=self.dummy_args
- )
-
- def test_init(self):
- self.assertEqual((self.cback1.command, self.cback1.args, self.cback1.kwargs),
- (self.dummy_func, self.dummy_args, self.dummy_kwargs))
- self.assertEqual((self.cback2.command, self.cback2.args, self.cback2.kwargs),
- (None, self.dummy_args, self.dummy_kwargs))
- self.assertEqual((self.cback3.command, self.cback3.args, self.cback3.kwargs),
- (self.dummy_func, (), self.dummy_kwargs))
- self.assertEqual((self.cback4.command, self.cback4.args, self.cback4.kwargs),
- (self.dummy_func, self.dummy_args, {}))
-
- def test_call(self):
- self.assertEqual(self.cback1(), (self.dummy_args, self.dummy_kwargs))
- self.assertEqual(self.cback1(100, 200), ((100, 200), self.dummy_kwargs))
- self.assertEqual(self.cback1(a=100, b=200), (self.dummy_args, {'a':100, 'b':200}))
- self.assertEqual(self.cback2(), None)
- self.assertEqual(self.cback3(), ((), self.dummy_kwargs))
- self.assertEqual(self.cback3(100, 200), ((100, 200), self.dummy_kwargs))
- self.assertEqual(self.cback3(a=100, b=200), ((), {'a':100, 'b':200}))
- self.assertEqual(self.cback4(), (self.dummy_args, {}))
- self.assertEqual(self.cback4(100, 200), ((100, 200), {}))
- self.assertEqual(self.cback4(a=100, b=200), (self.dummy_args, {'a':100, 'b':200}))
-
-class TestStats(unittest.TestCase):
- def setUp(self):
- # set the signal handler for deadlock
- self.const_timestamp = 1308730448.965706
- self.const_datetime = '2011-06-22T08:14:08Z'
- self.const_default_datetime = '1970-01-01T00:00:00Z'
- # Record original module-defined functions in case we replace them
- self.__orig_timestamp = stats.get_timestamp
- self.__orig_get_datetime = stats.get_datetime
-
- def tearDown(self):
- # restore the stored original function in case we replaced them
- stats.get_timestamp = self.__orig_timestamp
- stats.get_datetime = self.__orig_get_datetime
-
- def test_init(self):
- self.stats = MyStats()
- self.assertEqual(self.stats.module_name, 'Stats')
- self.assertFalse(self.stats.running)
- self.assertTrue('command_show' in self.stats.callbacks)
- self.assertTrue('command_status' in self.stats.callbacks)
- self.assertTrue('command_shutdown' in self.stats.callbacks)
- self.assertTrue('command_show' in self.stats.callbacks)
- self.assertTrue('command_showschema' in self.stats.callbacks)
- self.assertEqual(self.stats.config['poll-interval'], 60)
-
- def test_init_undefcmd(self):
- spec_str = """\
-{
- "module_spec": {
- "module_name": "Stats",
- "module_description": "Stats daemon",
- "config_data": [],
- "commands": [
- {
- "command_name": "_undef_command_",
- "command_description": "a undefined command in stats",
- "command_args": []
- }
- ],
- "statistics": []
- }
-}
-"""
- orig_spec_location = stats.SPECFILE_LOCATION
- stats.SPECFILE_LOCATION = io.StringIO(spec_str)
- self.assertRaises(stats.StatsError, MyStats)
- stats.SPECFILE_LOCATION = orig_spec_location
-
- def __send_command(self, stats, command_name, params=None):
- '''Emulate a command arriving to stats by directly calling callback'''
- return isc.config.ccsession.parse_answer(
- stats.command_handler(command_name, params))
-
- def test_check_command(self):
- """Test _check_command sets proper timeout values and sets proper values
- returned from group_recvmsg"""
- stat = MyStats()
- set_timeouts = []
- orig_timeout = 1
- stat.cc_session.get_timeout = lambda: orig_timeout
- stat.cc_session.set_timeout = lambda x: set_timeouts.append(x)
- msg = create_answer(0, 'msg')
- env = {'from': 'frominit'}
- stat._answers = [(msg, env)]
- stat._check_command()
- self.assertListEqual([500, orig_timeout], set_timeouts)
- self.assertEqual(msg, stat.mccs._msg)
- self.assertEqual(env, stat.mccs._env)
-
- def test_check_command_sessiontimeout(self):
- """Test _check_command doesn't perform but sets proper timeout values in
- case that a SesstionTimeout exception is caught while doing
- group_recvmsg()"""
- stat = MyStats()
- set_timeouts = []
- orig_timeout = 1
- stat.cc_session.get_timeout = lambda: orig_timeout
- stat.cc_session.set_timeout = lambda x: set_timeouts.append(x)
- msg = create_answer(0, 'msg')
- env = {'from': 'frominit'}
- stat._answers = [(msg, env)]
- # SessionTimeout is raised while doing group_recvmsg()
- ex = isc.cc.session.SessionTimeout
- def __raise(*x): raise ex(*x)
- stat.cc_session.group_recvmsg = lambda x: __raise()
- stat._check_command()
- self.assertListEqual([500, orig_timeout], set_timeouts)
- self.assertEqual(None, stat.mccs._msg)
- self.assertEqual(None, stat.mccs._env)
-
- def test_start(self):
- # Define a separate exception class so we can be sure that's actually
- # the one raised in __check_start() below
- class CheckException(Exception):
- pass
-
- def __check_start(tested_stats):
- self.assertTrue(tested_stats.running)
- raise CheckException # terminate the loop
-
- # start without err
- self.stats = MyStats()
- self.assertFalse(self.stats.running)
- self.stats._check_command = lambda: __check_start(self.stats)
- # We are going to confirm start() will set running to True, avoiding
- # to fall into a loop with the exception trick.
- self.assertRaises(CheckException, self.stats.start)
- self.assertEqual(self.__send_command(self.stats, "status"),
- (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
- self.assertTrue(self.stats.mccs.stopped)
-
- def test_start_set_next_polltime(self):
- """Test start() properly sets the time next_polltime to do_poll() next
- time"""
- orig_get_timestamp = stats.get_timestamp
- stats.get_timestamp = lambda : self.const_timestamp
- stat = MyStats()
- # manupilate next_polltime to go it through the inner if-condition
- stat.next_polltime = self.const_timestamp - stat.get_interval() - 1
- # stop an infinity loop at once
- def __stop_running(): stat.running = False
- stat.do_polling = __stop_running
- # do nothing in _check_command()
- stat._check_command = lambda: None
- stat.start()
- # check stat.next_polltime reassigned
- self.assertEqual(self.const_timestamp, stat.next_polltime)
- stats.get_timestamp = orig_get_timestamp
-
- def test_shutdown(self):
- def __check_shutdown(tested_stats):
- self.assertTrue(tested_stats.running)
- self.assertEqual(self.__send_command(tested_stats, "shutdown"),
- (0, None))
- self.assertFalse(tested_stats.running)
- # override get_interval() so it won't go poll statistics
- tested_stats.get_interval = lambda : 0
-
- self.stats = MyStats()
- self.stats._check_command = lambda: __check_shutdown(self.stats)
- self.stats.start()
- self.assertTrue(self.stats.mccs.stopped)
-
- def test_handlers(self):
- """Test command_handler"""
-
- __stats = MyStats()
-
- # 'show' command. We're going to check the expected methods are
- # called in the expected order, and check the resulting response.
- # Details of each method are tested separately.
- call_log = []
- def __steal_method(fn_name, *arg):
- call_log.append((fn_name, arg))
- if fn_name == 'update_stat':
- return False # "no error"
- if fn_name == 'showschema':
- return isc.config.create_answer(0, 'no error')
-
- # Fake some methods and attributes for inspection
- __stats.do_polling = lambda: __steal_method('polling')
- __stats.update_statistics_data = \
- lambda x, y, z: __steal_method('update_stat', x, y, z)
- __stats.update_modules = lambda: __steal_method('update_module')
- __stats.mccs.lname = 'test lname'
- __stats.statistics_data = {'Init': {'boot_time': self.const_datetime}}
-
- # skip initial polling
- stats.get_timestamp = lambda: 0
- __stats._lasttime_poll = 0
-
- stats.get_datetime = lambda: 42 # make the result predictable
-
- # now send the command
- self.assertEqual(
- self.__send_command(
- __stats, 'show',
- params={ 'owner' : 'Init', 'name' : 'boot_time' }),
- (0, {'Init': {'boot_time': self.const_datetime}}))
- # Check if expected methods are called
- self.assertEqual([('update_stat',
- ('Stats', 'test lname',
- {'timestamp': 0,
- 'report_time': 42})),
- ('update_module', ())], call_log)
-
- # Then update faked timestamp so the initial polling will happen, and
- # confirm that.
- call_log = []
- stats.get_timestamp = lambda: 10
- self.assertEqual(
- self.__send_command(
- __stats, 'show',
- params={ 'owner' : 'Init', 'name' : 'boot_time' }),
- (0, {'Init': {'boot_time': self.const_datetime}}))
- self.assertEqual([('polling', ()),
- ('update_stat',
- ('Stats', 'test lname',
- {'timestamp': 10,
- 'report_time': 42})),
- ('update_module', ())], call_log)
-
- # 'status' command. We can confirm the behavior without any fake
- self.assertEqual(
- self.__send_command(__stats, 'status'),
- (0, "Stats is up. (PID " + str(os.getpid()) + ")"))
-
- # 'showschema' command. update_modules() will be called, which
- # (implicitly) confirms the correct method is called; further details
- # are tested separately.
- call_log = []
- (rcode, value) = self.__send_command(__stats, 'showschema')
- self.assertEqual([('update_module', ())], call_log)
-
- # Unknown command. Error should be returned
- self.assertEqual(
- self.__send_command(__stats, '__UNKNOWN__'),
- (1, "Unknown command: '__UNKNOWN__'"))
-
- def test_update_modules(self):
- """Confirm the behavior of Stats.update_modules().
-
- It checks whether the expected command is sent to ConfigManager,
- and whether the answer from ConfigManager is handled as expected.
-
- """
-
- def __check_rpc_call(command, group):
- self.assertEqual('ConfigManager', group)
- self.assertEqual(command,
- isc.config.ccsession.COMMAND_GET_STATISTICS_SPEC)
- answer_value = {'Init': [{
- "item_name": "boot_time",
- "item_type": "string",
- "item_optional": False,
- # Use a different default so we can check it below
- "item_default": "2013-01-01T00:00:01Z",
- "item_title": "Boot time",
- "item_description": "dummy desc",
- "item_format": "date-time"
- }]}
- return answer_value
-
- self.stats = MyStats()
- self.stats.cc_session.rpc_call = __check_rpc_call
-
- self.stats.update_modules()
-
- # Stats is always incorporated. For others, only the ones returned
- # by group_recvmsg() above is available.
- self.assertTrue('Stats' in self.stats.modules)
- self.assertTrue('Init' in self.stats.modules)
- self.assertFalse('Dummy' in self.stats.modules)
-
- my_statistics_data = stats.get_spec_defaults(
- self.stats.modules['Stats'].get_statistics_spec())
- self.assertTrue('report_time' in my_statistics_data)
- self.assertTrue('boot_time' in my_statistics_data)
- self.assertTrue('last_update_time' in my_statistics_data)
- self.assertTrue('timestamp' in my_statistics_data)
- self.assertTrue('lname' in my_statistics_data)
- self.assertEqual(my_statistics_data['report_time'],
- self.const_default_datetime)
- self.assertEqual(my_statistics_data['boot_time'],
- self.const_default_datetime)
- self.assertEqual(my_statistics_data['last_update_time'],
- self.const_default_datetime)
- self.assertEqual(my_statistics_data['timestamp'], 0.0)
- self.assertEqual(my_statistics_data['lname'], "")
- my_statistics_data = stats.get_spec_defaults(
- self.stats.modules['Init'].get_statistics_spec())
- self.assertTrue('boot_time' in my_statistics_data)
- self.assertEqual(my_statistics_data['boot_time'],
- "2013-01-01T00:00:01Z")
-
- # Error case
- def __raise_on_rpc_call(x, y):
- raise isc.config.RPCError(99, 'error')
- orig_parse_answer = stats.isc.config.ccsession.parse_answer
- self.stats.cc_session.rpc_call = __raise_on_rpc_call
- self.assertRaises(stats.StatsError, self.stats.update_modules)
-
- def test_get_statistics_data(self):
- """Confirm the behavior of Stats.get_statistics_data().
-
- It should first call update_modules(), and then retrieve the requested
- data from statistics_data. We confirm this by fake update_modules()
- where we set the expected data in statistics_data.
-
- """
- self.stats = MyStats()
- def __faked_update_modules():
- self.stats.statistics_data = { \
- 'Stats': {
- 'report_time': self.const_default_datetime,
- 'boot_time': None,
- 'last_update_time': None,
- 'timestamp': 0.0,
- 'lname': 'dummy name'
- },
- 'Init': { 'boot_time': None }
- }
-
- self.stats.update_modules = __faked_update_modules
-
- my_statistics_data = self.stats.get_statistics_data()
- self.assertTrue('Stats' in my_statistics_data)
- self.assertTrue('Init' in my_statistics_data)
- self.assertTrue('boot_time' in my_statistics_data['Init'])
-
- my_statistics_data = self.stats.get_statistics_data(owner='Stats')
- self.assertTrue('Stats' in my_statistics_data)
- self.assertTrue('report_time' in my_statistics_data['Stats'])
- self.assertTrue('boot_time' in my_statistics_data['Stats'])
- self.assertTrue('last_update_time' in my_statistics_data['Stats'])
- self.assertTrue('timestamp' in my_statistics_data['Stats'])
- self.assertTrue('lname' in my_statistics_data['Stats'])
- self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
- owner='Foo')
-
- my_statistics_data = self.stats.get_statistics_data(
- owner='Stats', name='report_time')
- self.assertEqual(my_statistics_data['Stats']['report_time'],
- self.const_default_datetime)
-
- my_statistics_data = self.stats.get_statistics_data(
- owner='Stats', name='boot_time')
- self.assertTrue('boot_time' in my_statistics_data['Stats'])
-
- my_statistics_data = self.stats.get_statistics_data(
- owner='Stats', name='last_update_time')
- self.assertTrue('last_update_time' in my_statistics_data['Stats'])
-
- my_statistics_data = self.stats.get_statistics_data(
- owner='Stats', name='timestamp')
- self.assertEqual(my_statistics_data['Stats']['timestamp'], 0.0)
-
- my_statistics_data = self.stats.get_statistics_data(
- owner='Stats', name='lname')
- self.assertTrue(len(my_statistics_data['Stats']['lname']) >0)
- self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
- owner='Stats', name='Bar')
- self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
- owner='Foo', name='Bar')
- self.assertRaises(stats.StatsError, self.stats.get_statistics_data,
- name='Bar')
-
- def test_update_statistics_data(self):
- """test for list-type statistics"""
- self.stats = MyStats()
- _test_exp1 = {
- 'zonename': 'test1.example',
- 'queries.tcp': 5,
- 'queries.udp': 4
- }
- _test_exp2 = {
- 'zonename': 'test2.example',
- 'queries.tcp': 3,
- 'queries.udp': 2
- }
- _test_exp3 = {}
- _test_exp4 = {
- 'queries.udp': 4
- }
- _test_exp5_1 = {
- 'queries.perzone': [
- { },
- {
- 'queries.udp': 9876
- }
- ]
- }
- _test_exp5_2 = {
- 'queries.perzone[1]/queries.udp':
- isc.cc.data.find(_test_exp5_1,
- 'queries.perzone[1]/queries.udp')
- }
- # Success cases
- self.assertEqual(self.stats.statistics_data['Stats']['lname'],
- self.stats.cc_session.lname)
- self.stats.update_statistics_data(
- 'Stats', self.stats.cc_session.lname,
- {'lname': 'foo@bar'})
- self.assertEqual(self.stats.statistics_data['Stats']['lname'],
- 'foo@bar')
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'queries.perzone': [_test_exp1]}))
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['queries.perzone'],\
- [_test_exp1])
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'queries.perzone': [_test_exp2]}))
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['queries.perzone'],\
- [_test_exp2])
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'queries.perzone': [_test_exp1,_test_exp2]}))
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['queries.perzone'],
- [_test_exp1,_test_exp2])
- # differential update
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'queries.perzone': [_test_exp3,_test_exp4]}))
- _new_data = stats.merge_oldnew(_test_exp2,_test_exp4)
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['queries.perzone'], \
- [_test_exp1,_new_data])
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', _test_exp5_2))
- _new_data = stats.merge_oldnew(_new_data,
- _test_exp5_1['queries.perzone'][1])
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['queries.perzone'], \
- [_test_exp1,_new_data])
- # Error cases
- self.assertEqual(self.stats.update_statistics_data('Stats', None,
- {'lname': 0.0}),
- ['0.0 should be a string'])
- self.assertEqual(self.stats.update_statistics_data('Dummy', None,
- {'foo': 'bar'}),
- ['unknown module name: Dummy'])
- self.assertEqual(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'queries.perzone': [None]}), ['None should be a map'])
-
- def test_update_statistics_data_pt2(self):
- """test for named_set-type statistics"""
- self.stats = MyStats()
- _test_exp1 = \
- { 'test10.example': { 'queries.tcp': 5, 'queries.udp': 4 } }
- _test_exp2 = \
- { 'test20.example': { 'queries.tcp': 3, 'queries.udp': 2 } }
- _test_exp3 = {}
- _test_exp4 = { 'test20.example': { 'queries.udp': 4 } }
- _test_exp5_1 = { 'test10.example': { 'queries.udp': 5432 } }
- _test_exp5_2 ={
- 'nds_queries.perzone/test10.example/queries.udp':
- isc.cc.data.find(_test_exp5_1, 'test10.example/queries.udp')
- }
- _test_exp6 = { 'foo/bar': 'brabra' }
- _test_exp7 = { 'foo[100]': 'bar' }
- # Success cases
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'nds_queries.perzone': _test_exp1}))
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['nds_queries.perzone'],\
- _test_exp1)
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'nds_queries.perzone': _test_exp2}))
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['nds_queries.perzone'],\
- dict(_test_exp1,**_test_exp2))
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'nds_queries.perzone':
- dict(_test_exp1, **_test_exp2)}))
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['nds_queries.perzone'],
- dict(_test_exp1, **_test_exp2))
- # differential update
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'nds_queries.perzone':
- dict(_test_exp3, **_test_exp4)}))
- _new_val = dict(_test_exp1,
- **stats.merge_oldnew(_test_exp2,_test_exp4))
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['nds_queries.perzone'],\
- _new_val)
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo1', _test_exp5_2))
- _new_val = stats.merge_oldnew(_new_val, _test_exp5_1)
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['nds_queries.perzone'],\
- _new_val)
- self.assertIsNone(self.stats.update_statistics_data(
- 'Auth', 'foo2', _test_exp5_2))
- _new_val = stats.merge_oldnew(_new_val, _test_exp5_1)
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo2']['nds_queries.perzone'],\
- _test_exp5_1)
- # Error cases
- self.assertEqual(self.stats.update_statistics_data(
- 'Auth', 'foo1', {'nds_queries.perzone': None}),
- ['None should be a map'])
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['nds_queries.perzone'],\
- _new_val)
- self.assertEqual(self.stats.update_statistics_data(
- 'Auth', 'foo1', _test_exp6), ['unknown item foo'])
- self.assertEqual(self.stats.statistics_data_bymid['Auth']\
- ['foo1']['nds_queries.perzone'],\
- _new_val)
- self.assertEqual(self.stats.update_statistics_data(
- 'Init', 'bar1', _test_exp7), ["KeyError: 'foo'"])
- self.assertEqual(self.stats.update_statistics_data(
- 'Foo', 'foo1', _test_exp6), ['unknown module name: Foo'])
-
- def test_update_statistics_data_withmid(self):
- self.stats = MyStats()
-
- # This test relies on existing statistics data at the Stats object.
- # This version of test prepares the data using the do_polling() method;
- # that's a bad practice because a unittest for a method
- # (update_statistics_data) would heavily depend on details of another
- # method (do_polling). However, there's currently no direct test
- # for do_polling (which is also bad), so we still keep that approach,
- # partly for testing do_polling indirectly. #2781 should provide
- # direct test for do_polling, with which this test scenario should
- # also be changed to be more stand-alone.
-
- # We use the knowledge of what kind of messages are sent via
- # do_polling, and return the following faked answer directly.
- self.stats._answers = [
- # Answer for "show_processes"
- (create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
- [1035, 'b10-auth-2', 'Auth']]), None),
- # Answers for "getstats". 2 for Auth instances and 1 for Init.
- # we return some bogus values for Init, but the rest of the test
- # doesn't need it, so it's okay.
- (create_answer(0, self.stats._auth_sdata), {'from': 'auth1'}),
- (create_answer(0, self.stats._auth_sdata), {'from': 'auth2'}),
- (create_answer(0, self.stats._auth_sdata), {'from': 'auth3'})
- ]
- # do_polling calls update_modules internally; in our scenario there's
- # no change in modules, so we make it no-op.
- self.stats.update_modules = lambda: None
- # Now call do_polling.
- self.stats.do_polling()
-
- # samples of query number
- bar1_tcp = 1001
- bar2_tcp = 2001
- bar3_tcp = 1002
- bar3_udp = 1003
- # two auth instances invoked, so we double the pre-set stat values
- sum_qtcp = self.stats._queries_tcp * 2
- sum_qudp = self.stats._queries_udp * 2
- self.stats.update_statistics_data('Auth', "bar1@foo",
- {'queries.tcp': bar1_tcp})
- self.assertTrue('Auth' in self.stats.statistics_data)
- self.assertTrue('queries.tcp' in self.stats.statistics_data['Auth'])
- self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
- bar1_tcp + sum_qtcp)
- self.assertTrue('Auth' in self.stats.statistics_data_bymid)
- self.assertTrue('bar1@foo' in self.stats.statistics_data_bymid['Auth'])
- self.assertTrue('queries.tcp' in self.stats.statistics_data_bymid
- ['Auth']['bar1@foo'])
- self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar1@foo'],
- {'queries.tcp': bar1_tcp})
- # check consolidation of statistics data even if there is
- # non-existent mid of Auth
- self.stats.update_statistics_data('Auth', "bar2@foo",
- {'queries.tcp': bar2_tcp})
- self.assertTrue('Auth' in self.stats.statistics_data)
- self.assertTrue('queries.tcp' in self.stats.statistics_data['Auth'])
- self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
- bar1_tcp + bar2_tcp + sum_qtcp)
- self.assertTrue('Auth' in self.stats.statistics_data_bymid)
- self.assertTrue('bar1@foo' in self.stats.statistics_data_bymid['Auth'])
- self.assertTrue('queries.tcp' in self.stats.statistics_data_bymid['Auth']['bar1@foo'])
- self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar1@foo'],
- {'queries.tcp': bar1_tcp})
- self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar2@foo'],
- {'queries.tcp': bar2_tcp})
- # kill running Auth but the statistics data doesn't change
- self.stats.update_statistics_data()
- self.assertTrue('Auth' in self.stats.statistics_data)
- self.assertTrue('queries.tcp' in self.stats.statistics_data['Auth'])
- self.assertTrue('queries.udp' in self.stats.statistics_data['Auth'])
- self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
- bar1_tcp + bar2_tcp + sum_qtcp)
- self.assertEqual(self.stats.statistics_data['Auth']['queries.udp'],
- sum_qudp)
- self.assertTrue('Auth' in self.stats.statistics_data_bymid)
- # restore statistics data of killed auth
- self.stats.update_statistics_data('Auth',
- "bar1@foo",
- {'queries.tcp': bar1_tcp})
- # set another mid of Auth
- self.stats.update_statistics_data('Auth',
- "bar3@foo",
- {'queries.tcp':bar3_tcp,
- 'queries.udp':bar3_udp})
- self.assertTrue('Auth' in self.stats.statistics_data)
- self.assertTrue('queries.tcp' in self.stats.statistics_data['Auth'])
- self.assertTrue('queries.udp' in self.stats.statistics_data['Auth'])
- self.assertEqual(self.stats.statistics_data['Auth']['queries.tcp'],
- bar1_tcp + bar2_tcp + bar3_tcp + sum_qtcp)
- self.assertEqual(self.stats.statistics_data['Auth']['queries.udp'],
- bar3_udp + sum_qudp)
- self.assertTrue('Auth' in self.stats.statistics_data_bymid)
- self.assertTrue('bar1@foo' in self.stats.statistics_data_bymid['Auth'])
- self.assertTrue('bar3@foo' in self.stats.statistics_data_bymid['Auth'])
- self.assertTrue('queries.tcp' in self.stats.statistics_data_bymid['Auth']['bar1@foo'])
- self.assertTrue('queries.udp' in self.stats.statistics_data_bymid['Auth']['bar3@foo'])
- self.assertTrue('queries.udp' in self.stats.statistics_data_bymid['Auth']['bar3@foo'])
- self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar1@foo']['queries.tcp'], bar1_tcp)
- self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar3@foo']['queries.tcp'], bar3_tcp)
- self.assertEqual(self.stats.statistics_data_bymid['Auth']['bar3@foo']['queries.udp'], bar3_udp)
-
- def test_config(self):
- orig_get_timestamp = stats.get_timestamp
- stats.get_timestamp = lambda : self.const_timestamp
- stat = MyStats()
-
- # test updating poll-interval
- self.assertEqual(stat.config['poll-interval'], 60)
- self.assertEqual(stat.get_interval(), 60)
- self.assertEqual(stat.next_polltime, self.const_timestamp + 60)
- self.assertEqual(stat.config_handler({'poll-interval': 120}),
- isc.config.create_answer(0))
- self.assertEqual(stat.config['poll-interval'], 120)
- self.assertEqual(stat.get_interval(), 120)
- self.assertEqual(stat.next_polltime, self.const_timestamp + 120)
- stats.get_timestamp = orig_get_timestamp
- self.assertEqual(stat.config_handler({'poll-interval': "foo"}),
- isc.config.create_answer(1, 'foo should be an integer'))
- self.assertEqual(stat.config_handler({'poll-interval': -1}),
- isc.config.create_answer(1, 'Negative integer ignored'))
- # unknown item
- self.assertEqual(
- stat.config_handler({'_UNKNOWN_KEY_': None}),
- isc.config.ccsession.create_answer(
- 1, "unknown item _UNKNOWN_KEY_"))
- # test no change if zero interval time
- self.assertEqual(stat.config_handler({'poll-interval': 0}),
- isc.config.create_answer(0))
- self.assertEqual(stat.config['poll-interval'], 0)
-
- # see the comment for test_update_statistics_data_withmid. We abuse
- # do_polling here, too. With #2781 we should make it more direct.
- stat._answers = [\
- # Answer for "show_processes"
- (create_answer(0, []), None),
- # Answers for "getstats" for Init (the other one for Auth, but
- # that doesn't matter for this test)
- (create_answer(0, stat._init_sdata), {'from': 'init'}),
- (create_answer(0, stat._init_sdata), {'from': 'init'})
- ]
- stat.update_modules = lambda: None
-
- self.assertEqual(
- self.__send_command(
- stat, 'show',
- params={ 'owner' : 'Init', 'name' : 'boot_time' }),
- (0, {'Init': {'boot_time': self.const_datetime}}))
-
- def test_commands(self):
- self.stats = MyStats()
-
- # status
- self.assertEqual(self.stats.command_status(),
- isc.config.create_answer(
- 0, "Stats is up. (PID " + str(os.getpid()) + ")"))
-
- # shutdown
- self.stats.running = True
- self.assertEqual(self.stats.command_shutdown(),
- isc.config.create_answer(0))
- self.assertFalse(self.stats.running)
-
- def test_command_show_error(self):
- self.stats = MyStats()
- self.assertEqual(self.stats.command_show(owner='Foo', name=None),
- isc.config.create_answer(
- 1,
- "specified arguments are incorrect: owner: Foo, name: None"))
- self.assertEqual(self.stats.command_show(owner='Foo', name='_bar_'),
- isc.config.create_answer(
- 1,
- "specified arguments are incorrect: owner: Foo, name: _bar_"))
- self.assertEqual(self.stats.command_show(owner='Foo', name='bar'),
- isc.config.create_answer(
- 1,
- "specified arguments are incorrect: owner: Foo, name: bar"))
-
- def test_command_show_auth(self):
- self.stats = MyStats()
- self.stats.update_modules = lambda: None
-
- # Test data borrowed from test_update_statistics_data_withmid
- self.stats._answers = [
- (create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
- [1035, 'b10-auth-2', 'Auth']]), None),
- (create_answer(0, self.stats._auth_sdata), {'from': 'auth1'}),
- (create_answer(0, self.stats._auth_sdata), {'from': 'auth2'}),
- (create_answer(0, self.stats._auth_sdata), {'from': 'auth3'})
- ]
-
- num_instances = 2
- sum_qtcp = 0
- sum_qudp = 0
- sum_qtcp_perzone1 = 0
- sum_qudp_perzone1 = 0
- sum_qtcp_perzone2 = 4 * num_instances
- sum_qudp_perzone2 = 3 * num_instances
- sum_qtcp_nds_perzone10 = 0
- sum_qudp_nds_perzone10 = 0
- sum_qtcp_nds_perzone20 = 4 * num_instances
- sum_qudp_nds_perzone20 = 3 * num_instances
-
- self.maxDiff = None
- for a in (0, num_instances):
- sum_qtcp += self.stats._queries_tcp
- sum_qudp += self.stats._queries_udp
- sum_qtcp_perzone1 += self.stats._queries_per_zone[0]['queries.tcp']
- sum_qudp_perzone1 += self.stats._queries_per_zone[0]['queries.udp']
- sum_qtcp_nds_perzone10 += \
- self.stats._nds_queries_per_zone['test10.example']['queries.tcp']
- sum_qudp_nds_perzone10 += \
- self.stats._nds_queries_per_zone['test10.example']['queries.udp']
-
- self.assertEqual(self.stats.command_show(owner='Auth'),
- isc.config.create_answer(
- 0, {'Auth':{ 'queries.udp': sum_qudp,
- 'queries.tcp': sum_qtcp,
- 'queries.perzone': [{ 'zonename': 'test1.example',
- 'queries.udp': sum_qudp_perzone1,
- 'queries.tcp': sum_qtcp_perzone1 },
- { 'zonename': 'test2.example',
- 'queries.udp': sum_qudp_perzone2,
- 'queries.tcp': sum_qtcp_perzone2 }
- ],
- 'nds_queries.perzone': { 'test10.example' : {
- 'queries.udp': sum_qudp_nds_perzone10,
- 'queries.tcp': sum_qtcp_nds_perzone10 },
- 'test20.example' : {
- 'queries.udp': sum_qudp_nds_perzone20,
- 'queries.tcp': sum_qtcp_nds_perzone20 }
- }}}))
- self.assertEqual(self.stats.command_show(owner='Auth', name='queries.udp'),
- isc.config.create_answer(
- 0, {'Auth': {'queries.udp': sum_qudp}}))
- self.assertEqual(self.stats.command_show(owner='Auth', name='queries.perzone'),
- isc.config.create_answer(
- 0, {'Auth': {'queries.perzone': [
- { 'zonename': 'test1.example',
- 'queries.udp': sum_qudp_perzone1,
- 'queries.tcp': sum_qtcp_perzone1 },
- { 'zonename': 'test2.example',
- 'queries.udp': sum_qudp_perzone2,
- 'queries.tcp': sum_qtcp_perzone2 }]}}))
- self.assertEqual(self.stats.command_show(owner='Auth', name='nds_queries.perzone'),
- isc.config.create_answer(
- 0, {'Auth': {'nds_queries.perzone': {
- 'test10.example': {
- 'queries.udp': sum_qudp_nds_perzone10,
- 'queries.tcp': sum_qtcp_nds_perzone10 },
- 'test20.example': {
- 'queries.udp': sum_qudp_nds_perzone20,
- 'queries.tcp': sum_qtcp_nds_perzone20 }}}}))
-
- def test_command_show_stats(self):
- self.stats = MyStats()
- orig_get_datetime = stats.get_datetime
- orig_get_timestamp = stats.get_timestamp
- stats.get_datetime = lambda x=None: self.const_datetime
- stats.get_timestamp = lambda : self.const_timestamp
- self.assertEqual(self.stats.command_show(owner='Stats',
- name='report_time'),
- isc.config.create_answer(
- 0, {'Stats': {'report_time':self.const_datetime}}))
- self.assertEqual(self.stats.command_show(owner='Stats',
- name='timestamp'),
- isc.config.create_answer(
- 0, {'Stats': {'timestamp':self.const_timestamp}}))
- stats.get_datetime = orig_get_datetime
- stats.get_timestamp = orig_get_timestamp
- self.stats.do_polling = lambda : None
- self.stats.modules[self.stats.module_name] = \
- isc.config.module_spec.ModuleSpec(
- { "module_name": self.stats.module_name, "statistics": [] } )
- self.assertRaises(
- stats.StatsError, self.stats.command_show,
- owner=self.stats.module_name, name='bar')
-
- def test_command_showchema(self):
- self.stats = MyStats()
- (rcode, value) = isc.config.ccsession.parse_answer(
- self.stats.command_showschema())
- self.assertEqual(rcode, 0)
- self.assertEqual(len(value), 3)
- self.assertTrue('Stats' in value)
- self.assertTrue('Init' in value)
- self.assertTrue('Auth' in value)
- self.assertFalse('__Dummy__' in value)
- schema = value['Stats']
- self.assertEqual(len(schema), 5)
- for item in schema:
- self.assertTrue(len(item) == 6 or len(item) == 7)
- self.assertTrue('item_name' in item)
- self.assertTrue('item_type' in item)
- self.assertTrue('item_optional' in item)
- self.assertTrue('item_default' in item)
- self.assertTrue('item_title' in item)
- self.assertTrue('item_description' in item)
- if len(item) == 7:
- self.assertTrue('item_format' in item)
-
- schema = value['Init']
- self.assertEqual(len(schema), 1)
- for item in schema:
- self.assertTrue(len(item) == 7)
- self.assertTrue('item_name' in item)
- self.assertTrue('item_type' in item)
- self.assertTrue('item_optional' in item)
- self.assertTrue('item_default' in item)
- self.assertTrue('item_title' in item)
- self.assertTrue('item_description' in item)
- self.assertTrue('item_format' in item)
-
- schema = value['Auth']
- self.assertEqual(len(schema), 4)
- for item in schema:
- if item['item_type'] == 'list' or item['item_type'] == 'named_set':
- self.assertEqual(len(item), 7)
- else:
- self.assertEqual(len(item), 6)
- self.assertTrue('item_name' in item)
- self.assertTrue('item_type' in item)
- self.assertTrue('item_optional' in item)
- self.assertTrue('item_default' in item)
- self.assertTrue('item_title' in item)
- self.assertTrue('item_description' in item)
-
- (rcode, value) = isc.config.ccsession.parse_answer(
- self.stats.command_showschema(owner='Stats'))
- self.assertEqual(rcode, 0)
- self.assertTrue('Stats' in value)
- self.assertFalse('Init' in value)
- self.assertFalse('Auth' in value)
- for item in value['Stats']:
- self.assertTrue(len(item) == 6 or len(item) == 7)
- self.assertTrue('item_name' in item)
- self.assertTrue('item_type' in item)
- self.assertTrue('item_optional' in item)
- self.assertTrue('item_default' in item)
- self.assertTrue('item_title' in item)
- self.assertTrue('item_description' in item)
- if len(item) == 7:
- self.assertTrue('item_format' in item)
-
- (rcode, value) = isc.config.ccsession.parse_answer(
- self.stats.command_showschema(owner='Stats', name='report_time'))
- self.assertEqual(rcode, 0)
- self.assertTrue('Stats' in value)
- self.assertFalse('Init' in value)
- self.assertFalse('Auth' in value)
- self.assertEqual(len(value['Stats'][0]), 7)
- self.assertTrue('item_name' in value['Stats'][0])
- self.assertTrue('item_type' in value['Stats'][0])
- self.assertTrue('item_optional' in value['Stats'][0])
- self.assertTrue('item_default' in value['Stats'][0])
- self.assertTrue('item_title' in value['Stats'][0])
- self.assertTrue('item_description' in value['Stats'][0])
- self.assertTrue('item_format' in value['Stats'][0])
- self.assertEqual(value['Stats'][0]['item_name'], 'report_time')
- self.assertEqual(value['Stats'][0]['item_format'], 'date-time')
-
- self.assertEqual(self.stats.command_showschema(owner='Foo'),
- isc.config.create_answer(
- 1, "specified arguments are incorrect: owner: Foo, name: None"))
- self.assertEqual(self.stats.command_showschema(owner='Foo', name='bar'),
- isc.config.create_answer(
- 1, "specified arguments are incorrect: owner: Foo, name: bar"))
- self.assertEqual(self.stats.command_showschema(owner='Auth'),
- isc.config.create_answer(
- 0, {'Auth': [{
- "item_default": 0,
- "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially",
- "item_name": "queries.tcp",
- "item_optional": False,
- "item_title": "Queries TCP",
- "item_type": "integer"
- },
- {
- "item_default": 0,
- "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially",
- "item_name": "queries.udp",
- "item_optional": False,
- "item_title": "Queries UDP",
- "item_type": "integer"
- },
- {
- "item_name": "queries.perzone",
- "item_type": "list",
- "item_optional": False,
- "item_default": [
- {
- "zonename" : "test1.example",
- "queries.udp" : 1,
- "queries.tcp" : 2
- },
- {
- "zonename" : "test2.example",
- "queries.udp" : 3,
- "queries.tcp" : 4
- }
- ],
- "item_title": "Queries per zone",
- "item_description": "Queries per zone",
- "list_item_spec": {
- "item_name": "zones",
- "item_type": "map",
- "item_optional": False,
- "item_default": {},
- "map_item_spec": [
- {
- "item_name": "zonename",
- "item_type": "string",
- "item_optional": False,
- "item_default": "",
- "item_title": "Zonename",
- "item_description": "Zonename"
- },
- {
- "item_name": "queries.udp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries UDP per zone",
- "item_description": "A number of UDP query counts per zone"
- },
- {
- "item_name": "queries.tcp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries TCP per zone",
- "item_description": "A number of TCP query counts per zone"
- }
- ]
- }
- },
- {
- "item_name": "nds_queries.perzone",
- "item_type": "named_set",
- "item_optional": False,
- "item_default": {
- "test10.example" : {
- "queries.udp" : 1,
- "queries.tcp" : 2
- },
- "test20.example" : {
- "queries.udp" : 3,
- "queries.tcp" : 4
- }
- },
- "item_title": "Queries per zone",
- "item_description": "Queries per zone",
- "named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
- "item_optional": False,
- "item_default": {},
- "item_title": "Zonename",
- "item_description": "Zonename",
- "map_item_spec": [
- {
- "item_name": "queries.udp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries UDP per zone",
- "item_description": "A number of UDP query counts per zone"
- },
- {
- "item_name": "queries.tcp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries TCP per zone",
- "item_description": "A number of TCP query counts per zone"
- }
- ]
- }
- }]}))
- self.assertEqual(self.stats.command_showschema(owner='Auth', name='queries.tcp'),
- isc.config.create_answer(
- 0, {'Auth': [{
- "item_default": 0,
- "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially",
- "item_name": "queries.tcp",
- "item_optional": False,
- "item_title": "Queries TCP",
- "item_type": "integer"
- }]}))
- self.assertEqual(self.stats.command_showschema(owner='Auth', name='queries.perzone'),
- isc.config.create_answer(
- 0, {'Auth':[{
- "item_name": "queries.perzone",
- "item_type": "list",
- "item_optional": False,
- "item_default": [
- {
- "zonename" : "test1.example",
- "queries.udp" : 1,
- "queries.tcp" : 2
- },
- {
- "zonename" : "test2.example",
- "queries.udp" : 3,
- "queries.tcp" : 4
- }
- ],
- "item_title": "Queries per zone",
- "item_description": "Queries per zone",
- "list_item_spec": {
- "item_name": "zones",
- "item_type": "map",
- "item_optional": False,
- "item_default": {},
- "map_item_spec": [
- {
- "item_name": "zonename",
- "item_type": "string",
- "item_optional": False,
- "item_default": "",
- "item_title": "Zonename",
- "item_description": "Zonename"
- },
- {
- "item_name": "queries.udp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries UDP per zone",
- "item_description": "A number of UDP query counts per zone"
- },
- {
- "item_name": "queries.tcp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries TCP per zone",
- "item_description": "A number of TCP query counts per zone"
- }
- ]
- }
- }]}))
- self.assertEqual(self.stats.command_showschema(owner='Auth', name='nds_queries.perzone'),
- isc.config.create_answer(
- 0, {'Auth':[{
- "item_name": "nds_queries.perzone",
- "item_type": "named_set",
- "item_optional": False,
- "item_default": {
- "test10.example" : {
- "queries.udp" : 1,
- "queries.tcp" : 2
- },
- "test20.example" : {
- "queries.udp" : 3,
- "queries.tcp" : 4
- }
- },
- "item_title": "Queries per zone",
- "item_description": "Queries per zone",
- "named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
- "item_optional": False,
- "item_default": {},
- "item_title": "Zonename",
- "item_description": "Zonename",
- "map_item_spec": [
- {
- "item_name": "queries.udp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries UDP per zone",
- "item_description": "A number of UDP query counts per zone"
- },
- {
- "item_name": "queries.tcp",
- "item_type": "integer",
- "item_optional": False,
- "item_default": 0,
- "item_title": "Queries TCP per zone",
- "item_description": "A number of TCP query counts per zone"
- }
- ]
- }
- }]}))
-
- self.assertEqual(self.stats.command_showschema(owner='Stats', name='bar'),
- isc.config.create_answer(
- 1, "specified arguments are incorrect: owner: Stats, name: bar"))
- self.assertEqual(self.stats.command_showschema(name='bar'),
- isc.config.create_answer(
- 1, "module name is not specified"))
-
- def test_get_multi_module_list(self):
- """Test _get_multi_module_list() returns a module list which is running
- as multiple modules."""
- stat = MyStats()
- # no answer
- self.assertListEqual([], stat._get_multi_module_list())
- # proc list returned
- proc_list = [
- [29317, 'b10-xfrout', 'Xfrout'],
- [29318, 'b10-xfrin', 'Xfrin'],
- [20061, 'b10-auth','Auth'],
- [20103, 'b10-auth-2', 'Auth']]
- mod_list = [ a[2] for a in proc_list ]
- stat._answers = [
- # Answer for "show_processes"
- (create_answer(0, proc_list), {'from': 'init'})
- ]
- self.assertListEqual(mod_list, stat._get_multi_module_list())
- # invalid proc list
- stat._answers = [
- # Answer for "show_processes"
- (create_answer(0, [[999, 'invalid', 'Invalid'], 'invalid']),
- {'from': 'init'})
- ]
- self.assertListEqual(['Invalid', None], stat._get_multi_module_list())
-
- def test_get_multi_module_list_rpcrecipientmissing(self):
- """Test _get_multi_module_list() raises an RPCRecipientMissing exception
- if rcp_call() raise the exception"""
- # RPCRecipientMissing case
- stat = MyStats()
- ex = isc.config.RPCRecipientMissing
- def __raise(*x): raise ex(*x)
- stat.mccs.rpc_call = lambda x,y: __raise('Error')
- self.assertRaises(ex, stat._get_multi_module_list)
-
- def test_get_multi_module_list_rpcerror(self):
- """Test _get_multi_module_list() returns an empty list if rcp_call()
- raise an RPCError exception"""
- # RPCError case
- stat = MyStats()
- ex = isc.config.RPCError
- def __raise(*x): raise ex(*x)
- stat.mccs.rpc_call = lambda x,y: __raise(99, 'Error')
- self.assertListEqual([], stat._get_multi_module_list())
-
- def test_get_multi_module_list_initsessiontimeout(self):
- """Test _get_multi_module_list() raises an InitSeeionTimeout exception
- if a CC session times out in rcp_call()"""
- # InitSeeionTimeout case
- stat = MyStats()
- ex = isc.cc.session.SessionTimeout
- def __raise(*x): raise ex(*x)
- stat.mccs.rpc_call = lambda x,y: __raise()
- self.assertRaises(stats.InitSessionTimeout, stat._get_multi_module_list)
-
- def test_query_statistics(self):
- """Test _query_statistics returns a list of pairs of module and
- sequences from group_sendmsg()"""
- stat = MyStats()
- # imitate stat.get_statistics_data().items. The order of the array
- # returned by items() should be preserved.
- class DummyDict:
- def items(self):
- return [('Init', 'dummy'), ('Stats', 'dummy'), ('Auth', 'dummy')]
- stat.get_statistics_data = lambda: DummyDict()
- mod = ('Init', 'Auth', 'Auth')
- seq = [('Init', stat._seq + 1),
- ('Auth', stat._seq + 2),
- ('Auth', stat._seq + 2) ]
- self.assertListEqual(seq, stat._query_statistics(mod))
-
- def test_collect_statistics(self):
- """Test _collect_statistics() collects statistics data from each module
- based on the sequences which is a list of values returned from
- group_sendmsg()"""
- stat = MyStats()
- seq = [ ('Init', stat._seq + 1),
- ('Auth', stat._seq + 2),
- ('Auth', stat._seq + 2) ]
- ret = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'}),
- ('Auth', 'fromauth1', {'queries.tcp': 100}),
- ('Auth', 'fromauth2', {'queries.udp': 200})]
- stat._answers = [
- (create_answer(0, r[2]), {'from': r[1]}) for r in ret
- ]
- self.assertListEqual(ret, stat._collect_statistics(seq))
-
- def test_collect_statistics_nodata(self):
- """Test _collect_statistics() returns empty statistics data if
- a module returns an empty list"""
- stat = MyStats()
- seq = []
- stat._answers = []
- ret = []
- self.assertListEqual(ret, stat._collect_statistics(seq))
-
- def test_collect_statistics_nonzero_rcode(self):
- """Test _collect_statistics() returns empty statistics data if
- a module returns non-zero rcode"""
- stat = MyStats()
- seq = [('Init', stat._seq + 1)]
- stat._answers = [
- (create_answer(1, 'error'), {'from': 'frominit'})
- ]
- ret = []
- self.assertListEqual(ret, stat._collect_statistics(seq))
-
- def test_collect_statistics_sessiontimeout(self):
- """Test _collect_statistics() collects statistics data from each module
- based on the sequences which is a list of values returned from
- group_sendmsg(). In this test case, SessionTimeout exceptions are raised
- while collecting from Auth. This tests _collect_statistics skips
- collecting from Auth."""
- # SessionTimeout case
- stat = MyStats()
- ex = isc.cc.session.SessionTimeout
- def __raise(*x): raise ex(*x)
- # SessionTimeout is raised when asking to Auth
- stat.cc_session.group_recvmsg = lambda x,seq: \
- __raise() if seq == stat._seq + 2 else stat._answers.pop(0)
- seq = [ ('Init', stat._seq + 1),
- ('Auth', stat._seq + 2),
- ('Auth', stat._seq + 2) ]
- ret = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'})]
- stat._answers = [
- (create_answer(0, r[2]), {'from': r[1]}) for r in ret
- ]
- self.assertListEqual(ret, stat._collect_statistics(seq))
-
- def test_refresh_statistics(self):
- """Test _refresh_statistics() refreshes statistics data from given data
- """
- stat = MyStats()
- self.assertEqual(self.const_default_datetime,
- stat.statistics_data['Init']['boot_time'])
- self.assertEqual(0,
- stat.statistics_data['Auth']['queries.tcp'])
- self.assertEqual(0,
- stat.statistics_data['Auth']['queries.udp'])
- # change stats.get_datetime() for testing 'last_update_time'
- orig_get_datetime = stats.get_datetime
- stats.get_datetime = lambda : self.const_datetime
- arg = [('Init', 'frominit', {'boot_time': '2013-01-01T00:00:00Z'}),
- ('Auth', 'fromauth1', {'queries.tcp': 100}),
- ('Auth', 'fromauth2', {'queries.udp': 200})]
- stat._refresh_statistics(arg)
- self.assertEqual('2013-01-01T00:00:00Z',
- stat.statistics_data['Init']['boot_time'])
- self.assertEqual(100,
- stat.statistics_data['Auth']['queries.tcp'])
- self.assertEqual(200,
- stat.statistics_data['Auth']['queries.udp'])
- self.assertEqual(self.const_datetime,
- stat.statistics_data['Stats']['last_update_time'])
- stats.get_datetime = orig_get_datetime
-
- def test_polling_init(self):
- """check statistics data of 'Init'."""
-
- stat = MyStats()
- # At this point 'stat' is initialized with statistics for Stats,
- # Init and Auth modules. In this test, we only need to check for Init
- # statistics, while do_polling() can ask for module statistics in an
- # unpredictable order (if hash randomization is enabled, which is
- # the case by default for Python 3.3). To make it predictable and
- # ensure the prepared answer doesn't cause disruption, we remove the
- # information for the Auth module for this test.
- del stat.statistics_data['Auth']
-
- stat.update_modules = lambda: None
-
- stat._answers = [
- # Answer for "show_processes"
- (create_answer(0, []), None),
- # Answers for "getstats" for Init (type of boot_time is invalid)
- (create_answer(0, {'boot_time': self.const_datetime}),
- {'from': 'init'}),
- ]
-
- stat.do_polling()
- self.assertEqual(
- stat.statistics_data_bymid['Init']['init'],
- {'boot_time': self.const_datetime})
-
- def test_polling_consolidate(self):
- """check statistics data of multiple instances of same module."""
- stat = MyStats()
- stat.update_modules = lambda: None
-
- # Test data borrowed from test_update_statistics_data_withmid
- stat._answers = [
- (create_answer(0, [[1034, 'b10-auth-1', 'Auth'],
- [1035, 'b10-auth-2', 'Auth']]), None),
- (create_answer(0, stat._auth_sdata), {'from': 'auth1'}),
- (create_answer(0, stat._auth_sdata), {'from': 'auth2'}),
- (create_answer(0, stat._auth_sdata), {'from': 'auth3'})
- ]
-
- stat.do_polling()
-
- # check statistics data of each 'Auth' instances. expected data
- # for 'nds_queries.perzone' is special as it needs data merge.
- self.assertEqual(2, len(stat.statistics_data_bymid['Auth'].values()))
- for s in stat.statistics_data_bymid['Auth'].values():
- self.assertEqual(
- s, {'queries.perzone': stat._auth_sdata['queries.perzone'],
- 'nds_queries.perzone': stat._nds_queries_per_zone,
- 'queries.tcp': stat._auth_sdata['queries.tcp'],
- 'queries.udp': stat._auth_sdata['queries.udp']})
-
- # check consolidation of statistics data of the auth instances.
- # it's union of the reported data and the spec default.
- n = len(stat.statistics_data_bymid['Auth'].values())
- self.maxDiff = None
- self.assertEqual(
- stat.statistics_data['Auth'],
- {'queries.perzone': [
- {'zonename': 'test1.example',
- 'queries.tcp': 5 * n,
- 'queries.udp': 4 * n},
- {'zonename': 'test2.example',
- 'queries.tcp': 4 * n,
- 'queries.udp': 3 * n},
- ],
- 'nds_queries.perzone': {
- 'test10.example': {
- 'queries.tcp': 5 * n,
- 'queries.udp': 4 * n
- },
- 'test20.example': {
- 'queries.tcp': 4 * n,
- 'queries.udp': 3 * n
- },
- },
- 'queries.tcp': 3 * n,
- 'queries.udp': 2 * n})
-
- def test_polling_stats(self):
- """Check statistics data of 'Stats'
-
- This is actually irrelevant to do_polling(), but provided to
- compatibility of older tests.
-
- """
- stat = MyStats()
- self.assertEqual(len(stat.statistics_data['Stats']), 5)
- self.assertTrue('boot_time' in stat.statistics_data['Stats'])
- self.assertTrue('last_update_time' in stat.statistics_data['Stats'])
- self.assertTrue('report_time' in stat.statistics_data['Stats'])
- self.assertTrue('timestamp' in stat.statistics_data['Stats'])
- self.assertEqual(stat.statistics_data['Stats']['lname'],
- stat.mccs._session.lname)
-
- def test_refresh_statistics_broken_statistics_data(self):
- """Test _refresh_statistics() doesn't incorporate broken statistics data
- """
- stat = MyStats()
- # check default statistics data of 'Init'
- self.assertEqual(
- {'boot_time': self.const_default_datetime},
- stat.statistics_data['Init'])
- last_update_time = stat.statistics_data['Stats']['last_update_time']
- # _refresh_statistics() should ignore the invalid statistics_data(type
- # of boot_time is invalid); default data shouldn't be replaced.
- arg = [('Init', 'lname', {'boot_time': 1})]
- stat._refresh_statistics(arg)
- self.assertEqual(
- {'boot_time': self.const_default_datetime},
- stat.statistics_data['Init'])
- # 'last_update_time' doesn't change
- self.assertEqual(
- last_update_time,
- stat.statistics_data['Stats']['last_update_time'])
-
- def test_polling_update_lasttime_poll(self):
- """Test _lasttime_poll is updated after do_polling()
- """
- orig_get_timestamp = stats.get_timestamp
- stats.get_timestamp = lambda : self.const_timestamp
- stat = MyStats()
- self.assertEqual(0.0, stat._lasttime_poll)
- stat.do_polling()
- self.assertEqual(self.const_timestamp, stat._lasttime_poll)
- stats.get_timestamp = orig_get_timestamp
-
- def test_polling_initsessiontimeout(self):
- """Test _lasttime_poll is updated after do_polling() in case that it catches
- InitSesionTimeout at _get_multi_module_list()
- """
- orig_get_timestamp = stats.get_timestamp
- stats.get_timestamp = lambda : self.const_timestamp
- ex = stats.InitSessionTimeout
- def __raise(*x): raise ex(*x)
- stat = MyStats()
- self.assertEqual(0.0, stat._lasttime_poll)
- stat._get_multi_module_list = lambda: __raise()
- stat.do_polling()
- self.assertEqual(self.const_timestamp, stat._lasttime_poll)
- stats.get_timestamp = orig_get_timestamp
-
-class Z_TestOSEnv(unittest.TestCase):
- # Running this test would break logging setting. To prevent it from
- # affecting other tests we use the same workaround as Z_TestOSEnv in
- # stats-httpd_test.py.
- def test_osenv(self):
- """
- test for the environ variable "B10_FROM_SOURCE"
- "B10_FROM_SOURCE" is set in Makefile
- """
- # test case having B10_FROM_SOURCE
- self.assertTrue("B10_FROM_SOURCE" in os.environ)
- self.assertEqual(stats.SPECFILE_LOCATION, \
- os.environ["B10_FROM_SOURCE"] + os.sep + \
- "src" + os.sep + "bin" + os.sep + "stats" + \
- os.sep + "stats.spec")
- # test case not having B10_FROM_SOURCE
- path = os.environ["B10_FROM_SOURCE"]
- os.environ.pop("B10_FROM_SOURCE")
- self.assertFalse("B10_FROM_SOURCE" in os.environ)
- # import stats again
- imp.reload(stats)
- # revert the changes
- os.environ["B10_FROM_SOURCE"] = path
- imp.reload(stats)
-
-if __name__ == "__main__":
- isc.log.resetUnitTestRootLogger()
- unittest.main()
+++ /dev/null
-# Copyright (C) 2011-2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-Utilities and mock modules for unittests of statistics modules
-
-"""
-import os
-import io
-import time
-import threading
-import json
-import signal
-import socket
-
-import isc.config.cfgmgr
-import stats
-import stats_httpd
-
-CONST_BASETIME = (2011, 6, 22, 8, 14, 8, 2, 173, 0)
-
-class SignalHandler():
- """A signal handler class for deadlock in unittest"""
- def __init__(self, fail_handler, timeout=20):
- """sets a schedule in SIGARM for invoking the handler via
- unittest.TestCase after timeout seconds (default is 20)"""
- self.fail_handler = fail_handler
- self.orig_handler = signal.signal(signal.SIGALRM, self.sig_handler)
- signal.alarm(timeout)
-
- def reset(self):
- """resets the schedule in SIGALRM"""
- signal.alarm(0)
- signal.signal(signal.SIGALRM, self.orig_handler)
-
- def sig_handler(self, signal, frame):
- """invokes unittest.TestCase.fail as a signal handler"""
- self.fail_handler("A deadlock might be detected")
-
-class ThreadingServerManager:
- def __init__(self, server, *args, **kwargs):
- self.server = server(*args, **kwargs)
- self.server_name = server.__name__
- self.server._thread = threading.Thread(
- name=self.server_name, target=self.server.run)
- self.server._thread.daemon = True
-
- def run(self):
- self.server._thread.start()
- self.server._started.wait()
- self.server._started.clear()
-
- def shutdown(self, blocking=False):
- """Shut down the server by calling its own shutdown() method.
- Then wait for its thread to finish. If blocking is True,
- the thread.join() blocks until the thread finishes. If not,
- it uses a zero timeout. The latter is necessary in a number
- of existing tests. We should redo this part (we should not
- even need threads in most, if not all, of these threads, see
- ticket #1668)"""
- self.server.shutdown()
- if blocking:
- self.server._thread.join()
- else:
- self.server._thread.join(0) # timeout is 0
-
-INIT_SPEC_STR = """\
-{
- "module_spec": {
- "module_name": "Init",
- "module_description": "Mock Master process",
- "config_data": [
- {
- "item_name": "components",
- "item_type": "named_set",
- "item_optional": false,
- "item_default": {
- "b10-stats": { "address": "Stats", "kind": "dispensable" },
- "b10-cmdctl": { "special": "cmdctl", "kind": "needed" }
- },
- "named_set_item_spec": {
- "item_name": "component",
- "item_type": "map",
- "item_optional": false,
- "item_default": { },
- "map_item_spec": [
- {
- "item_name": "special",
- "item_optional": true,
- "item_type": "string"
- },
- {
- "item_name": "process",
- "item_optional": true,
- "item_type": "string"
- },
- {
- "item_name": "kind",
- "item_optional": false,
- "item_type": "string",
- "item_default": "dispensable"
- },
- {
- "item_name": "address",
- "item_optional": true,
- "item_type": "string"
- },
- {
- "item_name": "params",
- "item_optional": true,
- "item_type": "list",
- "list_item_spec": {
- "item_name": "param",
- "item_optional": false,
- "item_type": "string",
- "item_default": ""
- }
- },
- {
- "item_name": "priority",
- "item_optional": true,
- "item_type": "integer"
- }
- ]
- }
- }
- ],
- "commands": [
- {
- "command_name": "shutdown",
- "command_description": "Shut down BIND 10",
- "command_args": []
- },
- {
- "command_name": "ping",
- "command_description": "Ping the b10-init process",
- "command_args": []
- },
- {
- "command_name": "show_processes",
- "command_description": "List the running BIND 10 processes",
- "command_args": []
- }
- ],
- "statistics": [
- {
- "item_name": "boot_time",
- "item_type": "string",
- "item_optional": false,
- "item_default": "1970-01-01T00:00:00Z",
- "item_title": "Boot time",
- "item_description": "A date time when bind10 process starts initially",
- "item_format": "date-time"
- }
- ]
- }
-}
-"""
-
-# Note: this is derived of the spec for the DNS authoritative server, but
-# for the purpose of this test, it's completely irrelevant to DNS.
-# Some statisittics specs do not make sense for practical sense but used
-# just cover various types of statistics data (list, map/dict, etc).
-AUTH_SPEC_STR = """\
-{
- "module_spec": {
- "module_name": "Auth",
- "module_description": "Mock Authoritative service",
- "config_data": [],
- "commands": [],
- "statistics": [
- {
- "item_name": "queries.tcp",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Queries TCP",
- "item_description": "A number of total query counts which all auth servers receive over TCP since they started initially"
- },
- {
- "item_name": "queries.udp",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Queries UDP",
- "item_description": "A number of total query counts which all auth servers receive over UDP since they started initially"
- },
- {
- "item_name": "queries.perzone",
- "item_type": "list",
- "item_optional": false,
- "item_default": [
- {
- "zonename" : "test1.example",
- "queries.udp" : 1,
- "queries.tcp" : 2
- },
- {
- "zonename" : "test2.example",
- "queries.udp" : 3,
- "queries.tcp" : 4
- }
- ],
- "item_title": "Queries per zone",
- "item_description": "Queries per zone",
- "list_item_spec": {
- "item_name": "zones",
- "item_type": "map",
- "item_optional": false,
- "item_default": {},
- "map_item_spec": [
- {
- "item_name": "zonename",
- "item_type": "string",
- "item_optional": false,
- "item_default": "",
- "item_title": "Zonename",
- "item_description": "Zonename"
- },
- {
- "item_name": "queries.udp",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Queries UDP per zone",
- "item_description": "A number of UDP query counts per zone"
- },
- {
- "item_name": "queries.tcp",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Queries TCP per zone",
- "item_description": "A number of TCP query counts per zone"
- }
- ]
- }
- },
- {
- "item_name": "nds_queries.perzone",
- "item_type": "named_set",
- "item_optional": false,
- "item_default": {
- "test10.example" : {
- "queries.udp" : 1,
- "queries.tcp" : 2
- },
- "test20.example" : {
- "queries.udp" : 3,
- "queries.tcp" : 4
- }
- },
- "item_title": "Queries per zone",
- "item_description": "Queries per zone",
- "named_set_item_spec": {
- "item_name": "zonename",
- "item_type": "map",
- "item_optional": false,
- "item_default": {},
- "item_title": "Zonename",
- "item_description": "Zonename",
- "map_item_spec": [
- {
- "item_name": "queries.udp",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Queries UDP per zone",
- "item_description": "A number of UDP query counts per zone"
- },
- {
- "item_name": "queries.tcp",
- "item_type": "integer",
- "item_optional": false,
- "item_default": 0,
- "item_title": "Queries TCP per zone",
- "item_description": "A number of TCP query counts per zone"
- }
- ]
- }
- }
- ]
- }
-}
-"""
-
-class MyModuleCCSession(isc.config.ConfigData):
- """Mocked ModuleCCSession class.
-
- This class incorporates the module spec directly from the file,
- and works as if the ModuleCCSession class as much as possible
- without involving network I/O.
-
- """
- def __init__(self, spec_file, config_handler, command_handler):
- module_spec = isc.config.module_spec_from_file(spec_file)
- isc.config.ConfigData.__init__(self, module_spec)
- self._session = self
- self.stopped = False
- self.closed = False
- self.lname = 'mock_mod_ccs'
- self._msg = None
- self._env = None
-
- def start(self):
- pass
-
- def send_stopping(self):
- self.stopped = True # just record it's called to inspect it later
-
- def close(self):
- self.closed = True
-
- def check_command_without_recvmsg(self, msg, env):
- self._msg = msg
- self._env = env
-
-class MyStats(stats.Stats):
- """A faked Stats class for unit tests.
-
- This class inherits most of the real Stats class, but replaces the
- ModuleCCSession with a fake one so we can avoid network I/O in tests,
- and can also inspect or tweak messages via the session more easily.
- This class also maintains some faked module information and statistics
- data that can be retrieved from the implementation of the Stats class.
-
- """
- def __init__(self):
- # First, setup some internal attributes. All of them are essentially
- # private (so prefixed with double '_'), but some are defined as if
- # "protected" (with a single '_') for the convenient of tests that
- # may want to inspect or tweak them.
-
- # initial seq num for faked group_sendmsg, arbitrary choice.
- self._seq = 4200
- # if set, use them as faked response to group_recvmsg (see below).
- # it's a list of tuples, each of which is of (answer, envelope).
- self._answers = []
- # the default answer from faked recvmsg if _answers is empty
- self.__default_answer = isc.config.ccsession.create_answer(
- 0, {'Init':
- json.loads(INIT_SPEC_STR)['module_spec']['statistics'],
- 'Auth':
- json.loads(AUTH_SPEC_STR)['module_spec']['statistics']
- })
- # setup faked auth statistics
- self.__init_auth_stat()
- # statistics data for faked Init module
- self._init_sdata = {
- 'boot_time': time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)
- }
-
- # Incorporate other setups of the real Stats module. We use the faked
- # ModuleCCSession to avoid blocking network operation. Note also that
- # we replace _init_statistics_data() (see below), so we don't
- # initialize statistics data yet.
- stats.Stats.__init__(self, MyModuleCCSession)
-
- # replace some (faked) ModuleCCSession methods so we can inspect/fake
- # the data exchanged via the CC session, then call
- # _init_statistics_data. This will get the Stats module info from
- # the file directly and some amount information about the Init and
- # Auth modules (hardcoded below).
- self.cc_session.group_sendmsg = self.__group_sendmsg
- self.cc_session.group_recvmsg = self.__group_recvmsg
- self.cc_session.rpc_call = self.__rpc_call
- stats.Stats._init_statistics_data(self)
-
- def __init_auth_stat(self):
- self._queries_tcp = 3
- self._queries_udp = 2
- self._queries_per_zone = [{
- 'zonename': 'test1.example', 'queries.tcp': 5, 'queries.udp': 4
- }]
- self._nds_queries_per_zone = \
- { 'test10.example': { 'queries.tcp': 5, 'queries.udp': 4 } }
- self._auth_sdata = \
- { 'queries.tcp': self._queries_tcp,
- 'queries.udp': self._queries_udp,
- 'queries.perzone' : self._queries_per_zone,
- 'nds_queries.perzone' : {
- 'test10.example': {
- 'queries.tcp': isc.cc.data.find(
- self._nds_queries_per_zone,
- 'test10.example/queries.tcp')
- }
- },
- 'nds_queries.perzone/test10.example/queries.udp' :
- isc.cc.data.find(self._nds_queries_per_zone,
- 'test10.example/queries.udp')
- }
-
- def _init_statistics_data(self):
- # Inherited from real Stats class, just for deferring the
- # initialization until we are ready.
- pass
-
- def __group_sendmsg(self, command, destination, want_answer=False):
- """Faked ModuleCCSession.group_sendmsg for tests.
-
- Skipping actual network communication, and just returning an internally
- generated sequence number.
-
- """
- self._seq += 1
- return self._seq
-
- def __group_recvmsg(self, nonblocking = True, seq = None):
- """Faked ModuleCCSession.group_recvmsg for tests.
-
- Skipping actual network communication, and returning an internally
- prepared answer. sequence number. If faked anser is given in
- _answers, use it; otherwise use the default. we don't actually check
- the sequence.
-
- """
- if len(self._answers) == 0:
- return self.__default_answer, {'from': 'no-matter'}
- return self._answers.pop(0)
-
- def __rpc_call(self, command, group):
- """Faked ModuleCCSession.rpc_call for tests.
-
- At the moment we don't have to cover failure cases, so this is a
- simple wrapper for the faked group_recvmsg().
-
- """
- answer, _ = self.__group_recvmsg(None, None)
- return isc.config.ccsession.parse_answer(answer)[1]
-
-class MyStatsHttpd(stats_httpd.StatsHttpd):
- """A faked StatsHttpd class for unit tests.
-
- This class inherits most of the real StatsHttpd class, but replaces the
- ModuleCCSession with a fake one so we can avoid network I/O in tests,
- and can also inspect or tweak messages via the session more easily.
-
- """
-
- ORIG_SPECFILE_LOCATION = stats_httpd.SPECFILE_LOCATION
- def __init__(self, *server_address):
- self._started = threading.Event()
- self.__dummy_sock = None # see below
-
- # Prepare commonly used statistics schema and data requested in
- # stats-httpd tests. For the purpose of these tests, the content of
- # statistics data is not so important (they don't test whther the
- # counter values are correct, etc), so hardcoding the common case
- # should suffice. Note also that some of the statistics values and
- # specs don't make sense in practice (see also comments on
- # AUTH_SPEC_STR).
- with open(stats.SPECFILE_LOCATION) as f:
- stat_spec_str = f.read()
- self.__default_spec_answer = {
- 'Init': json.loads(INIT_SPEC_STR)['module_spec']['statistics'],
- 'Auth': json.loads(AUTH_SPEC_STR)['module_spec']['statistics'],
- 'Stats': json.loads(stat_spec_str)['module_spec']['statistics']
- }
- self.__default_data_answer = {
- 'Init': {'boot_time':
- time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME)},
- 'Stats': {'last_update_time':
- time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
- 'report_time':
- time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
- 'lname': 'test-lname',
- 'boot_time':
- time.strftime('%Y-%m-%dT%H:%M:%SZ', CONST_BASETIME),
- 'timestamp': time.mktime(CONST_BASETIME)},
- 'Auth': {'queries.udp': 4, 'queries.tcp': 6,
- 'queries.perzone': [
- {'queries.udp': 8, 'queries.tcp': 10,
- 'zonename': 'test1.example'},
- {'queries.udp': 6, 'queries.tcp': 8,
- 'zonename': 'test2.example'}],
- 'nds_queries.perzone': {
- 'test10.example': {'queries.udp': 8, 'queries.tcp': 10},
- 'test20.example': {'queries.udp': 6, 'queries.tcp': 8}}}}
-
- # if set, use them as faked response to rpc_call (see below).
- # it's a list of answer data of rpc_call.
- self._rpc_answers = []
-
- if server_address:
- stats_httpd.SPECFILE_LOCATION = \
- self.__create_specfile(*server_address)
- try:
- stats_httpd.StatsHttpd.__init__(self)
- finally:
- if hasattr(stats_httpd.SPECFILE_LOCATION, "close"):
- stats_httpd.SPECFILE_LOCATION.close()
- stats_httpd.SPECFILE_LOCATION = self.ORIG_SPECFILE_LOCATION
- else:
- stats_httpd.StatsHttpd.__init__(self)
-
- # replace some (faked) ModuleCCSession methods so we can inspect/fake.
- # in order to satisfy select.select() we need some real socket. We
- # use an unusable AF_UNIX socket; we won't actually use it for
- # communication.
- self.cc_session.rpc_call = self.__rpc_call
- self.__dummy_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
- self.mccs.get_socket = lambda: self.__dummy_sock
-
- def open_mccs(self):
- self.mccs = MyModuleCCSession(stats_httpd.SPECFILE_LOCATION,
- self.config_handler,
- self.command_handler)
- self.cc_session = self.mccs._session
- self.mccs.start = self.load_config # force reload
-
- # check_command could be called from the main select() loop due to
- # Linux's bug of spurious wakeup. We don't need the actual behavior
- # of check_command in our tests, so we can basically replace it with a
- # no-op mock function.
- def mock_check_command(nonblock):
- pass
- self.mccs.check_command = mock_check_command
-
- def close_mccs(self):
- super().close_mccs()
- if self.__dummy_sock is not None:
- self.__dummy_sock.close()
- self.__dummy_sock = None
-
- def __rpc_call(self, command, group, params={}):
- """Faked ModuleCCSession.rpc_call for tests.
-
- The stats httpd module only issues two commands: 'showschema' and
- 'show'. In most cases we can simply use the prepared default
- answer. If customization is needed, the test case can add a
- faked answer by appending it to _rpc_answers. If the added object
- is of Exception type this method raises it instead of return it,
- emulating the situation where rpc_call() results in an exception.
-
- """
- if len(self._rpc_answers) == 0:
- if command == 'showschema':
- return self.__default_spec_answer
- elif command == 'show':
- return self.__default_data_answer
- assert False, "unexpected command for faked rpc_call: " + command
-
- answer = self._rpc_answers.pop(0)
- if issubclass(type(answer), Exception):
- raise answer
- return answer
-
- def __create_specfile(self, *server_address):
- spec_io = open(self.ORIG_SPECFILE_LOCATION)
- try:
- spec = json.load(spec_io)
- spec_io.close()
- config = spec['module_spec']['config_data']
- for i in range(len(config)):
- if config[i]['item_name'] == 'listen_on':
- config[i]['item_default'] = \
- [ dict(address=a[0], port=a[1])
- for a in server_address ]
- break
- return io.StringIO(json.dumps(spec))
- finally:
- spec_io.close()
-
- def run(self):
- self._started.set()
- self.start()
-
- def shutdown(self):
- self.command_handler('shutdown', None)
+++ /dev/null
-EXTRA_DIST = b10-config.db
+++ /dev/null
-{ "version": 2,
- "Init": {
- "components": {
- "b10-auth": {
- "kind": "needed",
- "special": "auth"
- },
- "b10-auth-2": {
- "kind": "needed",
- "special": "auth"
- }
- }
- }
-}
+++ /dev/null
-/isc-sysinfo
-/isc-sysinfo.1
-/run_sysinfo.sh
-/sysinfo.py
+++ /dev/null
-bin_SCRIPTS = isc-sysinfo
-noinst_SCRIPTS = run_sysinfo.sh
-
-CLEANFILES = isc-sysinfo sysinfo.pyc
-
-# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
-isc-sysinfo: sysinfo.py
- $(SED) -e "s|@@PYTHONPATH@@|@pyexecdir@|" sysinfo.py >$@
- chmod a+x $@
-
-MAN1_FILES = \
- isc-sysinfo.xml
-
-man_MANS = \
- $(MAN1_FILES:.xml=.1)
-DISTCLEANFILES = $(man_MANS)
-
-if GENERATE_DOCS
-
-.xml.1:
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $<
-
-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 BIND 10 > $@
-
-endif
-
-EXTRA_DIST = $(man_MANS) $(MAN1_FILES)
-
-CLEANDIRS = __pycache__
-
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2012 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>June 26, 2012</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>isc-sysinfo</refentrytitle>
- <manvolnum>1</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>isc-sysinfo</refname>
- <refpurpose>BIND 10 system information display tool</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2012</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>isc-sysinfo</command>
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>
- The <command>isc-sysinfo</command> program collects and outputs a
- variety of information about the system that BIND 10 is running
- on. This information can be useful to people involved in debugging
- and technical support.
- </para>
- </refsect1>
-
- <refsect1>
- <title>ARGUMENTS</title>
-
- <variablelist>
-
- <varlistentry>
- <term>-h</term>
- <listitem><para>
- Displays usage instructions.
- </para></listitem>
- </varlistentry>
-
- <varlistentry>
- <term>-o <replaceable class="parameter">output-file</replaceable></term>
- <listitem><para>
- If an output file is specified, the output
- of <command>isc-sysinfo</command> is written to this file. By
- default, the output is written to standard output.
- </para></listitem>
- </varlistentry>
-
- </variablelist>
-
- </refsect1>
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>bind10</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- The <command>isc-sysinfo</command> daemon was initially
- implemented by ISC staff in June, 2012.
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-SYSINFO_PATH=@abs_top_builddir@/src/bin/sysinfo
-
-PYTHONPATH=@abs_top_builddir@/src/lib/python:@abs_top_srcdir@/src/lib/python
-export PYTHONPATH
-
-cd ${SYSINFO_PATH}
-exec ${PYTHON_EXEC} -O sysinfo.py "$@"
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2012 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""
-ISC sysinfo program.
-
-"""
-
-import sys; sys.path.append ('@@PYTHONPATH@@')
-import getopt
-from isc.sysinfo import *
-
-def usage():
- print("Usage: %s [-h] [-o <output-file>]" % sys.argv[0], \
- file=sys.stderr)
- exit(1)
-
-def write_value(out, fmt, call):
- '''Helper function for standard value writing.
- Writes the result from the call in the given format to out.
- Does not write anything if the result of the call is None.
- '''
- value = call()
- if value is not None:
- out.write(fmt % value)
-
-def main():
- try:
- opts, args = getopt.getopt(sys.argv[1:], "o:h", \
- ["output", "help"])
- except getopt.GetoptError as e:
- print(str(e))
- usage()
- exit(1)
-
- output_filename = None
- for option, arg in opts:
- if option in ("-o", "--output"):
- output_filename = arg
- elif option in ("-h", "--help"):
- usage()
- else:
- assert False, "unhandled option"
-
- if output_filename is None:
- f = sys.stdout
- else:
- f = open(output_filename, 'w')
-
- s = SysInfoFromFactory()
-
- f.write('ISC Sysinfo tool\n')
- f.write('================\n')
-
- f.write('\nCPU\n');
- write_value(f, ' + Number of processors: %d\n', s.get_num_processors)
- write_value(f, ' + Endianness: %s\n', s.get_endianness)
-
- f.write('\nPlatform\n');
- write_value(f, ' + Operating system: %s\n', s.get_platform_name)
- write_value(f, ' + Distribution: %s\n', s.get_platform_distro)
- write_value(f, ' + Kernel version: %s\n', s.get_platform_version)
-
- if s.get_platform_is_smp() is not None:
- f.write(' + SMP kernel: ')
- if s.get_platform_is_smp():
- f.write('yes')
- else:
- f.write('no')
- f.write('\n')
-
- write_value(f, ' + Machine name: %s\n', s.get_platform_machine)
- write_value(f, ' + Hostname: %s\n', s.get_platform_hostname)
- write_value(f, ' + Uptime: %d seconds', s.get_uptime)
- write_value(f, ' (%s)\n', s.get_uptime_desc)
-
- write_value(f, ' + Loadavg: %f %f %f\n', s.get_loadavg)
-
- f.write('\nMemory\n');
- write_value(f, ' + Total: %d bytes\n', s.get_mem_total)
- write_value(f, ' + Free: %d bytes\n', s.get_mem_free)
- write_value(f, ' + Cached: %d bytes\n', s.get_mem_cached)
- write_value(f, ' + Buffers: %d bytes\n', s.get_mem_buffers)
- write_value(f, ' + Swap total: %d bytes\n', s.get_mem_swap_total)
- write_value(f, ' + Swap free: %d bytes\n', s.get_mem_swap_free)
-
- f.write('\n\nNetwork\n');
- f.write('-------\n\n');
-
- f.write('Interfaces\n')
- f.write('~~~~~~~~~~\n\n')
-
- write_value(f, '%s', s.get_net_interfaces)
-
- f.write('\nRouting table\n')
- f.write('~~~~~~~~~~~~~\n\n')
- write_value(f, '%s', s.get_net_routing_table)
-
- f.write('\nStatistics\n')
- f.write('~~~~~~~~~~\n\n')
- write_value(f, '%s', s.get_net_stats)
-
- f.write('\nConnections\n')
- f.write('~~~~~~~~~~~\n\n')
- write_value(f, '%s', s.get_net_connections)
-
- try:
- if os.getuid() != 0:
- sys.stderr.write('\n')
- sys.stderr.write('NOTE: You have to run this program as the root user so that it can\n')
- sys.stderr.write(' collect all the information it requires. Some information is\n')
- sys.stderr.write(' only available to the root user.\n\n')
- except Exception:
- pass
-
- if f != sys.stdout:
- f.close()
-
-if __name__ == '__main__':
- main()
+++ /dev/null
-/process_rename_test.py
+++ /dev/null
-PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
-PYTESTS = process_rename_test.py
-noinst_SCRIPTS = $(PYTESTS)
-# .py will be generated by configure, so we don't have to include it
-# in EXTRA_DIST.
-
-# If necessary (rare cases), explicitly specify paths to dynamic libraries
-# required by loadable python modules.
-LIBRARY_PATH_PLACEHOLDER =
-if SET_ENV_LIBRARY_PATH
-LIBRARY_PATH_PLACEHOLDER += $(ENV_LIBRARY_PATH)=$(abs_top_builddir)/src/lib/cryptolink/.libs:$(abs_top_builddir)/src/lib/dns/.libs:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/cc/.libs:$(abs_top_builddir)/src/lib/config/.libs:$(abs_top_builddir)/src/lib/log/.libs:$(abs_top_builddir)/src/lib/util/.libs:$(abs_top_builddir)/src/lib/util/threads/.libs:$(abs_top_builddir)/src/lib/exceptions/.libs:$(abs_top_builddir)/src/lib/util/io/.libs:$(abs_top_builddir)/src/lib/datasrc/.libs:$$$(ENV_LIBRARY_PATH)
-endif
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- chmod +x $(abs_builddir)/$$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/lib/dns/python/.libs \
- $(PYCOVERAGE_RUN) $(abs_builddir)/$$pytest || exit ; \
- done
+++ /dev/null
-This directory does not contain any code a user might want to run. It contains
-"global" tests -- tests that are not specific to a single library or program.
-Do not expect to find b10-tests here, there's no such program.
+++ /dev/null
-# Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-"""Tests to see if every python process renames itself."""
-import unittest
-import os
-import os.path
-import isc.util.process
-import re
-
-class TestRename(unittest.TestCase):
- """Test scanning all python scripts if they rename themself."""
- def __scan(self, directory, script, fun):
- # Scan one script if it contains call to the renaming function
- filename = os.path.join(directory, script)
- if not os.path.exists(filename):
- # We either didn't compile it yet or it is not compiled based
- # on some environmental condition. That is OK, we don't want
- # to break on that.
- return
- with open(filename) as f:
- data = ''.join(f.readlines())
- prettyname = 'src' + filename[filename.rfind('../') + 2:]
- self.assertTrue(fun.search(data),
- "Didn't find a call to isc.util.process.rename in " + prettyname)
-
- def test_calls(self):
- """
- Test if every script renames itself.
-
- Scan all Makefile and look for scripts.
- Then scan them by looking at the source text
- (without actually running them)
- """
-
- # Scripts named in this list are not expected to be renamed and
- # should be excluded from the scan.
- EXCLUDED_SCRIPTS = ['isc-sysinfo', 'bind10']
-
- # Regexp to find all the *_SCRIPTS = something lines (except for
- # noinst_SCRIPTS, which are scripts for tests), including line
- # continuations (backslash and newline)
- excluded_lines = re.compile(r'^(noinst_SCRIPTS.*$)', re.MULTILINE)
- lines = re.compile(r'^\w+_SCRIPTS\s*=\s*((.|\\\n)*)$',
- re.MULTILINE)
- # Script name regular expression
- scripts = re.compile(r'((\w|[-.0-9])+)')
- # Line with the call
- fun = re.compile(r'^\s*isc\.util\.process\.rename\s*\(.*\)\s*(|#.*)$',
- re.MULTILINE)
-
- # Find all Makefile and extract names of scripts
- for (d, _, fs) in os.walk('@top_builddir@'):
- if 'Makefile' in fs:
- with open(os.path.join(d, "Makefile")) as f:
- makefile = ''.join(f.readlines())
- for (var, _) in lines.findall(re.sub(excluded_lines, '',
- makefile)):
- for (script, _) in scripts.findall(var):
- if script in EXCLUDED_SCRIPTS:
- continue
- self.__scan(d, script, fun)
-
-if __name__ == "__main__":
- unittest.main()
+++ /dev/null
-/b10-cmdctl-usermgr
-/b10-cmdctl-usermgr.py
-/run_b10-cmdctl-usermgr.sh
-/b10-cmdctl-usermgr.8
+++ /dev/null
-SUBDIRS = tests
-
-sbin_SCRIPTS = b10-cmdctl-usermgr
-noinst_SCRIPTS = run_b10-cmdctl-usermgr.sh
-
-b10_cmdctl_usermgrdir = $(pkgdatadir)
-
-CLEANFILES= b10-cmdctl-usermgr b10-cmdctl-usermgr.pyc
-
-man_MANS = b10-cmdctl-usermgr.8
-DISTCLEANFILES = $(man_MANS)
-EXTRA_DIST = $(man_MANS) b10-cmdctl-usermgr.xml
-
-if GENERATE_DOCS
-
-b10-cmdctl-usermgr.8: b10-cmdctl-usermgr.xml
- @XSLTPROC@ --novalid --xinclude --nonet -o $@ http://docbook.sourceforge.net/release/xsl/current/manpages/docbook.xsl $(srcdir)/b10-cmdctl-usermgr.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 BIND 10 > $@
-
-endif
-
-# this is done here since configure.ac AC_OUTPUT doesn't expand exec_prefix
-b10-cmdctl-usermgr: b10-cmdctl-usermgr.py
- $(SED) "s|@@PYTHONPATH@@|@pyexecdir@|" b10-cmdctl-usermgr.py >$@
- chmod a+x $@
-
-CLEANDIRS = __pycache__
-clean-local:
- rm -rf $(CLEANDIRS)
+++ /dev/null
-Create new account by running the following command:
-
-b10-cmdctl-usermgr -f filename
-
-If parameter '-f' is specified, new accounts will be appended to that file if it exists, or else the accounts will be saved in default file 'cmdctl-accounts.csv'.
-
+++ /dev/null
-#!@PYTHON@
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-'''
-This tool implements user management for b10-cmdctl. It is used to
-add and remove users from the accounts file.
-'''
-import sys; sys.path.append ('@@PYTHONPATH@@')
-from bind10_config import SYSCONFPATH
-from collections import OrderedDict
-import random
-from hashlib import sha1
-import csv
-import getpass
-from optparse import OptionParser, OptionValueError
-import os
-import isc.util.process
-
-isc.util.process.rename()
-
-VERSION_STRING = "b10-cmdctl-usermgr @PACKAGE_VERSION@"
-DEFAULT_FILE = SYSCONFPATH + "/cmdctl-accounts.csv"
-
-# Actions that can be performed (used for argument parsing,
-# code paths, and output)
-COMMAND_ADD = "add"
-COMMAND_DELETE = "delete"
-
-# Non-zero return codes, used in tests
-BAD_ARGUMENTS = 1
-FILE_ERROR = 2
-USER_EXISTS = 3
-USER_DOES_NOT_EXIST = 4
-
-class UserManager:
- def __init__(self, options, args):
- self.options = options
- self.args = args
-
- def __print(self, msg):
- if not self.options.quiet:
- print(msg)
-
- def __gen_password_hash(self, password):
- salt = "".join(chr(random.randint(ord('!'), ord('~')))\
- for x in range(64))
- saltedpwd = sha1((password + salt).encode()).hexdigest()
- return salt, saltedpwd
-
- def __read_user_info(self):
- """
- Read the existing user info
- Raises an IOError if the file can't be read
- """
- # Currently, this is done quite naively (there is no
- # check that the file is modified between read and write)
- # But taking multiple simultaneous users of this tool on the
- # same file seems unnecessary at this point.
- self.user_info = OrderedDict()
- if os.path.exists(self.options.output_file):
- # Just let any file read error bubble up; it will
- # be caught in the run() method
- with open(self.options.output_file, newline='') as csvfile:
- reader = csv.reader(csvfile, strict=True)
- for row in reader:
- self.user_info[row[0]] = row
-
- def __save_user_info(self):
- """
- Write out the (modified) user info
- Raises an IOError if the file can't be written
- """
- # Just let any file write error bubble up; it will
- # be caught in the run() method
- with open(self.options.output_file, 'w',
- newline='') as csvfile:
- writer = csv.writer(csvfile)
- for row in self.user_info.values():
- writer.writerow(row)
-
- def __add_user(self, name, password):
- """
- Add the given username/password combination to the stored user_info.
- First checks if the username exists, and returns False if so.
- If not, it is added, and this method returns True.
- """
- if name in self.user_info:
- return False
- salt, pw = self.__gen_password_hash(password)
- self.user_info[name] = [name, pw, salt]
- return True
-
- def __delete_user(self, name):
- """
- Removes the row with the given name from the stored user_info
- First checks if the username exists, and returns False if not.
- Otherwise, it is removed, and this mehtod returns True
- """
- if name not in self.user_info:
- return False
- del self.user_info[name]
- return True
-
- # overridable input() call, used in testing
- def _input(self, prompt):
- return input(prompt)
-
- # in essence this is private, but made 'protected' for ease
- # of testing
- def _prompt_for_username(self, command):
- # Note, direct prints here are intentional
- while True:
- name = self._input("Username to " + command + ": ")
- if name == "":
- print("Error username can't be empty")
- continue
-
- if command == COMMAND_ADD and name in self.user_info:
- print("user already exists")
- continue
- elif command == COMMAND_DELETE and name not in self.user_info:
- print("user does not exist")
- continue
-
- return name
-
- # in essence this is private, but made 'protected' for ease
- # of testing
- def _prompt_for_password(self):
- # Note, direct prints here are intentional
- while True:
- pwd1 = getpass.getpass("Choose a password: ")
- if pwd1 == "":
- print("Error: password cannot be empty")
- continue
- pwd2 = getpass.getpass("Re-enter password: ")
- if pwd1 != pwd2:
- print("passwords do not match, try again")
- continue
- return pwd1
-
- def __verify_options_and_args(self):
- """
- Basic sanity checks on command line arguments.
- Returns False if there is a problem, True if everything seems OK.
- """
- if len(self.args) < 1:
- self.__print("Error: no command specified")
- return False
- if len(self.args) > 3:
- self.__print("Error: extraneous arguments")
- return False
- if self.args[0] not in [ COMMAND_ADD, COMMAND_DELETE ]:
- self.__print("Error: command must be either add or delete")
- return False
- if self.args[0] == COMMAND_DELETE and len(self.args) > 2:
- self.__print("Error: delete only needs username, not a password")
- return False
- return True
-
- def run(self):
- if not self.__verify_options_and_args():
- return BAD_ARGUMENTS
-
- try:
- self.__print("Using accounts file: " + self.options.output_file)
- self.__read_user_info()
-
- command = self.args[0]
-
- if len(self.args) > 1:
- username = self.args[1]
- else:
- username = self._prompt_for_username(command)
-
- if command == COMMAND_ADD:
- if len(self.args) > 2:
- password = self.args[2]
- else:
- password = self._prompt_for_password()
- if not self.__add_user(username, password):
- print("Error: username exists")
- return USER_EXISTS
- elif command == COMMAND_DELETE:
- if not self.__delete_user(username):
- print("Error: username does not exist")
- return USER_DOES_NOT_EXIST
-
- self.__save_user_info()
- return 0
- except IOError as ioe:
- self.__print("Error accessing " + ioe.filename +\
- ": " + str(ioe.strerror))
- return FILE_ERROR
- except csv.Error as csve:
- self.__print("Error parsing csv file: " + str(csve))
- return FILE_ERROR
-
-def set_options(parser):
- parser.add_option("-f", "--file",
- dest="output_file", default=DEFAULT_FILE,
- help="Accounts file to modify"
- )
- parser.add_option("-q", "--quiet",
- dest="quiet", action="store_true", default=False,
- help="Quiet mode, don't print any output"
- )
-
-def main():
- usage = "usage: %prog [options] <command> [username] [password]\n\n"\
- "Arguments:\n"\
- " command\t\teither 'add' or 'delete'\n"\
- " username\t\tthe username to add or delete\n"\
- " password\t\tthe password to set for the added user\n"\
- "\n"\
- "If username or password are not specified, %prog will\n"\
- "prompt for them. It is recommended practice to let the\n"\
- "tool prompt for the password, as command-line\n"\
- "arguments can be visible through history or process\n"\
- "viewers."
- parser = OptionParser(usage=usage, version=VERSION_STRING)
- set_options(parser)
- (options, args) = parser.parse_args()
-
- usermgr = UserManager(options, args)
- return usermgr.run()
-
-if __name__ == '__main__':
- sys.exit(main())
-
+++ /dev/null
-<!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook XML V4.2//EN"
- "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"
- [<!ENTITY mdash "—">]>
-<!--
- - Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
- -
- - Permission to use, copy, modify, and/or distribute this software for any
- - purpose with or without fee is hereby granted, provided that the above
- - copyright notice and this permission notice appear in all copies.
- -
- - THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
- - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
- - AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
- - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
- - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT, NEGLIGENCE
- - OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
- - PERFORMANCE OF THIS SOFTWARE.
--->
-
-<refentry>
-
- <refentryinfo>
- <date>March 17, 2010</date>
- </refentryinfo>
-
- <refmeta>
- <refentrytitle>b10-cmdctl-usermgr</refentrytitle>
- <manvolnum>8</manvolnum>
- <refmiscinfo>BIND10</refmiscinfo>
- </refmeta>
-
- <refnamediv>
- <refname>b10-cmdctl-usermgr</refname>
- <refpurpose>cmdctl user maintenance tool</refpurpose>
- </refnamediv>
-
- <docinfo>
- <copyright>
- <year>2010</year>
- <holder>Internet Systems Consortium, Inc. ("ISC")</holder>
- </copyright>
- </docinfo>
-
- <refsynopsisdiv>
- <cmdsynopsis>
- <command>b10-cmdctl-usermgr</command>
-
- <arg><option>-f <replaceable>filename</replaceable></option></arg>
- <arg><option>-h</option></arg>
- <arg><option>--file=<replaceable>filename</replaceable></option></arg>
- <arg><option>--help</option></arg>
- <arg><option>--version</option></arg>
- <arg choice="plain"><replaceable>command</replaceable></arg>
- <arg><option>username</option></arg>
- <arg><option>password</option></arg>
-
- </cmdsynopsis>
- </refsynopsisdiv>
-
- <refsect1>
- <title>DESCRIPTION</title>
- <para>The <command>b10-cmdctl-usermgr</command> tool may be used
- to add and remove accounts with passwords for the
- <citerefentry><refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum></citerefentry>
- daemon.
- </para>
-
- <para>
- By default, the accounts are saved in the
- <filename>cmdctl-accounts.csv</filename> file in the system config
- directory, unless the <option>--filename</option> switch is used.
- The entry is appended to or removed from the file.
- </para>
-
- <para>
- The tool can't replace existing entries, but this can easily be
- accomplished by removing the entry and adding a new one.
- </para>
- </refsect1>
-
- <refsect1>
- <title>OPTIONS</title>
-
- <para>The arguments are as follows:</para>
-
- <para>
- command is either 'add' or 'delete', respectively to add or delete users.
- </para>
-
- <para>
- If a username and password are given (or just a username in case of
- deletion), these are used. Otherwise, the tool shall prompt for a
- username and/or password. It is recommended practice to let the
- tool prompt for the password, as command-line arguments can be
- visible through history or process viewers.
- </para>
-
- <variablelist>
-
- <varlistentry>
- <term><option>-h</option></term>
- <term><option>--help</option></term>
- <listitem><para>
- Report the usage statement and exit.
- </para></listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>-f <replaceable>filename</replaceable></option></term>
- <term><option>--file <replaceable>filename</replaceable></option></term>
- <listitem><para>
- Specify the accounts file to update. The default is
- <filename>cmdctl-accounts.csv</filename> in the system config
- directory.
- </para></listitem>
- </varlistentry>
-
- <varlistentry>
- <term><option>--version</option></term>
- <listitem><para>
- Report the version and exit.
- </para></listitem>
- </varlistentry>
-
- </variablelist>
-
- </refsect1>
-
-<!--
- <refsect1>
- <title>FILES</title>
- <para>
- </para>
- </refsect1>
--->
-
- <refsect1>
- <title>SEE ALSO</title>
- <para>
- <citerefentry>
- <refentrytitle>b10-cmdctl</refentrytitle><manvolnum>8</manvolnum>
- </citerefentry>,
- <citetitle>BIND 10 Guide</citetitle>.
- </para>
- </refsect1>
-
- <refsect1>
- <title>HISTORY</title>
- <para>
- The <command>b10-cmdctl-usermgr</command> tool
- was implemented in January 2010
- by Zhang Likun of CNNIC for the ISC BIND 10 project.
- </para>
- </refsect1>
-</refentry><!--
- - Local variables:
- - mode: sgml
- - End:
--->
+++ /dev/null
-#! /bin/sh
-
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-PYTHON_EXEC=${PYTHON_EXEC:-@PYTHON@}
-export PYTHON_EXEC
-
-PYTHONPATH=@abs_top_builddir@/src/lib/python
-export PYTHONPATH
-
-MYPATH_PATH=@abs_top_builddir@/src/bin/usermgr
-
-BIND10_MSGQ_SOCKET_FILE=@abs_top_builddir@/msgq_socket
-export BIND10_MSGQ_SOCKET_FILE
-
-cd ${MYPATH_PATH}
-exec ${PYTHON_EXEC} b10-cmdctl-usermgr "$@"
+++ /dev/null
-PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
-PYTESTS = b10-cmdctl-usermgr_test.py
-EXTRA_DIST = $(PYTESTS)
-
-CLEANFILES = *.csv
-
-# test using command-line arguments, so use check-local target instead of TESTS
-check-local:
-if ENABLE_PYTHON_COVERAGE
- touch $(abs_top_srcdir)/.coverage
- rm -f .coverage
- ${LN_S} $(abs_top_srcdir)/.coverage .coverage
-endif
- for pytest in $(PYTESTS) ; do \
- echo Running test: $$pytest ; \
- $(LIBRARY_PATH_PLACEHOLDER) \
- PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/cmdctl \
- CMDCTL_BUILD_PATH=$(abs_top_builddir)/src/bin/cmdctl \
- CMDCTL_SRC_PATH=$(abs_top_srcdir)/src/bin/cmdctl \
- B10_LOCKFILE_DIR_FROM_BUILD=$(abs_top_builddir) \
- $(PYCOVERAGE_RUN) $(abs_srcdir)/$$pytest || exit ; \
- done
+++ /dev/null
-# Copyright (C) 2013 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN COMMAND OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS COMMAND, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-import csv
-from hashlib import sha1
-import getpass
-import imp
-import os
-import subprocess
-import stat
-import sys
-import unittest
-from bind10_config import SYSCONFPATH
-
-class PrintCatcher:
- def __init__(self):
- self.stdout_lines = []
-
- def __enter__(self):
- self.__orig_stdout_write = sys.stdout.write
- def new_write(line):
- self.stdout_lines.append(line)
-
- sys.stdout.write = new_write
- return self
-
- def __exit__(self, type, value, traceback):
- sys.stdout.write = self.__orig_stdout_write
-
-class OverrideGetpass:
- def __init__(self, new_getpass):
- self.__new_getpass = new_getpass
- self.__orig_getpass = getpass.getpass
-
- def __enter__(self):
- getpass.getpass = self.__new_getpass
- return self
-
- def __exit__(self, type, value, traceback):
- getpass.getpass = self.__orig_getpass
-
-# input() is a built-in function and not easily overridable
-# so this one uses usermgr for that
-class OverrideInput:
- def __init__(self, usermgr, new_getpass):
- self.__usermgr = usermgr
- self.__new_input = new_getpass
- self.__orig_input = usermgr._input
-
- def __enter__(self):
- self.__usermgr._input = self.__new_input
- return self
-
- def __exit__(self, type, value, traceback):
- self.__usermgr._input = self.__orig_input
-
-def run(command):
- """
- Small helper function that returns a tuple of (rcode, stdout, stderr)
- after running the given command (an array of command and arguments, as
- passed on to subprocess).
- Parameters:
- command: an array of command and argument strings, which will be
- passed to subprocess.Popen()
- """
- subp = subprocess.Popen(command, stdout=subprocess.PIPE,
- stderr=subprocess.PIPE)
- (stdout, stderr) = subp.communicate()
- return (subp.returncode, stdout, stderr)
-
-class TestUserMgr(unittest.TestCase):
- TOOL = '../b10-cmdctl-usermgr'
- OUTPUT_FILE = 'test_users.csv'
-
- def setUp(self):
- self.delete_output_file()
- # For access to the actual module, we load it directly
- self.usermgr_module = imp.load_source('usermgr',
- '../b10-cmdctl-usermgr.py')
- # And instantiate 1 instance (with fake options/args)
- self.usermgr = self.usermgr_module.UserManager(object(), object())
-
- def tearDown(self):
- self.delete_output_file()
-
- def delete_output_file(self):
- if os.path.exists(self.OUTPUT_FILE):
- os.remove(self.OUTPUT_FILE)
-
- def check_output_file(self, expected_content):
- self.assertTrue(os.path.exists(self.OUTPUT_FILE))
-
- csv_entries = []
- with open(self.OUTPUT_FILE, newline='') as csvfile:
- reader = csv.reader(csvfile)
- csv_entries = [row for row in reader]
-
- self.assertEqual(len(expected_content), len(csv_entries))
- csv_entries.reverse()
- for expected_entry in expected_content:
- expected_name = expected_entry[0]
- expected_pass = expected_entry[1]
-
- csv_entry = csv_entries.pop()
- entry_name = csv_entry[0]
- entry_salt = csv_entry[2]
- entry_hash = csv_entry[1]
-
- self.assertEqual(expected_name, entry_name)
- expected_hash =\
- sha1((expected_pass + entry_salt).encode()).hexdigest()
- self.assertEqual(expected_hash, entry_hash)
-
- def run_check(self, expected_returncode, expected_stdout, expected_stderr,
- command):
- """
- Runs the given command, and checks return code, and outputs (if provided).
- Arguments:
- expected_returncode, return code of the command
- expected_stdout, (multiline) string that is checked against stdout.
- May be None, in which case the check is skipped.
- expected_stderr, (multiline) string that is checked against stderr.
- May be None, in which case the check is skipped.
-
- Returns the standard output and error captured to a string.
- """
- (returncode, stdout, stderr) = run(command)
- if expected_stderr is not None:
- self.assertEqual(expected_stderr, stderr.decode())
- if expected_stdout is not None:
- self.assertEqual(expected_stdout, stdout.decode())
- self.assertEqual(expected_returncode, returncode, " ".join(command))
- return (stdout.decode(), stderr.decode())
-
- def test_help(self):
- self.run_check(0,
-'''Usage: b10-cmdctl-usermgr [options] <command> [username] [password]
-
-Arguments:
- command either 'add' or 'delete'
- username the username to add or delete
- password the password to set for the added user
-
-If username or password are not specified, b10-cmdctl-usermgr will
-prompt for them. It is recommended practice to let the
-tool prompt for the password, as command-line
-arguments can be visible through history or process
-viewers.
-
-Options:
- --version show program's version number and exit
- -h, --help show this help message and exit
- -f OUTPUT_FILE, --file=OUTPUT_FILE
- Accounts file to modify
- -q, --quiet Quiet mode, don't print any output
-''',
- '',
- [self.TOOL, '-h'])
-
- def test_add_delete_users_ok(self):
- """
- Test that a file is created, and users are added.
- Also tests quiet mode for adding a user to an existing file.
- """
- # content is a list of (user, pass) tuples
- expected_content = []
-
- # Creating a file
- self.run_check(0,
- 'Using accounts file: test_users.csv\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user1', 'pass1'
- ])
- expected_content.append(('user1', 'pass1'))
- self.check_output_file(expected_content)
-
- # Add to existing file
- self.run_check(0,
- 'Using accounts file: test_users.csv\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user2', 'pass2'
- ])
- expected_content.append(('user2', 'pass2'))
- self.check_output_file(expected_content)
-
- # Quiet mode
- self.run_check(0,
- '',
- '',
- [ self.TOOL, '-q',
- '-f', self.OUTPUT_FILE,
- 'add', 'user3', 'pass3'
- ])
- expected_content.append(('user3', 'pass3'))
- self.check_output_file(expected_content)
-
- # Delete a user (let's pick the middle one)
- self.run_check(0,
- '',
- '',
- [ self.TOOL, '-q',
- '-f', self.OUTPUT_FILE,
- 'delete', 'user2'
- ])
- del expected_content[1]
- self.check_output_file(expected_content)
-
- def test_add_delete_users_bad(self):
- """
- More add/delete tests, this time for some error scenarios
- """
- # content is a list of (user, pass) tuples
- expected_content = []
- # First add one
- self.run_check(0, None, None,
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user', 'pass'
- ])
- expected_content.append(('user', 'pass'))
- self.check_output_file(expected_content)
-
- # Adding it again should error
- self.run_check(3,
- 'Using accounts file: test_users.csv\n'
- 'Error: username exists\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user', 'pass'
- ])
- self.check_output_file(expected_content)
-
- # Deleting a non-existent one should fail too
- self.run_check(4,
- 'Using accounts file: test_users.csv\n'
- 'Error: username does not exist\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'delete', 'nosuchuser'
- ])
- self.check_output_file(expected_content)
-
- def test_bad_arguments(self):
- """
- Assorted tests with bad command-line arguments
- """
- self.run_check(1,
- 'Error: no command specified\n',
- '',
- [ self.TOOL ])
- self.run_check(1,
- 'Error: command must be either add or delete\n',
- '',
- [ self.TOOL, 'foo' ])
- self.run_check(1,
- 'Error: extraneous arguments\n',
- '',
- [ self.TOOL, 'add', 'user', 'pass', 'toomuch' ])
- self.run_check(1,
- 'Error: delete only needs username, not a password\n',
- '',
- [ self.TOOL, 'delete', 'user', 'pass' ])
-
- def test_default_file(self):
- """
- Check the default file is the correct one.
- """
- # Hardcoded path .. should be ok since this is run from make check
- self.assertEqual(SYSCONFPATH + '/cmdctl-accounts.csv',
- self.usermgr_module.DEFAULT_FILE)
-
- def test_prompt_for_password_different(self):
- """
- Check that the method that prompts for a password verifies that
- the same value is entered twice
- """
- # returns a different string (the representation of the number
- # of times it has been called), until it has been called
- # over 10 times, in which case it will always return "11"
- getpass_different_called = 0
- def getpass_different(question):
- nonlocal getpass_different_called
- getpass_different_called += 1
- if getpass_different_called > 10:
- return "11"
- else:
- return str(getpass_different_called)
-
- with PrintCatcher() as pc:
- with OverrideGetpass(getpass_different):
- pwd = self.usermgr._prompt_for_password()
- self.assertEqual(12, getpass_different_called)
- self.assertEqual("11", pwd)
- # stdout should be 5 times the no match string;
- expected_output = "passwords do not match, try again\n"*5
- self.assertEqual(expected_output, ''.join(pc.stdout_lines))
-
- def test_prompt_for_password_empty(self):
- """
- Check that the method that prompts for a password verifies that
- the value entered is not empty
- """
- # returns an empty string until it has been called over 10
- # times
- getpass_empty_called = 0
- def getpass_empty(prompt):
- nonlocal getpass_empty_called
- getpass_empty_called += 1
- if getpass_empty_called > 10:
- return "nonempty"
- else:
- return ""
-
- with PrintCatcher() as pc:
- with OverrideGetpass(getpass_empty):
- pwd = self.usermgr._prompt_for_password()
- self.assertEqual("nonempty", pwd)
- self.assertEqual(12, getpass_empty_called)
- # stdout should be 10 times the 'cannot be empty' string
- expected_output = "Error: password cannot be empty\n"*10
- self.assertEqual(expected_output, ''.join(pc.stdout_lines))
-
- def test_prompt_for_user(self):
- """
- Test that the method that prompts for a username verifies that
- is not empty, and that it exists (or does not, depending on the
- action that is specified)
- """
- new_input_called = 0
- input_results = [ '', '', 'existinguser', 'nonexistinguser',
- '', '', 'nonexistinguser', 'existinguser' ]
- def new_input(prompt):
- nonlocal new_input_called
-
- if new_input_called < len(input_results):
- result = input_results[new_input_called]
- else:
- result = 'empty'
- new_input_called += 1
- return result
-
- # add fake user (value doesn't matter, method only checks for key)
- self.usermgr.user_info = { 'existinguser': None }
-
- expected_output = ''
-
- with PrintCatcher() as pc:
- with OverrideInput(self.usermgr, new_input):
- # should skip the first three since empty or existing
- # are not allowed, then return 'nonexistinguser'
- username = self.usermgr._prompt_for_username(
- self.usermgr_module.COMMAND_ADD)
- self.assertEqual('nonexistinguser', username)
- expected_output += "Error username can't be empty\n"*2
- expected_output += "user already exists\n"
- self.assertEqual(expected_output, ''.join(pc.stdout_lines))
-
- # For delete, should again not accept empty (in a while true
- # loop), and this time should not accept nonexisting users
- username = self.usermgr._prompt_for_username(
- self.usermgr_module.COMMAND_DELETE)
- self.assertEqual('existinguser', username)
- expected_output += "Error username can't be empty\n"*2
- expected_output += "user does not exist\n"
- self.assertEqual(expected_output, ''.join(pc.stdout_lines))
-
- def test_bad_file(self):
- """
- Check for graceful handling of bad file argument
- """
- self.run_check(2,
- 'Using accounts file: /\n'
- 'Error accessing /: Is a directory\n',
- '',
- [ self.TOOL, '-f', '/', 'add', 'user', 'pass' ])
-
- # Make sure we can initially write to the test file
- self.run_check(0, None, None,
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user1', 'pass1'
- ])
-
- @unittest.skipIf(os.getuid() == 0,
- 'test cannot be run as root user')
- def test_bad_file_permissions(self):
- """
- Check for graceful handling of bad file argument
- """
- # Create the test file
- self.run_check(0, None, None,
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user1', 'pass1'
- ])
-
- # Make it non-writable (don't worry about cleanup, the
- # file should be deleted after each test anyway
- os.chmod(self.OUTPUT_FILE, stat.S_IRUSR)
- self.run_check(2,
- 'Using accounts file: test_users.csv\n'
- 'Error accessing test_users.csv: Permission denied\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user2', 'pass1'
- ])
-
- self.run_check(2,
- 'Using accounts file: test_users.csv\n'
- 'Error accessing test_users.csv: Permission denied\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'delete', 'user1'
- ])
-
- # Making it write-only should have the same effect
- os.chmod(self.OUTPUT_FILE, stat.S_IWUSR)
- self.run_check(2,
- 'Using accounts file: test_users.csv\n'
- 'Error accessing test_users.csv: Permission denied\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user2', 'pass1'
- ])
-
- self.run_check(2,
- 'Using accounts file: test_users.csv\n'
- 'Error accessing test_users.csv: Permission denied\n',
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'delete', 'user1'
- ])
-
- def test_missing_fields(self):
- """
- Test that an invalid csv file is handled gracefully
- """
- # Valid but incomplete csv; should be handled
- # correctly
- with open(self.OUTPUT_FILE, 'w', newline='') as f:
- f.write('onlyuserfield\n')
- f.write('userfield,saltfield\n')
- f.write(',emptyuserfield,passwordfield\n')
-
- self.run_check(0, None, None,
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user1', 'pass1'
- ])
- self.run_check(0, None, None,
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'delete', 'onlyuserfield'
- ])
- self.run_check(0, None, None,
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'delete', ''
- ])
-
- def test_bad_data(self):
- # I can only think of one invalid format, an unclosed string
- with open(self.OUTPUT_FILE, 'w', newline='') as f:
- f.write('a,"\n')
- # Different versions of the csv library return different errors.
- # So we need to check the output in a little more complex way.
- # We ask the run_check not to check the output and check it
- # ourselves.
- (stdout, stderr) = self.run_check(2, None,
- '',
- [ self.TOOL,
- '-f', self.OUTPUT_FILE,
- 'add', 'user1', 'pass1'
- ])
- # This looks little bit awkward, but is probably easiest with
- # just 2 known possibilities. If there are more, we'll have to
- # think of something else.
- self.assertTrue(stdout ==
- 'Using accounts file: test_users.csv\n'
- 'Error parsing csv file: newline inside string\n' or
- stdout ==
- 'Using accounts file: test_users.csv\n'
- 'Error parsing csv file: unexpected end of data\n')
-
-
-
-if __name__== '__main__':
- unittest.main()
-
FakeSession::DoubleRead);
}
-TEST_F(CCSessionTest, loggingStart) {
- // provide the logging module spec
- ConstElementPtr log_spec = moduleSpecFromFile(LOG_SPEC_FILE).getFullSpec();
- session.getMessages()->add(createAnswer(0, log_spec));
- // just give an empty config
- session.getMessages()->add(createAnswer(0, el("{}")));
- ModuleCCSession mccs(ccspecfile("spec2.spec"), session, NULL, NULL,
- true, true);
- EXPECT_TRUE(session.haveSubscription("Logging", "*"));
-}
-
-TEST_F(CCSessionTest, loggingStartBadSpec) {
- // provide the logging module spec
- session.getMessages()->add(createAnswer(0, el("{}")));
- // just give an empty config
- session.getMessages()->add(createAnswer(0, el("{}")));
- EXPECT_THROW(new ModuleCCSession(ccspecfile("spec2.spec"), session,
- NULL, NULL), ModuleSpecError);
- EXPECT_FALSE(session.haveSubscription("Logging", "*"));
-}
-
// Similar to the above, but more implicitly by calling addRemoteConfig().
// We should construct ModuleCCSession with start_immediately being false
// if we need to call addRemoteConfig().
PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
#PYTESTS = args_test.py bind10_test.py
# NOTE: this has a generated test found in the builddir
-PYTESTS = sockcreator_test.py component_test.py socket_cache_test.py
+PYTESTS =
EXTRA_DIST = $(PYTESTS)
PYCOVERAGE_RUN=@PYCOVERAGE_RUN@
-PYTESTS = ccsession_test.py cfgmgr_test.py config_data_test.py
+PYTESTS = config_data_test.py
PYTESTS += module_spec_test.py
EXTRA_DIST = $(PYTESTS)
EXTRA_DIST += unittest_fakesession.py
+++ /dev/null
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-#
-# Tests for the ConfigData and MultiConfigData classes
-#
-
-import unittest
-import os
-from isc.config.ccsession import *
-from isc.config.config_data import BIND10_CONFIG_DATA_VERSION
-from unittest_fakesession import FakeModuleCCSession, WouldBlockForever
-import bind10_config
-import isc.log
-
-class TestHelperFunctions(unittest.TestCase):
- def test_parse_answer(self):
- self.assertRaises(ModuleCCSessionError, parse_answer, 1)
- self.assertRaises(ModuleCCSessionError, parse_answer, { 'just a dict': 1 })
- self.assertRaises(ModuleCCSessionError, parse_answer, { 'result': 1 })
- self.assertRaises(ModuleCCSessionError, parse_answer, { 'result': [] })
- self.assertRaises(ModuleCCSessionError, parse_answer, { 'result': [ 'not_an_rcode' ] })
- self.assertRaises(ModuleCCSessionError, parse_answer, { 'result': [ 1, 2 ] })
-
- rcode, val = parse_answer({ 'result': [ 0 ] })
- self.assertEqual(0, rcode)
- self.assertEqual(None, val)
- rcode, val = parse_answer({ 'result': [ 0, "something" ] })
- self.assertEqual(0, rcode)
- self.assertEqual("something", val)
- rcode, val = parse_answer({ 'result': [ 1, "some error" ] })
- self.assertEqual(1, rcode)
- self.assertEqual("some error", val)
-
- def test_create_answer(self):
- self.assertRaises(ModuleCCSessionError, create_answer, 'not_an_int')
- self.assertRaises(ModuleCCSessionError, create_answer, 1, 2)
- self.assertRaises(ModuleCCSessionError, create_answer, 1)
- self.assertEqual({ 'result': [ 0 ] }, create_answer(0))
- self.assertEqual({ 'result': [ 1, 'something bad' ] }, create_answer(1, 'something bad'))
- self.assertEqual({ 'result': [ 0, 'something good' ] }, create_answer(0, 'something good'))
- self.assertEqual({ 'result': [ 0, ['some', 'list' ] ] }, create_answer(0, ['some', 'list']))
- self.assertEqual({ 'result': [ 0, {'some': 'map' } ] }, create_answer(0, {'some': 'map'}))
-
- def test_parse_command(self):
- cmd, arg = parse_command(1)
- self.assertEqual(None, cmd)
- self.assertEqual(None, arg)
-
- cmd, arg = parse_command({})
- self.assertEqual(None, cmd)
- self.assertEqual(None, arg)
-
- cmd, arg = parse_command({ 'not a command': 1})
- self.assertEqual(None, cmd)
- self.assertEqual(None, arg)
-
- cmd, arg = parse_command({ 'command': 1})
- self.assertEqual(None, cmd)
- self.assertEqual(None, arg)
-
- cmd, arg = parse_command({ 'command': []})
- self.assertEqual(None, cmd)
- self.assertEqual(None, arg)
-
- cmd, arg = parse_command({ 'command': [ 1 ]})
- self.assertEqual(None, cmd)
- self.assertEqual(None, arg)
-
- cmd, arg = parse_command({ 'command': [ 'command' ]})
- self.assertEqual('command', cmd)
- self.assertEqual(None, arg)
-
- cmd, arg = parse_command({ 'command': [ 'command', 1 ]})
- self.assertEqual('command', cmd)
- self.assertEqual(1, arg)
-
- cmd, arg = parse_command({ 'command': [ 'command', ['some', 'argument', 'list'] ]})
- self.assertEqual('command', cmd)
- self.assertEqual(['some', 'argument', 'list'], arg)
-
- def test_create_command(self):
- self.assertRaises(ModuleCCSessionError, create_command, 1)
- self.assertEqual({'command': [ 'my_command' ]}, create_command('my_command'))
- self.assertEqual({'command': [ 'my_command', 1 ]}, create_command('my_command', 1))
- self.assertEqual({'command': [ 'my_command', [ 'some', 'list' ] ]}, create_command('my_command', [ 'some', 'list' ]))
- self.assertEqual({'command': [ 'my_command', { 'some': 'map' } ]}, create_command('my_command', { 'some': 'map' }))
-
-class TestModuleCCSession(unittest.TestCase):
- def setUp(self):
- if 'CONFIG_TESTDATA_PATH' in os.environ:
- self.data_path = os.environ['CONFIG_TESTDATA_PATH']
- else:
- self.data_path = "../../../testdata"
-
- def spec_file(self, file):
- return self.data_path + os.sep + file
-
- def create_session(self, spec_file_name, config_handler = None,
- command_handler = None, cc_session = None):
- return ModuleCCSession(self.spec_file(spec_file_name),
- config_handler, command_handler,
- cc_session, False)
-
- def test_init(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- self.assertEqual(isc.config.module_spec_from_file(self.spec_file("spec1.spec"))._module_spec, mccs.specification._module_spec)
- self.assertEqual(None, mccs._config_handler)
- self.assertEqual(None, mccs._command_handler)
-
- def test_start1(self):
- fake_session = FakeModuleCCSession()
- self.assertFalse("Spec1" in fake_session.subscriptions)
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- self.assertTrue("Spec1" in fake_session.subscriptions)
-
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg(None, 'Spec1')
- fake_session.group_sendmsg(None, 'Spec1')
- self.assertRaises(ModuleCCSessionError, mccs.start)
- self.assertEqual(len(fake_session.message_queue), 2)
- self.assertEqual({'command': ['module_spec', {'module_name': 'Spec1'}]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec1'}]},
- fake_session.get_message('ConfigManager', None))
-
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg({'result': [ 0 ]}, "Spec1")
- fake_session.group_sendmsg({'result': [ 0 ]}, "Spec1")
- mccs.start()
- self.assertEqual(len(fake_session.message_queue), 2)
-
- self.assertEqual({'command': ['module_spec', {'module_name': 'Spec1'}]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec1'}]},
- fake_session.get_message('ConfigManager', None))
-
- mccs = None
- self.assertFalse("Spec1" in fake_session.subscriptions)
-
- def test_start2(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg(None, 'Spec2')
- fake_session.group_sendmsg(None, 'Spec2')
- self.assertRaises(ModuleCCSessionError, mccs.start)
- self.assertEqual(len(fake_session.message_queue), 2)
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg({'result': [ 0 ]}, "Spec2")
- fake_session.group_sendmsg({'result': [ 0, {} ]}, "Spec2")
- mccs.start()
- self.assertEqual(len(fake_session.message_queue), 2)
-
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- def test_start3(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg(None, 'Spec2')
- fake_session.group_sendmsg(None, 'Spec2')
- self.assertRaises(ModuleCCSessionError, mccs.start)
- self.assertEqual(len(fake_session.message_queue), 2)
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg({'result': [ 0 ]}, "Spec2")
- fake_session.group_sendmsg({'result': [ 0, {} ]}, "Spec2")
- mccs.start()
- self.assertEqual(len(fake_session.message_queue), 2)
-
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- def test_start4(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg(None, 'Spec2')
- fake_session.group_sendmsg(None, 'Spec2')
- self.assertRaises(ModuleCCSessionError, mccs.start)
- self.assertEqual(len(fake_session.message_queue), 2)
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg({'result': [ 0 ]}, "Spec2")
- fake_session.group_sendmsg({'result': [ 1, "just an error" ]}, "Spec2")
- mccs.start()
- self.assertEqual(len(fake_session.message_queue), 2)
-
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- def test_start5(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg(None, 'Spec2')
- fake_session.group_sendmsg(None, 'Spec2')
- self.assertRaises(ModuleCCSessionError, mccs.start)
- self.assertEqual(len(fake_session.message_queue), 2)
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- self.assertEqual(len(fake_session.message_queue), 0)
- fake_session.group_sendmsg({'result': [ 0 ]}, "Spec2")
- fake_session.group_sendmsg({'result': [ 0, {"Wrong": True} ]}, "Spec2")
- self.assertRaises(ModuleCCSessionError, mccs.start)
- self.assertEqual(len(fake_session.message_queue), 2)
-
- self.assertEqual({'command': ['module_spec', mccs.specification._module_spec]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
-
- def test_stop(self):
- fake_session = FakeModuleCCSession()
- self.assertFalse("Spec1" in fake_session.subscriptions)
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- self.assertTrue("Spec1" in fake_session.subscriptions)
-
- self.assertEqual(len(fake_session.message_queue), 0)
- mccs.send_stopping()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'command': ['stopping', {'module_name': 'Spec1'}]},
- fake_session.get_message('ConfigManager', None))
-
- def test_get_socket(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- self.assertNotEqual(None, mccs.get_socket())
-
- def test_get_session(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- self.assertEqual(fake_session, mccs._session)
-
- def test_close(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.close()
- self.assertEqual(None, fake_session._socket)
-
- def test_del_opened(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.__del__() # with opened fake_session
-
- def test_del_closed(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- fake_session.close()
- mccs.__del__() # with closed fake_session
-
- def rpc_check(self, reply):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- fake_session.message_queue = [
- ["Spec1", None, reply, False]
- ]
- exception = None
- try:
- result = mccs.rpc_call("test", "Spec2", params={
- "param1": "Param 1",
- "param2": "Param 2"
- })
- except Exception as e:
- # We first want to check the value sent, raise the exception
- # afterwards. So store it for a short while.
- exception = e
- self.assertEqual([
- ["Spec2", "*", {"command": ["test", {
- "param1": "Param 1",
- "param2": "Param 2"
- }]}, True]
- ], fake_session.message_queue)
- if exception is not None:
- raise exception
- return result
-
- def test_rpc_call_success(self):
- """
- Test we can send an RPC (command) and get an answer. The answer is
- success in this case.
- """
- result = self.rpc_check({"result": [0, {"Hello": "a"}]})
- self.assertEqual({"Hello": "a"}, result)
-
- def test_rpc_call_success_none(self):
- """
- Test the success case of RPC command, but the answer is empty
- (eg. a "void" function on the remote side).
- """
- self.assertIsNone(self.rpc_check({"result": [0]}))
-
- def test_rpc_call_malformed_answer(self):
- """
- Test it successfully raises ModuleCCSessionError when a malformed
- reply is sent.
- """
- self.assertRaises(ModuleCCSessionError, self.rpc_check, ["Nonsense"])
-
- def test_rpc_call_error(self):
- """
- Test it raises an exception when the remote side reports an error.
- """
- self.assertRaises(RPCError, self.rpc_check, {"result": [1, "Error"]})
-
- def test_rpc_call_no_recpt(self):
- """
- Test RPC raises an error when the recipient is not there.
- """
- self.assertRaises(RPCRecipientMissing, self.rpc_check,
- {"result": [-1, "Error"]})
-
- def test_notify(self):
- """
- Test the sent notification has the right format.
- """
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.notify("group", "event", {"param": True})
- self.assertEqual([
- ["notifications/group", "*", {"notification": ["event", {
- "param": True
- }]}, False]], fake_session.message_queue)
-
- def test_notify_no_params(self):
- """
- Test the sent notification has the right format, this time
- without passing parameters.
- """
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.notify("group", "event")
- self.assertEqual([
- ["notifications/group", "*", {"notification": ["event"]},
- False]
- ],
- fake_session.message_queue)
-
- def test_notify_receive(self):
- """
- Test we can subscribe to notifications, receive them, unsubscribe, etc.
- """
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- fake_session.group_sendmsg({"notification": ["event", {
- "param": True
- }]}, "notifications/group")
- # Not subscribed to notifications -> not subscribed to
- # 'notifications/group' -> message not eaten yet
- mccs.check_command()
- self.assertEqual(fake_session.message_queue, [['notifications/group',
- None, {'notification': ['event', {'param': True}]},
- False]])
- # Place to log called notifications
- notifications = []
- def notified(tag, event, params):
- notifications.append((tag, event, params))
- # Subscribe to the notifications. Twice.
- id1 = mccs.subscribe_notification('group',
- lambda event, params:
- notified("first", event, params))
- id2 = mccs.subscribe_notification('group',
- lambda event, params:
- notified("second", event,
- params))
- # Now the message gets eaten because we are subscribed, and both
- # callbacks are called.
- mccs.check_command()
- self.assertEqual(fake_session.message_queue, [])
- self.assertEqual(notifications, [
- ("first", "event", {'param': True}),
- ("second", "event", {'param': True})
- ])
- del notifications[:]
- # If a notification for different group comes, it is left untouched.
- fake_session.group_sendmsg({"notification": ["event", {
- "param": True
- }]}, "notifications/other")
- mccs.check_command()
- self.assertEqual(notifications, [])
- self.assertEqual(fake_session.message_queue, [['notifications/other',
- None, {'notification': ['event', {'param': True}]},
- False]])
- del fake_session.message_queue[:]
- # Unsubscribe one of the notifications and see that only the other
- # is triggered.
- mccs.unsubscribe_notification(id2)
- fake_session.group_sendmsg({"notification": ["event", {
- "param": True
- }]}, "notifications/group")
- mccs.check_command()
- self.assertEqual(fake_session.message_queue, [])
- self.assertEqual(notifications, [
- ("first", "event", {'param': True})
- ])
- del notifications[:]
- # If we try to unsubscribe again, it complains.
- self.assertRaises(KeyError, mccs.unsubscribe_notification, id2)
- # Unsubscribe the other one too. From now on, it doesn't eat the
- # messages again.
- mccs.unsubscribe_notification(id1)
- fake_session.group_sendmsg({"notification": ["event", {
- "param": True
- }]}, "notifications/group")
- mccs.check_command()
- self.assertEqual(fake_session.message_queue, [['notifications/group',
- None, {'notification': ['event', {'param': True}]},
- False]])
-
- def my_config_handler_ok(self, new_config):
- return isc.config.ccsession.create_answer(0)
-
- def my_config_handler_err(self, new_config):
- return isc.config.ccsession.create_answer(1, "just an error")
-
- def my_config_handler_exc(self, new_config):
- raise Exception("just an exception")
-
- def my_command_handler_ok(self, command, args):
- return isc.config.ccsession.create_answer(0)
-
- def my_command_handler_no_answer(self, command, args):
- pass
-
- def test_check_command1(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
-
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 0)
-
- fake_session.group_sendmsg({'result': [ 0 ]}, "Spec1")
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 0)
-
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec1': 'a' })
-
- fake_session.group_sendmsg(cmd, 'Spec1')
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [2, 'Spec1 has no config handler']},
- fake_session.get_message('Spec1', None))
-
- def test_check_command2(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec1': 'a' })
- fake_session.group_sendmsg(cmd, 'Spec1')
- self.assertEqual(len(fake_session.message_queue), 1)
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [1, 'No config_data specification']},
- fake_session.get_message('Spec1', None))
-
- def test_check_command3(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'item1': 2 })
- fake_session.group_sendmsg(cmd, 'Spec2')
- self.assertEqual(len(fake_session.message_queue), 1)
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [0]},
- fake_session.get_message('Spec2', None))
-
- def test_check_command4(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_err)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'item1': 'aaa' })
- fake_session.group_sendmsg(cmd, 'Spec2')
- self.assertEqual(len(fake_session.message_queue), 1)
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [1, 'aaa should be an integer']},
- fake_session.get_message('Spec2', None))
-
- def test_check_command5(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_exc)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'item1': 'aaa' })
- fake_session.group_sendmsg(cmd, 'Spec2')
- self.assertEqual(len(fake_session.message_queue), 1)
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [1, 'aaa should be an integer']},
- fake_session.get_message('Spec2', None))
-
- def test_check_command6(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command("print_message", "just a message")
- fake_session.group_sendmsg(cmd, 'Spec2')
- self.assertEqual(len(fake_session.message_queue), 1)
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [2, 'Spec2 has no command handler']},
- fake_session.get_message('Spec2', None))
-
- """Many check_command tests look too similar, this is common body."""
- def common_check_command_check(self, cmd_handler,
- cmd_check=lambda mccs, _: mccs.check_command()):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_command_handler(cmd_handler)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command("print_message", "just a message")
- fake_session.group_sendmsg(cmd, 'Spec2')
- self.assertEqual(len(fake_session.message_queue), 1)
- cmd_check(mccs, fake_session)
- return fake_session
-
- def test_check_command7(self):
- fake_session = self.common_check_command_check(
- self.my_command_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [0]},
- fake_session.get_message('Spec2', None))
-
- def test_check_command8(self):
- fake_session = self.common_check_command_check(
- self.my_command_handler_no_answer)
- self.assertEqual(len(fake_session.message_queue), 0)
-
- def test_check_command_block(self):
- """See if the message gets there even in blocking mode."""
- fake_session = self.common_check_command_check(
- self.my_command_handler_ok,
- lambda mccs, _: mccs.check_command(False))
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [0]},
- fake_session.get_message('Spec2', None))
-
- def test_check_command_block_timeout(self):
- """Check it works if session has timeout and it sets it back."""
- def cmd_check(mccs, session):
- session.set_timeout(1)
- mccs.check_command(False)
- fake_session = self.common_check_command_check(
- self.my_command_handler_ok, cmd_check)
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [0]},
- fake_session.get_message('Spec2', None))
- self.assertEqual(fake_session.get_timeout(), 1)
-
- def test_check_command_blocks_forever(self):
- """Check it would wait forever checking a command."""
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_command_handler(self.my_command_handler_ok)
- self.assertRaises(WouldBlockForever, lambda: mccs.check_command(False))
-
- def test_check_command_blocks_forever_timeout(self):
- """Like above, but it should wait forever even with timeout here."""
- fake_session = FakeModuleCCSession()
- fake_session.set_timeout(1)
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_command_handler(self.my_command_handler_ok)
- self.assertRaises(WouldBlockForever, lambda: mccs.check_command(False))
-
- def test_check_command_without_recvmsg1(self):
- "copied from test_check_command2"
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec1': 'abcd' })
- env = { 'group': 'Spec1', 'from':None };
- mccs.check_command_without_recvmsg(cmd, env)
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [1, 'No config_data specification']},
- fake_session.get_message('Spec1', None))
-
- def test_check_command_without_recvmsg2(self):
- "copied from test_check_command3"
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'item1': 2 })
- self.assertEqual(len(fake_session.message_queue), 0)
- env = { 'group':'Spec2', 'from':None }
- mccs.check_command_without_recvmsg(cmd, env)
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [0]},
- fake_session.get_message('Spec2', None))
-
- def test_check_command_without_recvmsg3(self):
- "copied from test_check_command7"
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_command_handler(self.my_command_handler_ok)
- self.assertEqual(len(fake_session.message_queue), 0)
- cmd = isc.config.ccsession.create_command("print_message", "just a message")
- env = { 'group':'Spec2', 'from':None }
- self.assertEqual(len(fake_session.message_queue), 0)
- mccs.check_command_without_recvmsg(cmd, env)
- self.assertEqual({'result': [0]},
- fake_session.get_message('Spec2', None))
-
- def test_check_command_block_timeout(self):
- """Check it works if session has timeout and it sets it back."""
- def cmd_check(mccs, session):
- session.set_timeout(1)
- mccs.check_command(False)
- fake_session = self.common_check_command_check(
- self.my_command_handler_ok, cmd_check)
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [0]},
- fake_session.get_message('Spec2', None))
- self.assertEqual(fake_session.get_timeout(), 1)
-
- def test_check_command_blocks_forever(self):
- """Check it would wait forever checking a command."""
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_command_handler(self.my_command_handler_ok)
- self.assertRaises(WouldBlockForever, lambda: mccs.check_command(False))
-
- def test_check_command_blocks_forever_timeout(self):
- """Like above, but it should wait forever even with timeout here."""
- fake_session = FakeModuleCCSession()
- fake_session.set_timeout(1)
- mccs = self.create_session("spec2.spec", None, None, fake_session)
- mccs.set_command_handler(self.my_command_handler_ok)
- self.assertRaises(WouldBlockForever, lambda: mccs.check_command(False))
-
- # Now there's a group of tests testing both add_remote_config and
- # add_remote_config_by_name. Since they are almost the same (they differ
- # just in the parameter and that the second one asks one more question over
- # the bus), the actual test code is shared.
- #
- # These three functions are helper functions to easy up the writing of them.
- # To write a test, there need to be 3 functions. First, the function that
- # does the actual test. It looks like:
- # def _internal_test(self, function_lambda, param, fill_other_messages):
- #
- # The function_lambda provides the tested function if called on the
- # ccsession. The param is the parameter to pass to the function (either
- # the module name or the spec file name. The fill_other_messages fills
- # needed messages (the answer containing the module spec in case of add by
- # name, no messages in the case of adding by spec file) into the fake bus.
- # So, the code would look like:
- #
- # * Create the fake session and tested ccsession object
- # * function = function_lambda(ccsession object)
- # * fill_other_messages(fake session)
- # * Fill in answer to the get_module_config command
- # * Test by calling function(param)
- #
- # Then you need two wrappers that do launch the tests. There are helpers
- # for that, so you can just call:
- # def test_by_spec(self)
- # self._common_remote_module_test(self._internal_test)
- # def test_by_name(self)
- # self._common_remote_module_by_name_test(self._internal_test)
- def _common_remote_module_test(self, internal_test):
- internal_test(lambda ccs: ccs.add_remote_config,
- self.spec_file("spec2.spec"),
- lambda session: None)
-
- def _prepare_spec_message(self, session, spec_name):
- # It could have been one command, but the line would be way too long
- # to even split it
- spec_file = self.spec_file(spec_name)
- spec = isc.config.module_spec_from_file(spec_file)
- session.group_sendmsg({'result': [0, spec.get_full_spec()]}, "Spec1")
-
- def _common_remote_module_by_name_test(self, internal_test):
- internal_test(lambda ccs: ccs.add_remote_config_by_name, "Spec2",
- lambda session: self._prepare_spec_message(session,
- "spec2.spec"))
-
- def _internal_remote_module(self, function_lambda, parameter,
- fill_other_messages):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.remove_remote_config("Spec2")
- function = function_lambda(mccs)
-
- self.assertRaises(ModuleCCSessionError, mccs.get_remote_config_value, "Spec2", "item1")
-
- self.assertFalse("Spec2" in fake_session.subscriptions)
- fill_other_messages(fake_session)
- fake_session.group_sendmsg(None, 'Spec2')
- rmodname = function(parameter)
- self.assertTrue("Spec2" in fake_session.subscriptions)
- self.assertEqual("Spec2", rmodname)
- self.assertRaises(isc.cc.data.DataNotFoundError, mccs.get_remote_config_value, rmodname, "asdf")
- value, default = mccs.get_remote_config_value(rmodname, "item1")
- self.assertEqual(1, value)
- self.assertEqual(True, default)
-
- mccs.remove_remote_config(rmodname)
- self.assertFalse("Spec2" in fake_session.subscriptions)
- self.assertRaises(ModuleCCSessionError, mccs.get_remote_config_value, "Spec2", "item1")
-
- # test if unsubscription is also sent when object is deleted
- fill_other_messages(fake_session)
- fake_session.group_sendmsg({'result' : [0]}, 'Spec2')
- rmodname = function(parameter)
- self.assertTrue("Spec2" in fake_session.subscriptions)
- mccs = None
- function = None
- self.assertFalse("Spec2" in fake_session.subscriptions)
-
- def test_remote_module(self):
- """
- Test we can add a remote config and get the configuration.
- Remote module specified by the spec file name.
- """
- self._common_remote_module_test(self._internal_remote_module)
-
- def test_remote_module_by_name(self):
- """
- Test we can add a remote config and get the configuration.
- Remote module specified its name.
- """
- self._common_remote_module_by_name_test(self._internal_remote_module)
-
- def _internal_remote_module_with_custom_config(self, function_lambda,
- parameter,
- fill_other_messages):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- function = function_lambda(mccs)
- # override the default config value for "item1". add_remote_config[_by_name]()
- # should incorporate the overridden value, and we should be able to
- # get it via get_remote_config_value().
- fill_other_messages(fake_session)
- fake_session.group_sendmsg({'result': [0, {"item1": 10}]}, 'Spec2')
- rmodname = function(parameter)
- value, default = mccs.get_remote_config_value(rmodname, "item1")
- self.assertEqual(10, value)
- self.assertEqual(False, default)
-
- def test_remote_module_with_custom_config(self):
- """
- Test the config of module will load non-default values on
- initialization.
- Remote module specified by the spec file name.
- """
- self._common_remote_module_test(
- self._internal_remote_module_with_custom_config)
-
- def test_remote_module_by_name_with_custom_config(self):
- """
- Test the config of module will load non-default values on
- initialization.
- Remote module its name.
- """
- self._common_remote_module_by_name_test(
- self._internal_remote_module_with_custom_config)
-
- def _internal_ignore_command_remote_module(self, function_lambda, param,
- fill_other_messages):
- # Create a Spec1 module and subscribe to remote config for Spec2
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.set_command_handler(self.my_command_handler_ok)
- function = function_lambda(mccs)
- fill_other_messages(fake_session)
- fake_session.group_sendmsg(None, 'Spec2')
- rmodname = function(param)
-
- # remove the commands from queue
- while len(fake_session.message_queue) > 0:
- fake_session.get_message("ConfigManager")
-
- # check if the command for the module itself is received
- cmd = isc.config.ccsession.create_command("just_some_command", { 'foo': 'a' })
- fake_session.group_sendmsg(cmd, 'Spec1')
- self.assertEqual(len(fake_session.message_queue), 1)
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 1)
- self.assertEqual({'result': [ 0 ]},
- fake_session.get_message('Spec1', None))
-
- # check if the command for the other module is ignored
- cmd = isc.config.ccsession.create_command("just_some_command", { 'foo': 'a' })
- fake_session.group_sendmsg(cmd, 'Spec2')
- self.assertEqual(len(fake_session.message_queue), 1)
- mccs.check_command()
- self.assertEqual(len(fake_session.message_queue), 0)
-
- def test_ignore_command_remote_module(self):
- """
- Test that commands for remote modules aren't handled.
- Remote module specified by the spec file name.
- """
- self._common_remote_module_test(
- self._internal_ignore_command_remote_module)
-
- def test_ignore_command_remote_module_by_name(self):
- """
- Test that commands for remote modules aren't handled.
- Remote module specified by its name.
- """
- self._common_remote_module_by_name_test(
- self._internal_ignore_command_remote_module)
-
- def _internal_check_command_without_recvmsg_remote_module(self,
- function_lambda,
- param,
- fill_other_messages):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- function = function_lambda(mccs)
- self.assertEqual(len(fake_session.message_queue), 0)
-
- fill_other_messages(fake_session)
- fake_session.group_sendmsg(None, 'Spec2')
- rmodname = function(param)
- if (len(fake_session.message_queue) == 2):
- self.assertEqual({'command': ['get_module_spec',
- {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual(len(fake_session.message_queue), 0)
-
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec2': { 'item1': 2 }})
- env = { 'group':'Spec2', 'from':None }
- self.assertEqual(len(fake_session.message_queue), 0)
- mccs.check_command_without_recvmsg(cmd, env)
- self.assertEqual(len(fake_session.message_queue), 0)
-
- def test_check_command_without_recvmsg_remote_module(self):
- """
- Test updates on remote module.
- The remote module is specified by the spec file name.
- """
- self._common_remote_module_test(
- self._internal_check_command_without_recvmsg_remote_module)
-
- def test_check_command_without_recvmsg_remote_module_by_name(self):
- """
- Test updates on remote module.
- The remote module is specified by its name.
- """
- self._common_remote_module_by_name_test(
- self._internal_check_command_without_recvmsg_remote_module)
-
- def _internal_check_command_without_recvmsg_remote_module2(self,
- function_lambda,
- param,
- fill_other_messages):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- function = function_lambda(mccs)
- self.assertEqual(len(fake_session.message_queue), 0)
-
- fill_other_messages(fake_session)
- fake_session.group_sendmsg(None, 'Spec2')
- rmodname = function(param)
- if (len(fake_session.message_queue) == 2):
- self.assertEqual({'command': ['get_module_spec',
- {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual({'command': ['get_config', {'module_name': 'Spec2'}]},
- fake_session.get_message('ConfigManager', None))
- self.assertEqual(len(fake_session.message_queue), 0)
-
- cmd = isc.config.ccsession.create_command(isc.config.ccsession.COMMAND_CONFIG_UPDATE, { 'Spec3': { 'item1': 2 }})
- env = { 'group':'Spec3', 'from':None }
- self.assertEqual(len(fake_session.message_queue), 0)
- mccs.check_command_without_recvmsg(cmd, env)
- self.assertEqual(len(fake_session.message_queue), 0)
-
- def test_check_command_without_recvmsg_remote_module2(self):
- """
- Test updates on remote module.
- The remote module is specified by the spec file name.
- """
- self._common_remote_module_test(
- self._internal_check_command_without_recvmsg_remote_module2)
-
- def test_check_command_without_recvmsg_remote_module_by_name2(self):
- """
- Test updates on remote module.
- The remote module is specified by its name.
- """
- self._common_remote_module_by_name_test(
- self._internal_check_command_without_recvmsg_remote_module2)
-
- def _internal_remote_module_bad_config(self, function_lambda, parameter,
- fill_other_messages):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- function = function_lambda(mccs)
- # Provide wrong config data. It should be rejected.
- fill_other_messages(fake_session)
- fake_session.group_sendmsg({'result': [0, {"bad_item": -1}]}, 'Spec2')
- self.assertRaises(isc.config.ModuleCCSessionError,
- function, parameter)
-
- def test_remote_module_bad_config(self):
- """
- Test the remote module rejects bad config data.
- """
- self._common_remote_module_test(
- self._internal_remote_module_bad_config)
-
- def test_remote_module_by_name_bad_config(self):
- """
- Test the remote module rejects bad config data.
- """
- self._common_remote_module_by_name_test(
- self._internal_remote_module_bad_config)
-
- def _internal_remote_module_error_response(self, function_lambda,
- parameter, fill_other_messages):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- function = function_lambda(mccs)
- # Provide wrong config data. It should be rejected.
- fill_other_messages(fake_session)
- fake_session.group_sendmsg({'result': [1, "An error, and I mean it!"]},
- 'Spec2')
- self.assertRaises(isc.config.ModuleCCSessionError,
- function, parameter)
-
- def test_remote_module_bad_config(self):
- """
- Test the remote module complains if there's an error response."
- """
- self._common_remote_module_test(
- self._internal_remote_module_error_response)
-
- def test_remote_module_by_name_bad_config(self):
- """
- Test the remote module complains if there's an error response."
- """
- self._common_remote_module_by_name_test(
- self._internal_remote_module_error_response)
-
- def test_remote_module_bad_config(self):
- """
- Test the remote module rejects bad config data.
- """
- self._common_remote_module_by_name_test(
- self._internal_remote_module_bad_config)
-
- def test_module_name_mismatch(self):
- fake_session = FakeModuleCCSession()
- mccs = self.create_session("spec1.spec", None, None, fake_session)
- mccs.set_config_handler(self.my_config_handler_ok)
- self._prepare_spec_message(fake_session, 'spec1.spec')
- self.assertRaises(isc.config.ModuleCCSessionError,
- mccs.add_remote_config_by_name, "Spec2")
-
- def test_logconfig_handler(self):
- # test whether default_logconfig_handler reacts nicely to
- # bad data. We assume the actual logger output is tested
- # elsewhere
- self.assertRaises(TypeError, default_logconfig_handler);
- self.assertRaises(TypeError, default_logconfig_handler, 1);
-
- spec = isc.config.module_spec_from_file(
- path_search('logging.spec', bind10_config.PLUGIN_PATHS))
- config_data = ConfigData(spec)
-
- self.assertRaises(TypeError, default_logconfig_handler, 1, config_data)
-
- default_logconfig_handler({}, config_data)
-
- # Wrong data should not raise, but simply not be accepted
- # This would log a lot of errors, so we may want to suppress that later
- default_logconfig_handler({ "bad_data": "indeed" }, config_data)
- default_logconfig_handler({ "bad_data": 1}, config_data)
- default_logconfig_handler({ "bad_data": 1123 }, config_data)
- default_logconfig_handler({ "bad_data": True }, config_data)
- default_logconfig_handler({ "bad_data": False }, config_data)
- default_logconfig_handler({ "bad_data": 1.1 }, config_data)
- default_logconfig_handler({ "bad_data": [] }, config_data)
- default_logconfig_handler({ "bad_data": [[],[],[[1, 3, False, "foo" ]]] },
- config_data)
- default_logconfig_handler({ "bad_data": [ 1, 2, { "b": { "c": "d" } } ] },
- config_data)
-
- # Try a correct config
- log_conf = {"loggers":
- [{"name": "b10-xfrout", "output_options":
- [{"output": "/tmp/bind10.log",
- "destination": "file",
- "flush": True}]}]}
- default_logconfig_handler(log_conf, config_data)
-
-class fakeData:
- def decode(self):
- return "{}";
-
-class fakeAnswer:
- def read(self):
- return fakeData();
-
-class fakeUIConn():
- def __init__(self):
- self.get_answers = {}
- self.post_answers = {}
-
- def set_get_answer(self, name, answer):
- self.get_answers[name] = answer
-
- def set_post_answer(self, name, answer):
- self.post_answers[name] = answer
-
- def send_GET(self, name, arg = None):
- if name in self.get_answers:
- return self.get_answers[name]
- else:
- return {}
-
- def send_POST(self, name, arg = None):
- if name in self.post_answers:
- return self.post_answers[name]
- else:
- return fakeAnswer()
-
-
-class TestUIModuleCCSession(unittest.TestCase):
- def setUp(self):
- if 'CONFIG_TESTDATA_PATH' in os.environ:
- self.data_path = os.environ['CONFIG_TESTDATA_PATH']
- else:
- self.data_path = "../../../testdata"
-
- def spec_file(self, file):
- return self.data_path + os.sep + file
-
- def create_uccs(self, fake_conn, specfile="spec2.spec"):
- module_spec = isc.config.module_spec_from_file(self.spec_file(specfile))
- fake_conn.set_get_answer('/module_spec', { module_spec.get_module_name(): module_spec.get_full_spec()})
- fake_conn.set_get_answer('/config_data', { 'version': BIND10_CONFIG_DATA_VERSION })
- return UIModuleCCSession(fake_conn)
-
- def create_uccs_named_set(self, fake_conn):
- module_spec = isc.config.module_spec_from_file(self.spec_file("spec32.spec"))
- fake_conn.set_get_answer('/module_spec', { module_spec.get_module_name(): module_spec.get_full_spec()})
- fake_conn.set_get_answer('/config_data', { 'version': BIND10_CONFIG_DATA_VERSION })
- return UIModuleCCSession(fake_conn)
-
- def create_uccs_listtest(self, fake_conn):
- module_spec = isc.config.module_spec_from_file(self.spec_file("spec39.spec"))
- fake_conn.set_get_answer('/module_spec', { module_spec.get_module_name(): module_spec.get_full_spec()})
- fake_conn.set_get_answer('/config_data', { 'version': BIND10_CONFIG_DATA_VERSION })
- return UIModuleCCSession(fake_conn)
-
- def test_init(self):
- fake_conn = fakeUIConn()
- fake_conn.set_get_answer('/module_spec', {})
- fake_conn.set_get_answer('/config_data', { 'version': BIND10_CONFIG_DATA_VERSION })
- uccs = UIModuleCCSession(fake_conn)
- self.assertEqual({}, uccs._specifications)
- self.assertEqual({ 'version': BIND10_CONFIG_DATA_VERSION}, uccs._current_config)
-
- module_spec = isc.config.module_spec_from_file(self.spec_file("spec2.spec"))
- fake_conn.set_get_answer('/module_spec', { module_spec.get_module_name(): module_spec.get_full_spec()})
- fake_conn.set_get_answer('/config_data', { 'version': BIND10_CONFIG_DATA_VERSION })
- uccs = UIModuleCCSession(fake_conn)
- self.assertEqual(module_spec._module_spec, uccs._specifications['Spec2']._module_spec)
-
- fake_conn.set_get_answer('/config_data', { 'version': 123123 })
- self.assertRaises(ModuleCCSessionError, UIModuleCCSession, fake_conn)
-
- def test_request_specifications(self):
- module_spec1 = isc.config.module_spec_from_file(
- self.spec_file("spec1.spec"))
- module_spec_dict1 = { "module_spec": module_spec1.get_full_spec() }
- module_spec2 = isc.config.module_spec_from_file(
- self.spec_file("spec2.spec"))
- module_spec_dict2 = { "module_spec": module_spec2.get_full_spec() }
-
- fake_conn = fakeUIConn()
- # Set the first one in the answer
- fake_conn.set_get_answer('/module_spec', module_spec_dict1)
- fake_conn.set_get_answer('/config_data',
- { 'version': BIND10_CONFIG_DATA_VERSION })
- uccs = UIModuleCCSession(fake_conn)
-
- # We should now have the first one, but not the second.
- self.assertTrue("Spec1" in uccs._specifications)
- self.assertEqual(module_spec1.get_full_spec(),
- uccs._specifications["Spec1"].get_full_spec())
- self.assertFalse("Spec2" in uccs._specifications)
-
- # Now set an answer where only the second one is present
- fake_conn.set_get_answer('/module_spec', module_spec_dict2)
-
- uccs.request_specifications()
-
- # Now Spec1 should have been removed, and spec2 should be there
- self.assertFalse("Spec1" in uccs._specifications)
- self.assertTrue("Spec2" in uccs._specifications)
- self.assertEqual(module_spec2.get_full_spec(),
- uccs._specifications["Spec2"].get_full_spec())
-
- def test_add_remove_value(self):
- fake_conn = fakeUIConn()
- uccs = self.create_uccs(fake_conn)
-
- self.assertRaises(isc.cc.data.DataNotFoundError, uccs.add_value, 1, "a")
- self.assertRaises(isc.cc.data.DataNotFoundError, uccs.add_value, "no_such_item", "a")
- self.assertRaises(isc.cc.data.DataNotFoundError, uccs.remove_value, 1, "a")
- self.assertRaises(isc.cc.data.DataNotFoundError, uccs.remove_value, "no_such_item", "a")
- # add and remove should raise DataNotFoundError when used with items
- # that are not a list or named_set (more importantly, they should
- # not raise TypeError)
- self.assertRaises(isc.cc.data.DataTypeError, uccs.add_value, "Spec2/item1", "a")
- self.assertRaises(isc.cc.data.DataTypeError, uccs.remove_value, "Spec2/item1", "a")
- self.assertRaises(isc.cc.data.DataTypeError, uccs.add_value, "Spec2", "")
- self.assertRaises(isc.cc.data.DataTypeError, uccs.remove_value, "Spec2", "")
-
- self.assertEqual({}, uccs._local_changes)
- uccs.add_value("Spec2/item5", "foo")
- self.assertEqual({'Spec2': {'item5': ['a', 'b', 'foo']}}, uccs._local_changes)
- uccs.remove_value("Spec2/item5", "foo")
- self.assertEqual({'Spec2': {'item5': ['a', 'b']}}, uccs._local_changes)
- uccs._local_changes = {'Spec2': {'item5': []}}
- uccs.remove_value("Spec2/item5", "foo")
- uccs.add_value("Spec2/item5", "foo")
- self.assertEqual({'Spec2': {'item5': ['foo']}}, uccs._local_changes)
- self.assertRaises(isc.cc.data.DataAlreadyPresentError,
- uccs.add_value, "Spec2/item5", "foo")
- self.assertEqual({'Spec2': {'item5': ['foo']}}, uccs._local_changes)
- self.assertRaises(isc.cc.data.DataNotFoundError,
- uccs.remove_value, "Spec2/item5[123]", None)
- uccs.remove_value("Spec2/item5[0]", None)
- self.assertEqual({'Spec2': {'item5': []}}, uccs._local_changes)
- uccs.add_value("Spec2/item5", None);
- self.assertEqual({'Spec2': {'item5': ['']}}, uccs._local_changes)
- # Intending to empty a list element, but forget specifying the index.
- self.assertRaises(isc.cc.data.DataTypeError,
- uccs.remove_value, "Spec2/item5", None)
-
-
- # Check that the difference between no default and default = null
- # is recognized
- def test_default_null(self):
- fake_conn = fakeUIConn()
- uccs = self.create_uccs(fake_conn, "spec40.spec")
- (value, status) = uccs.get_value("/Spec40/item2")
- self.assertIsNone(value)
- self.assertEqual(uccs.NONE, status)
- (value, status) = uccs.get_value("/Spec40/item3")
- self.assertIsNone(value)
- self.assertEqual(uccs.DEFAULT, status)
-
- # Test adding and removing values for type = any
- def test_add_remove_value_any(self):
- fake_conn = fakeUIConn()
- uccs = self.create_uccs(fake_conn, "spec40.spec")
-
- # Test item set of basic types
- items = [ 1234, "foo", True, False ]
- items_as_str = [ '1234', 'foo', 'true', 'false' ]
-
- def test_fails():
- self.assertRaises(isc.cc.data.DataTypeError, uccs.add_value, "Spec40/item1", "foo")
- self.assertRaises(isc.cc.data.DataTypeError, uccs.add_value, "Spec40/item1", "foo", "bar")
- self.assertRaises(isc.cc.data.DataTypeError, uccs.remove_value, "Spec40/item1", "foo")
- self.assertRaises(isc.cc.data.DataTypeError, uccs.remove_value, "Spec40/item1[0]", None)
-
- # A few helper functions to perform a number of tests
- # (to repeat the same test for nested data)
- def check_list(identifier):
- for item in items_as_str:
- uccs.add_value(identifier, item)
- self.assertEqual((items, 1), uccs.get_value(identifier))
-
- # Removing from list should work in both ways
- uccs.remove_value(identifier, "foo")
- uccs.remove_value(identifier + "[1]", None)
- self.assertEqual(([1234, False], 1), uccs.get_value(identifier))
-
- # As should item indexing
- self.assertEqual((1234, 1), uccs.get_value(identifier + "[0]"))
- self.assertEqual((False, 1), uccs.get_value(identifier + "[1]"))
-
- def check_named_set(identifier):
- for item in items_as_str:
- # use string version as key as well
- uccs.add_value(identifier, item, item)
-
- self.assertEqual((1234, 1), uccs.get_value(identifier + "/1234"))
- self.assertEqual((True, 1), uccs.get_value(identifier + "/true"))
-
- for item in items_as_str:
- # use string version as key as well
- uccs.remove_value(identifier, item)
-
-
- # should fail when set to value of primitive type
- for item in items:
- uccs.set_value("Spec40/item1", item)
- test_fails()
-
- # When set to list, add and remove should work, and its elements
- # should be considered of type 'any' themselves.
- uccs.set_value("Spec40/item1", [])
- check_list("Spec40/item1")
-
- # When set to dict, it should have the behaviour of a named set
- uccs.set_value("Spec40/item1", {})
- check_named_set("Spec40/item1")
-
- # And, or course, we may need nesting.
- uccs.set_value("Spec40/item1", { "foo": {}, "bar": [] })
- check_named_set("Spec40/item1/foo")
- check_list("Spec40/item1/bar")
- uccs.set_value("Spec40/item1", [ {}, [] ] )
- check_named_set("Spec40/item1[0]")
- check_list("Spec40/item1[1]")
- uccs.set_value("Spec40/item1", [[[[[[]]]]]] )
- check_list("Spec40/item1[0][0][0][0][0]")
- uccs.set_value("Spec40/item1", { 'a': { 'a': { 'a': {} } } } )
- check_named_set("Spec40/item1/a/a/a")
-
- def test_add_dup_value(self):
- fake_conn = fakeUIConn()
- uccs = self.create_uccs_listtest(fake_conn)
-
- uccs.add_value("Spec39/list")
- self.assertRaises(isc.cc.data.DataAlreadyPresentError, uccs.add_value,
- "Spec39/list")
-
- def test_add_remove_value_named_set(self):
- fake_conn = fakeUIConn()
- uccs = self.create_uccs_named_set(fake_conn)
- value, status = uccs.get_value("/Spec32/named_set_item")
- self.assertEqual({'a': 1, 'b': 2}, value)
-
- # make sure that removing from default actually removes it
- uccs.remove_value("/Spec32/named_set_item", "a")
- value, status = uccs.get_value("/Spec32/named_set_item")
- self.assertEqual({'b': 2}, value)
- self.assertEqual(uccs.LOCAL, status)
-
- # ok, put it back now
- uccs.add_value("/Spec32/named_set_item", "a")
- uccs.set_value("/Spec32/named_set_item/a", 1)
-
- uccs.add_value("/Spec32/named_set_item", "foo")
- value, status = uccs.get_value("/Spec32/named_set_item")
- self.assertEqual({'a': 1, 'b': 2, 'foo': 3}, value)
-
- uccs.remove_value("/Spec32/named_set_item", "a")
- uccs.remove_value("/Spec32/named_set_item", "foo")
- value, status = uccs.get_value("/Spec32/named_set_item")
- self.assertEqual({'b': 2}, value)
-
- uccs.set_value("/Spec32/named_set_item/c", 5)
- value, status = uccs.get_value("/Spec32/named_set_item")
- self.assertEqual({"b": 2, "c": 5}, value)
-
- self.assertRaises(isc.cc.data.DataNotFoundError,
- uccs.set_value,
- "/Spec32/named_set_item/no_such_item/a",
- 4)
- self.assertRaises(isc.cc.data.DataNotFoundError,
- uccs.remove_value, "/Spec32/named_set_item",
- "no_such_item")
- self.assertRaises(isc.cc.data.DataAlreadyPresentError,
- uccs.add_value, "/Spec32/named_set_item", "c")
-
- def test_set_value_named_set(self):
- fake_conn = fakeUIConn()
- uccs = self.create_uccs_named_set(fake_conn)
- value, status = uccs.get_value("/Spec32/named_set_item2")
- self.assertEqual({}, value)
- self.assertEqual(status, uccs.DEFAULT)
-
- # Try setting a value that is optional but has no default
- uccs.add_value("/Spec32/named_set_item2", "new1")
- uccs.set_value("/Spec32/named_set_item2/new1/first", 3)
- # Different method to add a new element
- uccs.set_value("/Spec32/named_set_item2/new2", { "second": 4 })
-
- value, status = uccs.get_value("/Spec32/named_set_item2")
- self.assertEqual({ "new1": {"first": 3 }, "new2": {"second": 4}},
- value)
- self.assertEqual(status, uccs.LOCAL)
-
- uccs.set_value("/Spec32/named_set_item2/new1/second", "foo")
-
- value, status = uccs.get_value("/Spec32/named_set_item2")
- self.assertEqual({ "new1": {"first": 3, "second": "foo" },
- "new2": {"second": 4}},
- value)
- self.assertEqual(status, uccs.LOCAL)
-
- # make sure using a bad name still fails
- self.assertRaises(isc.cc.data.DataNotFoundError, uccs.set_value,
- "/Spec32/named_set_item2/doesnotexist/first", 3)
-
-
-
- def test_commit(self):
- fake_conn = fakeUIConn()
- uccs = self.create_uccs(fake_conn)
- uccs.commit()
- uccs._local_changes = {'Spec2': {'item5': [ 'a' ]}}
- uccs.commit()
-
-if __name__ == '__main__':
- isc.log.init("bind10")
- unittest.main()
-
+++ /dev/null
-# Copyright (C) 2010 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-#
-# Tests for the configuration manager module
-#
-
-import unittest
-import os
-from isc.config.cfgmgr import *
-from isc.config import config_data
-from unittest_fakesession import FakeModuleCCSession
-
-class TestConfigManagerData(unittest.TestCase):
- def setUp(self):
- self.data_path = os.environ['CONFIG_TESTDATA_PATH']
- self.writable_data_path = os.environ['CONFIG_WR_TESTDATA_PATH']
- self.config_manager_data = ConfigManagerData(self.writable_data_path,
- file_name="b10-config.db")
- self.assertTrue(self.config_manager_data)
-
- def test_abs_file(self):
- """
- Test what happens if we give the config manager an absolute path.
- It shouldn't append the data path to it.
- """
- abs_path = self.data_path + os.sep + "b10-config-imaginary.db"
- data = ConfigManagerData(self.data_path, abs_path)
- self.assertEqual(abs_path, data.db_filename)
- self.assertEqual(self.data_path, data.data_path)
-
- def test_init(self):
- self.assertEqual(self.config_manager_data.data['version'],
- config_data.BIND10_CONFIG_DATA_VERSION)
- self.assertEqual(self.config_manager_data.data_path,
- self.writable_data_path)
- self.assertEqual(self.config_manager_data.db_filename,
- self.writable_data_path + os.sep + "b10-config.db")
-
- def test_check_for_updates_up_to_date(self):
- # This should automatically give an up-to-date version
- file_config = ConfigManagerData.read_from_file(
- self.writable_data_path, "b10-config.db").data
- updated_config = ConfigManagerData.check_for_updates(file_config)
- self.assertEqual(file_config, updated_config)
-
- def test_check_for_updates_from_1(self):
- config = { "version": 1,
- "foo": "bar",
- "something": [ 1, 2, 3 ] }
- updated = ConfigManagerData.check_for_updates(config)
- config['version'] = config_data.BIND10_CONFIG_DATA_VERSION
- self.assertEqual(config, updated)
-
- def test_check_for_updates_from_2(self):
- # No 'Boss' present, no change (except version)
- config = { "version": 2,
- "foo": "bar",
- "something": [ 1, 2, 3 ] }
- updated = ConfigManagerData.check_for_updates(config)
- config['version'] = config_data.BIND10_CONFIG_DATA_VERSION
- self.assertEqual(config, updated)
-
- # With Boss, should be changed to 'Init'
- config = { "version": 2,
- "Boss": { "some config": 1 },
- "something": [ 1, 2, 3 ] }
- updated = ConfigManagerData.check_for_updates(config)
- config = { "version": config_data.BIND10_CONFIG_DATA_VERSION,
- "Init": { "some config": 1 },
- "something": [ 1, 2, 3 ] }
- self.assertEqual(config, updated)
-
- # With Boss AND Init, no change
- config = { "version": 2,
- "Boss": { "some config": 1 },
- "Init": { "some other config": 1 },
- "something": [ 1, 2, 3 ] }
- updated = ConfigManagerData.check_for_updates(config)
- config['version'] = config_data.BIND10_CONFIG_DATA_VERSION
- self.assertEqual(config, updated)
-
- def test_read_from_file(self):
- ConfigManagerData.read_from_file(self.writable_data_path, "b10-config.db")
- self.assertRaises(ConfigManagerDataEmpty,
- ConfigManagerData.read_from_file,
- "doesnotexist", "b10-config.db")
- self.assertRaises(ConfigManagerDataReadError,
- ConfigManagerData.read_from_file,
- self.data_path, "b10-config-bad1.db")
- self.assertRaises(ConfigManagerDataReadError,
- ConfigManagerData.read_from_file,
- self.data_path, "b10-config-bad2.db")
- self.assertRaises(ConfigManagerDataReadError,
- ConfigManagerData.read_from_file,
- self.data_path, "b10-config-bad3.db")
- self.assertRaises(ConfigManagerDataReadError,
- ConfigManagerData.read_from_file,
- self.data_path, "b10-config-bad4.db")
-
- def test_write_to_file(self):
- output_file_name = "b10-config-write-test"
- self.config_manager_data.write_to_file(output_file_name)
- new_config = ConfigManagerData(self.data_path, output_file_name)
- self.assertEqual(self.config_manager_data, new_config)
- os.remove(output_file_name)
-
- def check_existence(self, files, should_exist=[], should_not_exist=[]):
- """Helper function for test_rename_config_file.
- Arguments:
- files: array of file names to check.
- should_exist: array of indices, the files in 'files' with these
- indices should exist.
- should_not_exist: array of indices, the files in 'files' with
- these indices should not exist."""
- for n in should_exist:
- self.assertTrue(os.path.exists(files[n]))
- for n in should_not_exist:
- self.assertFalse(os.path.exists(files[n]))
-
- def test_rename_config_file(self):
- # test file names, put in array for easy cleanup
- filenames = [ "b10-config-rename-test",
- "b10-config-rename-test.bak",
- "b10-config-rename-test.bak.1",
- "b10-config-rename-test.bak.2" ]
-
- for filename in filenames:
- if os.path.exists(filename):
- os.remove(filename)
-
- # The original does not exist, so the new one should not be created
- self.config_manager_data.rename_config_file(filenames[0])
- self.check_existence(filenames, [], [0, 1, 2, 3])
-
- # now create a file to rename, and call rename again
- self.config_manager_data.write_to_file(filenames[0])
- self.config_manager_data.rename_config_file(filenames[0])
- self.check_existence(filenames, [1], [0, 2, 3])
-
- # If backup already exists, give it a new name automatically
- self.config_manager_data.write_to_file(filenames[0])
- self.config_manager_data.rename_config_file(filenames[0])
- self.check_existence(filenames, [1, 2], [0, 3])
-
- # If backup already exists, give it a new name automatically with
- # increasing postfix
- self.config_manager_data.write_to_file(filenames[0])
- self.config_manager_data.rename_config_file(filenames[0])
- self.check_existence(filenames, [1, 2, 3], [0])
-
- # Test with explicit renamed file argument
- self.config_manager_data.rename_config_file(filenames[1],
- filenames[0])
- self.check_existence(filenames, [0, 2, 3], [1])
-
- # clean up again to be nice
- for filename in filenames:
- if os.path.exists(filename):
- os.remove(filename)
-
- def test_equality(self):
- # tests the __eq__ function. Equality is only defined
- # by equality of the .data element. If data_path or db_filename
- # are different, but the contents are the same, it's still
- # considered equal
- cfd1 = ConfigManagerData(self.data_path, file_name="b10-config.db")
- cfd2 = ConfigManagerData(self.data_path, file_name="b10-config.db")
- self.assertEqual(cfd1, cfd2)
- cfd2.data_path = "some/unknown/path"
- self.assertEqual(cfd1, cfd2)
- cfd2.db_filename = "bad_file.name"
- self.assertEqual(cfd1, cfd2)
- cfd2.data['test'] = { 'a': [ 1, 2, 3]}
- self.assertNotEqual(cfd1, cfd2)
-
-
-class TestConfigManager(unittest.TestCase):
-
- def setUp(self):
- self.data_path = os.environ['CONFIG_TESTDATA_PATH']
- self.writable_data_path = os.environ['CONFIG_WR_TESTDATA_PATH']
- self.fake_session = FakeModuleCCSession()
- self.cm = ConfigManager(self.writable_data_path,
- database_filename="b10-config.db",
- session=self.fake_session)
- self.name = "TestModule"
- self.spec = isc.config.module_spec_from_file(self.data_path + os.sep + "/spec2.spec")
-
- def test_paths(self):
- """
- Test data_path and database filename is passed through to
- underlying ConfigManagerData.
- """
- cm = ConfigManager("datapath", "filename", self.fake_session)
- self.assertEqual("datapath" + os.sep + "filename",
- cm.config.db_filename)
- # It should preserve it while reading
- cm.read_config()
- self.assertEqual("datapath" + os.sep + "filename",
- cm.config.db_filename)
-
- def test_init(self):
- self.assertEqual(self.cm.module_specs, {})
- self.assertEqual(self.cm.data_path, self.writable_data_path)
- self.assertIsNotNone(self.cm.config)
- self.assertTrue(self.fake_session.has_subscription("ConfigManager"))
- self.assertTrue(self.fake_session.has_subscription("Init", "ConfigManager"))
- self.assertFalse(self.cm.running)
-
- def test_notify_b10_init(self):
- self.cm.notify_b10_init()
- msg = self.fake_session.get_message("Init", None)
- self.assertTrue(msg)
- # this one is actually wrong, but 'current status quo'
- self.assertEqual(msg, {"running": "ConfigManager"})
-
- def test_set_module_spec(self):
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- self.assertNotIn(module_spec.get_module_name(), self.cm.virtual_modules)
-
- def test_remove_module_spec(self):
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.remove_module_spec(module_spec.get_module_name())
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.assertNotIn(module_spec.get_module_name(), self.cm.virtual_modules)
-
- def test_add_remove_virtual_module(self):
- module_spec = isc.config.module_spec.module_spec_from_file(
- self.data_path + os.sep + "spec1.spec")
- check_func = lambda: True
- # Make sure it's not in there before
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.assertNotIn(module_spec.get_module_name(), self.cm.virtual_modules)
- # Add it there
- self.cm.set_virtual_module(module_spec, check_func)
- # Check it's in there
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- self.assertEqual(self.cm.module_specs[module_spec.get_module_name()],
- module_spec)
- self.assertEqual(self.cm.virtual_modules[module_spec.get_module_name()],
- check_func)
- # Remove it again
- self.cm.remove_module_spec(module_spec.get_module_name())
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.assertNotIn(module_spec.get_module_name(), self.cm.virtual_modules)
-
- def test_get_module_spec(self):
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- module_spec2 = self.cm.get_module_spec(module_spec.get_module_name())
- self.assertEqual(module_spec.get_full_spec(), module_spec2)
-
- self.assertEqual({}, self.cm.get_module_spec("nosuchmodule"))
-
- def test_get_config_spec(self):
- config_spec = self.cm.get_config_spec()
- self.assertEqual(config_spec, {})
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- config_spec = self.cm.get_config_spec()
- self.assertEqual(config_spec, { 'Spec1': None })
- self.cm.remove_module_spec('Spec1')
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- config_spec = self.cm.get_config_spec()
- self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
- config_spec = self.cm.get_config_spec('Spec2')
- self.assertEqual(config_spec['Spec2'], module_spec.get_config_spec())
-
-
- def test_get_commands_spec(self):
- commands_spec = self.cm.get_commands_spec()
- self.assertEqual(commands_spec, {})
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- commands_spec = self.cm.get_commands_spec()
- self.assertEqual(commands_spec, { 'Spec1': None })
- self.cm.remove_module_spec('Spec1')
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- commands_spec = self.cm.get_commands_spec()
- self.assertEqual(commands_spec['Spec2'], module_spec.get_commands_spec())
- commands_spec = self.cm.get_commands_spec('Spec2')
- self.assertEqual(commands_spec['Spec2'], module_spec.get_commands_spec())
-
- def test_get_statistics_spec(self):
- statistics_spec = self.cm.get_statistics_spec()
- self.assertEqual(statistics_spec, {})
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec1.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- statistics_spec = self.cm.get_statistics_spec()
- self.assertEqual(statistics_spec, { 'Spec1': None })
- self.cm.remove_module_spec('Spec1')
- module_spec = isc.config.module_spec.module_spec_from_file(self.data_path + os.sep + "spec2.spec")
- self.assertNotIn(module_spec.get_module_name(), self.cm.module_specs)
- self.cm.set_module_spec(module_spec)
- self.assertIn(module_spec.get_module_name(), self.cm.module_specs)
- statistics_spec = self.cm.get_statistics_spec()
- self.assertEqual(statistics_spec['Spec2'], module_spec.get_statistics_spec())
- statistics_spec = self.cm.get_statistics_spec('Spec2')
- self.assertEqual(statistics_spec['Spec2'], module_spec.get_statistics_spec())
-
- def test_read_config(self):
- self.assertEqual(self.cm.config.data, {'version': config_data.BIND10_CONFIG_DATA_VERSION})
- self.cm.data_path = "/no_such_path"
- self.cm.read_config()
- self.assertEqual(self.cm.config.data, {'version': config_data.BIND10_CONFIG_DATA_VERSION})
-
- def test_write_config(self):
- # tested in ConfigManagerData tests
- pass
-
- def _handle_msg_helper(self, msg, expected_answer):
- answer = self.cm.handle_msg(msg)
- self.assertEqual(expected_answer, answer)
-
- def test_handle_msg_basic_commands(self):
- # Some basic commands, where not much interaction happens, just
- # check the result
- self._handle_msg_helper({},
- { 'result': [ 1, 'Unknown message format: {}']})
- self._handle_msg_helper("",
- { 'result': [ 1, 'Unknown message format: ']})
- self._handle_msg_helper({ "command": [ "badcommand" ] },
- { 'result': [ 1, "Unknown command: badcommand"]})
- self._handle_msg_helper({ "command": [ "get_commands_spec" ] },
- { 'result': [ 0, {} ]})
- self._handle_msg_helper({ "command": [ "get_statistics_spec" ] },
- { 'result': [ 0, {} ]})
- self._handle_msg_helper({ "command": [ "get_module_spec" ] },
- { 'result': [ 0, {} ]})
- self._handle_msg_helper({ "command": [ "get_module_spec",
- { "module_name": "Spec2" } ] },
- { 'result': [ 0, {} ]})
- self._handle_msg_helper({ "command": [ "get_module_spec", 1 ] },
- {'result': [1, 'Bad get_module_spec command, '+
- 'argument not a dict']})
- self._handle_msg_helper({ "command": [ "get_module_spec", { } ] },
- {'result': [1, 'Bad module_name in '+
- 'get_module_spec command']})
- self._handle_msg_helper({ "command": [ "get_config" ] },
- { 'result': [ 0, { 'version':
- config_data.BIND10_CONFIG_DATA_VERSION }]})
- self._handle_msg_helper({ "command": [ "get_config",
- { "module_name": "nosuchmodule" } ] },
- {'result': [0, { 'version':
- config_data.BIND10_CONFIG_DATA_VERSION }]})
- self._handle_msg_helper({ "command": [ "get_config", 1 ] },
- {'result': [1, 'Bad get_config command, '+
- 'argument not a dict']})
- self._handle_msg_helper({ "command": [ "get_config", { } ] },
- {'result': [1, 'Bad module_name in '+
- 'get_config command']})
- self._handle_msg_helper({ "command": [ "set_config" ] },
- {'result': [1, 'Wrong number of arguments']})
- self._handle_msg_helper({ "command": [ "set_config", [{}]] },
- {'result': [0]})
-
- self.assertEqual(len(self.fake_session.message_queue), 0)
-
- def test_handle_msg_module_and_stats_commands(self):
- self._handle_msg_helper({ "command":
- ["module_spec", self.spec.get_full_spec()]
- },
- {'result': [0]})
- # There should be a message on the queue about the 'new' Spec2 module
- # from ConfigManager to Cmdctl, containing its name and full
- # specification
- self.assertEqual(ccsession.create_command(
- ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
- [ self.spec.get_module_name(),
- self.spec.get_full_spec()]),
- self.fake_session.get_message("Cmdctl", None))
-
- self._handle_msg_helper({ "command": [ "module_spec", { 'foo': 1 } ] },
- {'result': [1, 'Error in data definition: no '+
- 'module_name in module_spec']})
-
- self._handle_msg_helper({ "command": [ "get_module_spec" ] },
- { 'result': [ 0, { self.spec.get_module_name():
- self.spec.get_full_spec() } ]})
- self._handle_msg_helper({ "command": [ "get_module_spec",
- { "module_name" : "Spec2" } ] },
- { 'result': [ 0, self.spec.get_full_spec() ] })
- self._handle_msg_helper({ "command": [ "get_commands_spec" ] },
- { 'result': [ 0, { self.spec.get_module_name():
- self.spec.get_commands_spec()}]})
- self._handle_msg_helper({ "command": [ "get_statistics_spec" ] },
- { 'result': [ 0, { self.spec.get_module_name():
- self.spec.get_statistics_spec()}]})
-
-
- def __test_handle_msg_update_config_helper(self, new_config):
- # Helper function for the common pattern in
- # test_handle_msg_update_config; send 'set config', check for
- # update message, check if config has indeed been updated
-
- my_ok_answer = { 'result': [ 0 ] }
- # Send the 'ok' that cfgmgr expects back to the fake queue first
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
-
- config_version = config_data.BIND10_CONFIG_DATA_VERSION
- self._handle_msg_helper({ "command": [ "set_config",
- [ { "version": config_version,
- self.name: new_config } ] ] },
- my_ok_answer)
-
- # The cfgmgr should have eaten the ok message, and sent out an update
- # message
- self.assertEqual(len(self.fake_session.message_queue), 1)
- self.assertEqual({'command': [ 'config_update', new_config]},
- self.fake_session.get_message(self.name, None))
-
- # Config should have been updated
- self.assertEqual(self.cm.config.data, {self.name: new_config,
- 'version': config_version})
-
- # and the queue should now be empty again
- self.assertEqual(len(self.fake_session.message_queue), 0)
-
- def test_handle_msg_update_config(self):
- # Update the configuration and check results a few times
- # only work the first time
- self.__test_handle_msg_update_config_helper({ "test": 123 })
-
- self.__test_handle_msg_update_config_helper({ "test": 124 })
-
- self.__test_handle_msg_update_config_helper({ "test": 125 })
-
- self.__test_handle_msg_update_config_helper({ "test": 126 })
-
- # Now send an error result (i.e. config not accepted)
- my_bad_answer = { 'result': [1, "bad config"] }
- self.fake_session.group_sendmsg(my_bad_answer, "ConfigManager")
- self._handle_msg_helper({ "command": [ "set_config",
- [self.name, { "test": 127 }] ] },
- my_bad_answer )
- self.assertEqual(len(self.fake_session.message_queue), 1)
- self.assertEqual({'command': [ 'config_update', {'test': 127}]},
- self.fake_session.get_message(self.name, None))
- # Config should not be updated due to the error
- self.cm.read_config()
- self.assertEqual(self.cm.config.data, { self.name: {'test': 126},
- 'version': config_data.BIND10_CONFIG_DATA_VERSION})
-
- self.assertEqual(len(self.fake_session.message_queue), 0)
-
- self.fake_session.group_sendmsg(None, 'ConfigManager')
- self._handle_msg_helper({ "command": [ "set_config", [ ] ] },
- {'result': [1, 'Wrong number of arguments']} )
- self._handle_msg_helper({ "command": [ "set_config",
- [ self.name, { "test": 128 }]]},
- { 'result': [1, 'No answer message '+
- 'from TestModule']} )
-
- # This command should leave a message to the TestModule to update its
- # configuration (since the TestModule did not eat it)
- self.assertEqual(len(self.fake_session.message_queue), 1)
- self.assertEqual(
- ccsession.create_command(ccsession.COMMAND_CONFIG_UPDATE,
- { "test": 128 }),
- self.fake_session.get_message("TestModule", None))
-
- # Make sure queue is empty now
- self.assertEqual(len(self.fake_session.message_queue), 0)
-
- # Shutdown should result in 'ok' answer
- self._handle_msg_helper({ "command":
- ["shutdown"]
- },
- {'result': [0]})
-
- def test_stopping_message(self):
- # Update the system by announcing this module
- self._handle_msg_helper({ "command":
- ["module_spec", self.spec.get_full_spec()]
- },
- {'result': [0]})
-
- # This causes a update to be sent from the ConfigManager to the CmdCtl
- # channel, containing the new module's name and full specification
- self.assertEqual(ccsession.create_command(
- ccsession.COMMAND_MODULE_SPECIFICATION_UPDATE,
- [ self.spec.get_module_name(),
- self.spec.get_full_spec()]),
- self.fake_session.get_message("Cmdctl", None))
-
- # A stopping message should get no response, but should cause another
- # message to be sent, if it is a known module
- self._handle_msg_helper({ "command": [ "stopping",
- { "module_name": "Spec2"}] },
- None)
- self.assertEqual(len(self.fake_session.message_queue), 1)
- self.assertEqual({'command': [ 'module_specification_update',
- ['Spec2', None] ] },
- self.fake_session.get_message("Cmdctl", None))
-
- # but if the 'stopping' module is either unknown or not running,
- # no follow-up message should be sent
- self._handle_msg_helper({ "command":
- [ "stopping",
- { "module_name": "NoSuchModule" } ] },
- None)
- self.assertEqual(len(self.fake_session.message_queue), 0)
-
- def test_set_config_virtual(self):
- """Test that if the module is virtual, we don't send it over the
- message bus, but call the checking function.
- """
- # We run the same three times, with different return values
- def single_test(value, returnFunc, expectedResult):
- # Because closures can't assign to closed-in variables, we pass
- # it through self
- self.called_with = None
- def check_test(new_data):
- self.called_with = new_data
- return returnFunc()
-
- # Register our virtual module
- self.cm.set_virtual_module(self.spec, check_test)
- # The fake session will throw now if it tries to read a response.
- # Handy, we don't need to find a complicated way to check for it.
- result = self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG,
- [self.spec.get_module_name(), { "item1": value }]))
- # Check the correct result is passed and our function was called
- # With correct data
- self.assertEqual(self.called_with['item1'], value)
- self.assertEqual(result, {'result': expectedResult})
- if expectedResult[0] == 0:
- # Check it provided the correct notification
- self.assertEqual(len(self.fake_session.message_queue), 1)
- self.assertEqual({'command': [ 'config_update',
- {'item1': value}]},
- self.fake_session.get_message('Spec2', None))
- # and the queue should now be empty again
- self.assertEqual(len(self.fake_session.message_queue), 0)
- else:
- # It shouldn't send anything on error
- self.assertEqual(len(self.fake_session.message_queue), 0)
-
- # Success
- single_test(5, lambda: None, [0])
- # Graceful error
- single_test(6, lambda: "Just error", [1, "Just error"])
- # Exception from the checker
- def raiser():
- raise Exception("Just exception")
- single_test(7, raiser, [1, "Exception: Just exception"])
-
- def test_set_config_all(self):
- my_ok_answer = { 'result': [ 0 ] }
-
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION},
- self.cm.config.data)
-
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
- self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG, ["test", { "value1": 123 }]))
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION,
- "test": { "value1": 123 }
- }, self.cm.config.data)
-
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
- self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG, ["test", { "value1": 124 }]))
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION,
- "test": { "value1": 124 }
- }, self.cm.config.data)
-
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
- self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG, ["test", { "value2": True }]))
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION,
- "test": { "value1": 124,
- "value2": True
- }
- }, self.cm.config.data)
-
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
- self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG, ["test", { "value3": [ 1, 2, 3 ] }]))
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION,
- "test": { "value1": 124,
- "value2": True,
- "value3": [ 1, 2, 3 ]
- }
- }, self.cm.config.data)
-
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
- self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG, ["test", { "value2": False }]))
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION,
- "test": { "value1": 124,
- "value2": False,
- "value3": [ 1, 2, 3 ]
- }
- }, self.cm.config.data)
-
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
- self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG, ["test", { "value1": None }]))
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION,
- "test": { "value2": False,
- "value3": [ 1, 2, 3 ]
- }
- }, self.cm.config.data)
-
- self.fake_session.group_sendmsg(my_ok_answer, "ConfigManager")
- self.cm.handle_msg(ccsession.create_command(
- ccsession.COMMAND_SET_CONFIG, ["test", { "value3": [ 1 ] }]))
- self.assertEqual({"version": config_data.BIND10_CONFIG_DATA_VERSION,
- "test": { "value2": False,
- "value3": [ 1 ]
- }
- }, self.cm.config.data)
-
-
- def test_run(self):
- self.fake_session.group_sendmsg({ "command": [ "get_commands_spec" ] }, "ConfigManager")
- self.fake_session.group_sendmsg({ "command": [ "get_statistics_spec" ] }, "ConfigManager")
- self.fake_session.group_sendmsg({ "command": [ "stopping", { "module_name": "FooModule" } ] }, "ConfigManager")
- self.fake_session.group_sendmsg({ "command": [ "shutdown" ] }, "ConfigManager")
- self.assertEqual(len(self.fake_session.message_queue), 4)
- self.cm.run()
- # All commands should have been read out by run()
- # Three of the commands should have been responded to, so the queue
- # should now contain three answers
- self.assertEqual(len(self.fake_session.message_queue), 3)
-
-
-if __name__ == '__main__':
- if not 'CONFIG_TESTDATA_PATH' in os.environ or not 'CONFIG_WR_TESTDATA_PATH' in os.environ:
- print("You need to set the environment variable CONFIG_TESTDATA_PATH and CONFIG_WR_TESTDATA_PATH to point to the directory containing the test data files")
- exit(1)
- isc.log.init("unittests")
- isc.log.resetUnitTestRootLogger()
- unittest.main()
PYCOVERAGE_RUN = @PYCOVERAGE_RUN@
PYTESTS_GEN = log_console.py
-PYTESTS_NOGEN = log_test.py
+PYTESTS_NOGEN =
noinst_SCRIPTS = $(PYTESTS_GEN)
EXTRA_DIST = console.out check_output.sh $(PYTESTS_NOGEN)
+++ /dev/null
-# Copyright (C) 2011 Internet Systems Consortium.
-#
-# Permission to use, copy, modify, and distribute this software for any
-# purpose with or without fee is hereby granted, provided that the above
-# copyright notice and this permission notice appear in all copies.
-#
-# THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SYSTEMS CONSORTIUM
-# DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL
-# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL
-# INTERNET SYSTEMS CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT,
-# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING
-# FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
-# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
-# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-
-# This tests it can be loaded, nothing more yet
-import isc.log
-import unittest
-import json
-import sys
-import bind10_config
-from isc.config.ccsession import path_search
-
-class LogDict(unittest.TestCase):
- def setUp(self):
- # We work on a test dictionary now.
- isc.log.set_test_dictionary(True)
- def tearDown(self):
- # Return to the global dictionary
- isc.log.set_test_dictionary(False)
-
- def test_load_msgs(self):
- # Try loading a message and see it's there, but nothing more
- self.assertEqual(isc.log.create_message("ID", "Text"), "ID")
- self.assertEqual(isc.log.get_message("ID"), "Text")
- self.assertEqual(isc.log.get_message("no-ID"), None)
-
-class Manager(unittest.TestCase):
- def tearDown(self):
- isc.log.reset()
-
- def test_init_debug(self):
- # We try calling it now only, as we don't have any other functions
- # to check the outcome by it. Once we add the logger class, we may
- # check more.
- isc.log.init("root", "DEBUG", 50, None)
-
- def test_init_defaults(self):
- # We try calling it now only, as we don't have any other functions
- # to check the outcome by it. Once we add the logger class, we may
- # check more.
- isc.log.init("root")
-
- def test_init_notfound(self):
- # This should not throw, because the C++ one doesn't. Should we really
- # ignore errors like missing file?
- isc.log.init("root", "INFO", 0, "/no/such/file");
-
- def test_init_keywords(self):
- isc.log.init(name="root", severity="DEBUG", debuglevel=50, file=None,
- buffer=True)
- # unknown keyword
- self.assertRaises(TypeError, isc.log.init, name="root", foo="bar")
- # Replace the values for each keyword by a wrong type, one by one
- self.assertRaises(TypeError, isc.log.init, name=1,
- severity="DEBUG", debuglevel=50, file=None,
- buffer=True)
- self.assertRaises(TypeError, isc.log.init, name="root",
- severity=2, debuglevel=50, file=None,
- buffer=True)
- self.assertRaises(TypeError, isc.log.init, name="root",
- severity="DEBUG", debuglevel="50", file=None,
- buffer=True)
- self.assertRaises(TypeError, isc.log.init, name="root",
- severity="DEBUG", debuglevel=50, file=1,
- buffer=True)
- self.assertRaises(TypeError, isc.log.init, name="root",
- severity="DEBUG", debuglevel=50, file=None,
- buffer=None)
-
- def test_log_config_update(self):
- log_spec = json.dumps(isc.config.module_spec_from_file(path_search('logging.spec', bind10_config.PLUGIN_PATHS)).get_full_spec())
-
- self.assertRaises(TypeError, isc.log.log_config_update)
- self.assertRaises(TypeError, isc.log.log_config_update, 1)
- self.assertRaises(TypeError, isc.log.log_config_update, 1, 1)
- self.assertRaises(TypeError, isc.log.log_config_update, 1, 1, 1)
-
- self.assertRaises(TypeError, isc.log.log_config_update, 1, log_spec)
- self.assertRaises(TypeError, isc.log.log_config_update, [], log_spec)
- self.assertRaises(TypeError, isc.log.log_config_update, "foo", log_spec)
- self.assertRaises(TypeError, isc.log.log_config_update, "{ '", log_spec)
-
- # empty should pass
- isc.log.log_config_update("{}", log_spec)
-
- # bad spec
- self.assertRaises(TypeError, isc.log.log_config_update, "{}", json.dumps({"foo": "bar"}))
-
- # Try a correct one
- log_conf = json.dumps({"loggers":
- [{"name": "b10-xfrout", "output_options":
- [{"output": "/tmp/bind10.log",
- "destination": "file",
- "flush": True}]}]})
- isc.log.log_config_update(log_conf, log_spec)
-
-class Logger(unittest.TestCase):
- def tearDown(self):
- isc.log.reset()
-
- def setUp(self):
- isc.log.init("root", "DEBUG", 50)
- self.sevs = ['INFO', 'WARN', 'ERROR', 'FATAL']
- self.TEST_MSG = isc.log.create_message('TEST_MESSAGE', '%1')
-
- # Checks defaults of the logger
- def defaults(self, logger):
- self.assertEqual(logger.get_effective_severity(), "DEBUG")
- self.assertEqual(logger.get_effective_debug_level(), 50)
-
- def test_default_severity(self):
- logger = isc.log.Logger("child")
- self.defaults(logger)
-
- # Try changing the severities little bit
- def test_severity(self):
- logger = isc.log.Logger("child")
- logger.set_severity('DEBUG', 25)
- self.assertEqual(logger.get_effective_severity(), "DEBUG")
- self.assertEqual(logger.get_effective_debug_level(), 25)
- for sev in self.sevs:
- logger.set_severity(sev)
- self.assertEqual(logger.get_effective_severity(), sev)
- self.assertEqual(logger.get_effective_debug_level(), 0)
- # Return to default
- logger.set_severity(None)
- self.defaults(logger)
-
- def test_enabled(self):
- logger = isc.log.Logger("child")
- self.sevs.insert(0, 'DEBUG')
- methods = {
- 'DEBUG': logger.is_debug_enabled,
- 'INFO': logger.is_info_enabled,
- 'WARN': logger.is_warn_enabled,
- 'ERROR': logger.is_error_enabled,
- 'FATAL': logger.is_fatal_enabled
- }
- for sev in self.sevs:
- logger.set_severity(sev)
- enabled = False
- for tested in self.sevs:
- if tested == sev:
- enabled = True
- self.assertEqual(methods[tested](), enabled)
- logger.set_severity('DEBUG', 50)
- self.assertTrue(logger.is_debug_enabled())
- self.assertTrue(logger.is_debug_enabled(0))
- self.assertTrue(logger.is_debug_enabled(50))
- self.assertFalse(logger.is_debug_enabled(99))
-
- def test_invalid_params(self):
- """
- Tests invalid arguments for logging functions. The output is tested
- in check_output.sh.
- """
- logger = isc.log.Logger("child")
- methods = [
- logger.info,
- logger.warn,
- logger.error,
- logger.fatal
- ]
- for meth in methods:
- # Not enough arguments
- self.assertRaises(TypeError, meth)
- # Bad type
- self.assertRaises(TypeError, meth, 1)
- # Too few arguments
- self.assertRaises(TypeError, logger.debug, 42)
- self.assertRaises(TypeError, logger.debug)
- # Bad type
- self.assertRaises(TypeError, logger.debug, "42", "hello")
-
- def test_dbglevel_constants(self):
- """
- Just check a constant to make sure it is defined and is the
- correct value. (The constant chosen has a non-zero value to
- ensure that the code has both define the constant and set its
- value correctly.)
- """
- logger = isc.log.Logger("child")
- self.assertEqual(logger.DBGLVL_COMMAND, 10)
-
- def test_param_reference(self):
- """
- Check whether passing a parameter to a logger causes a reference leak.
- """
- class LogParam:
- def __str__(self):
- return 'LogParam'
- logger = isc.log.Logger("child")
- param = LogParam()
- orig_msgrefcnt = sys.getrefcount(param)
- orig_idrefcnt = sys.getrefcount(self.TEST_MSG)
- logger.info(self.TEST_MSG, param);
- self.assertEqual(sys.getrefcount(self.TEST_MSG), orig_idrefcnt)
- self.assertEqual(sys.getrefcount(param), orig_msgrefcnt)
-
- # intentionally pass an invalid type for debug level. It will
- # result in TypeError. The passed object still shouldn't leak a
- # reference.
- self.assertRaises(TypeError, logger.debug, param, self.TEST_MSG, param)
- self.assertEqual(sys.getrefcount(param), orig_msgrefcnt)
-
- def test_bad_parameter(self):
- # a log parameter cannot be converted to a string object.
- class LogParam:
- def __str__(self):
- raise ValueError("LogParam can't be converted to string")
- logger = isc.log.Logger("child")
- self.assertRaises(ValueError, logger.info, self.TEST_MSG, LogParam())
-
-if __name__ == '__main__':
- unittest.main()