From: Tomek Mrugalski Date: Mon, 2 Jun 2014 19:14:08 +0000 (+0200) Subject: [3413] BIND10 framework removal, part 1 X-Git-Tag: trac3473_base~11^2~13 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=2a4a413f66f13bf2daf85c5a6ce6d89d4b07a0bc;p=thirdparty%2Fkea.git [3413] BIND10 framework removal, part 1 --- diff --git a/configure.ac b/configure.ac index 8e6680bb0b..f39c2575d7 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ # 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 @@ -1415,31 +1415,6 @@ AC_CONFIG_FILES([compatcheck/Makefile 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 @@ -1457,26 +1432,8 @@ AC_CONFIG_FILES([compatcheck/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 @@ -1581,15 +1538,6 @@ AC_CONFIG_FILES([compatcheck/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 diff --git a/src/bin/Makefile.am b/src/bin/Makefile.am index 121d43aad1..8b970585cb 100644 --- a/src/bin/Makefile.am +++ b/src/bin/Makefile.am @@ -1,6 +1,4 @@ # 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 diff --git a/src/bin/bind10/.gitignore b/src/bin/bind10/.gitignore deleted file mode 100644 index 411e4fd9aa..0000000000 --- a/src/bin/bind10/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/run_bind10.sh -/bind10 -/bind10.8 -/b10-init -/b10-init.8 -/init.py diff --git a/src/bin/bind10/Makefile.am b/src/bin/bind10/Makefile.am deleted file mode 100644 index b2e9f053f7..0000000000 --- a/src/bin/bind10/Makefile.am +++ /dev/null @@ -1,64 +0,0 @@ -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) diff --git a/src/bin/bind10/README b/src/bin/bind10/README deleted file mode 100644 index d75c0cdc09..0000000000 --- a/src/bin/bind10/README +++ /dev/null @@ -1,16 +0,0 @@ -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. diff --git a/src/bin/bind10/TODO b/src/bin/bind10/TODO deleted file mode 100644 index 82abdb1bb0..0000000000 --- a/src/bin/bind10/TODO +++ /dev/null @@ -1,14 +0,0 @@ -- 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) - diff --git a/src/bin/bind10/b10-init.xml b/src/bin/bind10/b10-init.xml deleted file mode 100644 index de7832dcfc..0000000000 --- a/src/bin/bind10/b10-init.xml +++ /dev/null @@ -1,519 +0,0 @@ -]> - - - - - - February 5, 2013 - - - - b10-init - 8 - BIND10 - - - - b10-init - BIND 10 Init process - - - - - 2010-2013 - Internet Systems Consortium, Inc. ("ISC") - - - - - - b10-init - - - - - - - - - port - config-filename - directory - - - filename - - - - - - - - - DESCRIPTION - - The b10-init daemon starts up other - BIND 10 required daemons. It handles restarting of exiting - programs and also the shutdown of all managed daemons. - - - - - - - - - ARGUMENTS - - The arguments are as follows: - - - - - - config-filename, - config-filename - - - 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 b10-config.db. - - - - - - - - - - This will create a backup of the existing configuration - file, remove it and start - b10-cfgmgr8 - with the default configuration. - The name of the backup file can be found in the logs - (CFGMGR_BACKED_UP_CONFIG_FILE). - (It will append a number to the backup filename if a - previous backup file exists.) - - - - - - - - port - - - The b10-cmdctl daemon will listen - on this port. - (See - b10-cmdctl8 - for the default.) - - - - - - - directory, - directory - - - The path where BIND 10 programs look for various data files. - Currently only - b10-cfgmgr8 - uses it to locate the configuration file, but the usage - might be extended for other programs and other types of - files. - - - - - file, - file - - - The UNIX domain socket file for the - b10-msgq8 - daemon to use. - The default is - /usr/local/var/bind10/msg_socket. - - - - - - - , - - When this option is passed, b10-init - does not send SIGTERM and SIGKILL signals to modules during - shutdown. (This option was introduced for use during - testing.) - - - - - user, name - - - The username for b10-init to run as. - b10-init must be initially ran as the - root user to use this option. - The default is to run as the current user. - - - - - filename - - If defined, the PID of the b10-init is stored - in this file. - - - - - - - - - The name this process should have in tools like - ps or top. This - is handy if you have multiple versions/installations - of b10-init. - - - - - - - , - - Display more about what is going on for - b10-init and its child processes. - - - - - - wait_time, wait_time - - 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 - wait_time is specified in - seconds and has a default value of 10. - - - - - - - - - - - CONFIGURATION AND COMMANDS - - - The configuration provides settings for components for - b10-init to manage under - /Init/components/. - The default elements are: - - - - - - /Init/components/b10-cmdctl - - - - /Init/components/b10-stats - - - - - - (Note that the startup of b10-sockcreator, - b10-cfgmgr, and b10-msgq - is not configurable. They are hardcoded and b10-init - will not run without them.) - - - - The named sets for components contain the following settings: - - - - - - address - - The name used for communicating to it on the message - bus. - - - - - - kind - - - This defines how required a component is. - The possible settings for kind are: - core (system won't start if it won't - start and b10-init will shutdown if - a core component crashes), - dispensable (b10-init - will restart failing component), - and - needed (b10-init - will shutdown if component won't initially start, but - if crashes later, it will attempt to restart). - This setting is required. - - - - - - - - - priority - - This is an integer. b10-init - will start the components with largest priority numbers first. - - - - - - process - - This is the filename of the executable to be started. - If not defined, then b10-init will - use the component name instead. - - - - - - special - - - This defines if the component is started a special, hardcoded - way. - - - - - - - - - - - The Init configuration commands are: - - - - - - - - - - getstats tells b10-init - to send its statistics data to the b10-stats - daemon. - This is an internal command and not exposed to the administrator. - - - - - ping is used to check the connection with the - b10-init daemon. - It returns the text pong. - - - - show_processes lists the current processes - managed by b10-init. - 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. - - - - - - - shutdown tells b10-init - to shutdown the BIND 10 servers. - It will tell each process it manages to shutdown and, when - complete, b10-init will exit. - - - - - - STATISTICS DATA - - - The statistics data collected by the b10-stats - daemon for Init include: - - - - - - boot_time - - The date and time that the b10-init - process started. - This is represented in ISO 8601 format. - - - - - - - - - FILES - sockcreator-XXXXXX/sockcreator - — - the Unix Domain socket located in a temporary file directory for - b10-sockcreator - - communication. - - - - - SEE ALSO - - - bind108 - , - - bindctl1 - , - - b10-auth8 - , - - b10-cfgmgr8 - , - - b10-cmdctl8 - , - - b10-msgq8 - , - - b10-xfrin8 - , - - b10-xfrout8 - , - - b10-zonemgr8 - , - - b10-stats8 - , - - b10-stats-httpd8 - , - BIND 10 Guide. - - - - - HISTORY - The development of b10-init - was started in October 2009. - It was renamed and its configuration identifier changed - in February 2013. - - - - - AUTHORS - - The b10-init - daemon was initially designed by Shane Kerr of ISC. - - - diff --git a/src/bin/bind10/bind10.in b/src/bin/bind10/bind10.in deleted file mode 100755 index 88c45c9463..0000000000 --- a/src/bin/bind10/bind10.in +++ /dev/null @@ -1,11 +0,0 @@ -#!/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 diff --git a/src/bin/bind10/bind10.xml b/src/bin/bind10/bind10.xml deleted file mode 100644 index 16082f3497..0000000000 --- a/src/bin/bind10/bind10.xml +++ /dev/null @@ -1,76 +0,0 @@ -]> - - - - - - February 5, 2013 - - - - bind10 - 8 - BIND10 - - - - bind10 - BIND 10 start script - - - - - 2013 - Internet Systems Consortium, Inc. ("ISC") - - - - - - bind10 - - - - - - DESCRIPTION - - The bind10 script is a simple wrapper that - starts BIND 10 by running the b10-init daemon. All - options passed to bind10 are directly passed on to - b10-init. - - - - - SEE ALSO - - - b10-init8 - , - BIND 10 Guide. - - - - HISTORY - The bind10 script was added in February 2013. - - - - diff --git a/src/bin/bind10/creatorapi.txt b/src/bin/bind10/creatorapi.txt deleted file mode 100644 index d3e67f46ec..0000000000 --- a/src/bin/bind10/creatorapi.txt +++ /dev/null @@ -1,123 +0,0 @@ -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. diff --git a/src/bin/bind10/init.py.in b/src/bin/bind10/init.py.in deleted file mode 100755 index 67491b3dee..0000000000 --- a/src/bin/bind10/init.py.in +++ /dev/null @@ -1,1389 +0,0 @@ -#!@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 : 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:", ""}. - 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: - - [-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) diff --git a/src/bin/bind10/init.spec b/src/bin/bind10/init.spec deleted file mode 100644 index 62c6f09f69..0000000000 --- a/src/bin/bind10/init.spec +++ /dev/null @@ -1,92 +0,0 @@ -{ - "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" - } - ] - } -} - - diff --git a/src/bin/bind10/init_messages.mes b/src/bin/bind10/init_messages.mes deleted file mode 100644 index a3e34135e3..0000000000 --- a/src/bin/bind10/init_messages.mes +++ /dev/null @@ -1,334 +0,0 @@ -# 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
:). - -% 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. diff --git a/src/bin/bind10/run_bind10.sh.in b/src/bin/bind10/run_bind10.sh.in deleted file mode 100755 index 09c97084b6..0000000000 --- a/src/bin/bind10/run_bind10.sh.in +++ /dev/null @@ -1,49 +0,0 @@ -#! /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 "$@" - diff --git a/src/bin/bind10/tests/.gitignore b/src/bin/bind10/tests/.gitignore deleted file mode 100644 index 575a4c527c..0000000000 --- a/src/bin/bind10/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/init_test.py diff --git a/src/bin/bind10/tests/Makefile.am b/src/bin/bind10/tests/Makefile.am deleted file mode 100644 index d7e51d5a93..0000000000 --- a/src/bin/bind10/tests/Makefile.am +++ /dev/null @@ -1,29 +0,0 @@ -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 diff --git a/src/bin/bind10/tests/args_test.py b/src/bin/bind10/tests/args_test.py deleted file mode 100644 index c08343b22d..0000000000 --- a/src/bin/bind10/tests/args_test.py +++ /dev/null @@ -1,172 +0,0 @@ -# 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() diff --git a/src/bin/bind10/tests/init_test.py.in b/src/bin/bind10/tests/init_test.py.in deleted file mode 100644 index af41eb64a3..0000000000 --- a/src/bin/bind10/tests/init_test.py.in +++ /dev/null @@ -1,2489 +0,0 @@ -# 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() diff --git a/src/bin/bindctl/.gitignore b/src/bin/bindctl/.gitignore deleted file mode 100644 index 2d4ffbaf28..0000000000 --- a/src/bin/bindctl/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/bindctl -/bindctl_main.py -/run_bindctl.sh -/bindctl.1 diff --git a/src/bin/bindctl/Makefile.am b/src/bin/bindctl/Makefile.am deleted file mode 100644 index 1134240bf4..0000000000 --- a/src/bin/bindctl/Makefile.am +++ /dev/null @@ -1,41 +0,0 @@ -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) diff --git a/src/bin/bindctl/README b/src/bin/bindctl/README deleted file mode 100644 index a9a185f6be..0000000000 --- a/src/bin/bindctl/README +++ /dev/null @@ -1,2 +0,0 @@ -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". diff --git a/src/bin/bindctl/TODO b/src/bin/bindctl/TODO deleted file mode 100644 index 079d309299..0000000000 --- a/src/bin/bindctl/TODO +++ /dev/null @@ -1,21 +0,0 @@ -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. diff --git a/src/bin/bindctl/__init__.py b/src/bin/bindctl/__init__.py deleted file mode 100644 index 6a7afe14d3..0000000000 --- a/src/bin/bindctl/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__all__ = ['mycollections', 'exception', 'moduleinfo', 'command', 'bindctl'] diff --git a/src/bin/bindctl/bindcmd.py b/src/bin/bindctl/bindcmd.py deleted file mode 100644 index a83775bb19..0000000000 --- a/src/bin/bindctl/bindcmd.py +++ /dev/null @@ -1,945 +0,0 @@ -# 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: [param1 = value1 [, param2 = value2]] -Type Tab character to get the hint of module/command/parameters. -Type \"help(? h)\" for help on bindctl. -Type \" help\" for help on the specific module. -Type \" 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 '.) 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)) diff --git a/src/bin/bindctl/bindctl.xml b/src/bin/bindctl/bindctl.xml deleted file mode 100644 index 426c478243..0000000000 --- a/src/bin/bindctl/bindctl.xml +++ /dev/null @@ -1,348 +0,0 @@ -]> - - - - - - June 20, 2012 - - - - bindctl - 1 - BIND10 - - - - bindctl - control and configure BIND 10 - - - - - 2010 - Internet Systems Consortium, Inc. ("ISC") - - - - - - bindctl - - - - - - - - - - - - - - - DESCRIPTION - - The bindctl 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 - via its - interactive command interpreter. - - - - bindctl communicates over a HTTPS REST-ful - interface provided by - b10-cmdctl8. - The -b10-cfgmgr8 - daemon stores the configurations. - - - - - - ARGUMENTS - - The arguments are as follows: - - - - - address, address - - - The IPv4 or IPv6 address to use to connect to the running - b10-cmdctl8 - daemon. - The default is 127.0.0.1. - - - - - - file, - file - - - The PEM formatted server certificate validation chain file. - - - - - - - - - file - - - - - 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 ".bind10" directory - under the user's home directory will be used. - - - - - - , - - - Display command usage. - - - - - number, number - - - The port number to use to connect to the running - b10-cmdctl8 - daemon. - The default is 8080. - - This default port number may change. - - - - - - - - Display the version number and exit. - - - - - - - - - AUTHENTICATION - - - 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 bindctl. - The file name is "default_user.csv" - located under the directory specified by the - option. - - - - - - - - USAGE - - - The bindctl prompt shows - > . - The prompt will also display the location if changed. - The options are based on the module in use. - The usage is: - module command param1 = value1 , param2 = value2 - - - - bindctl's - interactive interface provides command-line completion and - hints. Press the Tab key to get a hint for the module, - command, and/or parameters. - - - The arrow keys and Emacs-style editing keys may be used to edit - and recall previous lines. - - - - - You can use the help - keyword to receive usage assistance for a module or a module's - command. - - - - The quit - command is used to exit - bindctl. - (It doesn't stop the BIND 10 services.) - - - - The following module is available by default: - config for Configuration commands. - - Additional modules may be available, such as - Init, Xfrin, and - Auth. - - - - - - - - - - SEE ALSO - - - b10-cfgmgr8 - , - - b10-cmdctl8 - , - - b10-init8 - , - - bind108 - , - BIND 10 Guide. - - - - - - AUTHORS - - The bindctl - 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. - - - - - HISTORY - - The initial version (with internal name of - BigTool) - was started in October 2009. - - - - - diff --git a/src/bin/bindctl/bindctl_main.py.in b/src/bin/bindctl/bindctl_main.py.in deleted file mode 100755 index 68a6237338..0000000000 --- a/src/bin/bindctl/bindctl_main.py.in +++ /dev/null @@ -1,167 +0,0 @@ -#!@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) diff --git a/src/bin/bindctl/cmdparse.py b/src/bin/bindctl/cmdparse.py deleted file mode 100644 index 30909a359b..0000000000 --- a/src/bin/bindctl/cmdparse.py +++ /dev/null @@ -1,175 +0,0 @@ -# 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[\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_with_quota_str = "[\"\'](?P.+?)(?\s*)(?P,?)(?P.*)$" - - -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[\w]+)(?P\s*)(?P.*)$") - -# 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) diff --git a/src/bin/bindctl/command_sets.py b/src/bin/bindctl/command_sets.py deleted file mode 100644 index b146c38ae7..0000000000 --- a/src/bin/bindctl/command_sets.py +++ /dev/null @@ -1,94 +0,0 @@ -# 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) diff --git a/src/bin/bindctl/exception.py b/src/bin/bindctl/exception.py deleted file mode 100644 index c6c48651ec..0000000000 --- a/src/bin/bindctl/exception.py +++ /dev/null @@ -1,124 +0,0 @@ -# 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" - - diff --git a/src/bin/bindctl/moduleinfo.py b/src/bin/bindctl/moduleinfo.py deleted file mode 100644 index 33114d8027..0000000000 --- a/src/bin/bindctl/moduleinfo.py +++ /dev/null @@ -1,249 +0,0 @@ -# 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 \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() - diff --git a/src/bin/bindctl/mycollections.py b/src/bin/bindctl/mycollections.py deleted file mode 100644 index 42279bda04..0000000000 --- a/src/bin/bindctl/mycollections.py +++ /dev/null @@ -1,82 +0,0 @@ -# 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 diff --git a/src/bin/bindctl/run_bindctl.sh.in b/src/bin/bindctl/run_bindctl.sh.in deleted file mode 100755 index 0bd881816b..0000000000 --- a/src/bin/bindctl/run_bindctl.sh.in +++ /dev/null @@ -1,41 +0,0 @@ -#! /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 "$@" diff --git a/src/bin/bindctl/tests/.gitignore b/src/bin/bindctl/tests/.gitignore deleted file mode 100644 index 284bacc90c..0000000000 --- a/src/bin/bindctl/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/bindctl_test diff --git a/src/bin/bindctl/tests/Makefile.am b/src/bin/bindctl/tests/Makefile.am deleted file mode 100644 index c781d7caa9..0000000000 --- a/src/bin/bindctl/tests/Makefile.am +++ /dev/null @@ -1,24 +0,0 @@ -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 diff --git a/src/bin/bindctl/tests/bindctl_test.in b/src/bin/bindctl/tests/bindctl_test.in deleted file mode 100644 index 66706ed7ba..0000000000 --- a/src/bin/bindctl/tests/bindctl_test.in +++ /dev/null @@ -1,27 +0,0 @@ -#! /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 $* - diff --git a/src/bin/bindctl/tests/bindctl_test.py b/src/bin/bindctl/tests/bindctl_test.py deleted file mode 100644 index c3262e1a5d..0000000000 --- a/src/bin/bindctl/tests/bindctl_test.py +++ /dev/null @@ -1,694 +0,0 @@ -# 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() - diff --git a/src/bin/bindctl/tests/cmdparse_test.py b/src/bin/bindctl/tests/cmdparse_test.py deleted file mode 100644 index 9150ed3250..0000000000 --- a/src/bin/bindctl/tests/cmdparse_test.py +++ /dev/null @@ -1,88 +0,0 @@ -# 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() - diff --git a/src/bin/cfgmgr/.gitignore b/src/bin/cfgmgr/.gitignore deleted file mode 100644 index 1ef37efd9f..0000000000 --- a/src/bin/cfgmgr/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/b10-cfgmgr -/b10-cfgmgr.py -/b10-cfgmgr.8 diff --git a/src/bin/cfgmgr/Makefile.am b/src/bin/cfgmgr/Makefile.am deleted file mode 100644 index 9c73f79ee6..0000000000 --- a/src/bin/cfgmgr/Makefile.am +++ /dev/null @@ -1,43 +0,0 @@ -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) diff --git a/src/bin/cfgmgr/b10-cfgmgr.py.in b/src/bin/cfgmgr/b10-cfgmgr.py.in deleted file mode 100755 index 4bc56df54d..0000000000 --- a/src/bin/cfgmgr/b10-cfgmgr.py.in +++ /dev/null @@ -1,132 +0,0 @@ -#!@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)) diff --git a/src/bin/cfgmgr/b10-cfgmgr.xml b/src/bin/cfgmgr/b10-cfgmgr.xml deleted file mode 100644 index 78dde76fdb..0000000000 --- a/src/bin/cfgmgr/b10-cfgmgr.xml +++ /dev/null @@ -1,174 +0,0 @@ -]> - - - - - - June 20, 2012 - - - - b10-cfgmgr - 8 - BIND10 - - - - b10-cfgmgr - Configuration manager - - - - - 2010 - Internet Systems Consortium, Inc. ("ISC") - - - - - - b10-cfgmgr - - - - - - - - - - DESCRIPTION - - The b10-cfgmgr daemon handles all BIND 10 - system configuration. It provides persistent storage for - configuration, and notifies running BIND 10 modules of - configuration changes. - - - - The bindctl can be used to talk to this - configuration manager via a b10-cmdctl connection. - - - - - This daemon communicates over a b10-msgq C-Channel - connection. If this connection is not established, - b10-cfgmgr will exit. - - - - - The daemon may be cleanly stopped by sending the SIGTERM - signal to the process. This shutdown does not notify the - subscribers. - - - - - - - ARGUMENTS - - The arguments are as follows: - - - - - - - - - This will create a backup of the existing configuration - file, remove it, and - b10-cfgmgr8 - will use the default configurations. - The name of the backup file can be found in the logs - (CFGMGR_BACKED_UP_CONFIG_FILE). - (It will append a number to the backup filename if a - previous backup file exists.) - - - - - - - config-filename, - config-filename - - - The configuration database filename to use. Can be either - absolute or relative to data path. - It defaults to "b10-config.db". - - - - - - data-path, - data-path - - - 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. - - - - - - - FILES - - /usr/local/var/bind10/b10-config.db - — Configuration storage file. - - - - - SEE ALSO - - - bind108 - , - - b10-cmdctl8 - . - - b10-init8 - , - - b10-msgq8 - . - - - - - HISTORY - - The b10-cfgmgr daemon and configuration - specification were initially designed by Jelte Jansen of ISC. - Its development began in October 2009. - - - diff --git a/src/bin/cfgmgr/local_plugins/.gitignore b/src/bin/cfgmgr/local_plugins/.gitignore deleted file mode 100644 index 724690c573..0000000000 --- a/src/bin/cfgmgr/local_plugins/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/datasrc.spec diff --git a/src/bin/cfgmgr/local_plugins/Makefile.am b/src/bin/cfgmgr/local_plugins/Makefile.am deleted file mode 100644 index 2f4dd39767..0000000000 --- a/src/bin/cfgmgr/local_plugins/Makefile.am +++ /dev/null @@ -1,11 +0,0 @@ -# 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 diff --git a/src/bin/cfgmgr/plugins/.gitignore b/src/bin/cfgmgr/plugins/.gitignore deleted file mode 100644 index 9641e02dc7..0000000000 --- a/src/bin/cfgmgr/plugins/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/datasrc.spec -/datasrc.spec.pre diff --git a/src/bin/cfgmgr/plugins/Makefile.am b/src/bin/cfgmgr/plugins/Makefile.am deleted file mode 100644 index 2fa1e126be..0000000000 --- a/src/bin/cfgmgr/plugins/Makefile.am +++ /dev/null @@ -1,18 +0,0 @@ -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) diff --git a/src/bin/cfgmgr/plugins/README b/src/bin/cfgmgr/plugins/README deleted file mode 100644 index 27fb0c0025..0000000000 --- a/src/bin/cfgmgr/plugins/README +++ /dev/null @@ -1,34 +0,0 @@ -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) diff --git a/src/bin/cfgmgr/plugins/b10logging.py b/src/bin/cfgmgr/plugins/b10logging.py deleted file mode 100644 index e288c6d3c0..0000000000 --- a/src/bin/cfgmgr/plugins/b10logging.py +++ /dev/null @@ -1,109 +0,0 @@ -# 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) - diff --git a/src/bin/cfgmgr/plugins/datasrc.spec.pre.in b/src/bin/cfgmgr/plugins/datasrc.spec.pre.in deleted file mode 100644 index 3f167586ce..0000000000 --- a/src/bin/cfgmgr/plugins/datasrc.spec.pre.in +++ /dev/null @@ -1,85 +0,0 @@ -{ - "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": [] - } -} diff --git a/src/bin/cfgmgr/plugins/datasrc_config_plugin.py b/src/bin/cfgmgr/plugins/datasrc_config_plugin.py deleted file mode 100644 index 12710d5e1c..0000000000 --- a/src/bin/cfgmgr/plugins/datasrc_config_plugin.py +++ /dev/null @@ -1,80 +0,0 @@ -# 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) diff --git a/src/bin/cfgmgr/plugins/logging.spec b/src/bin/cfgmgr/plugins/logging.spec deleted file mode 100644 index da20ce5091..0000000000 --- a/src/bin/cfgmgr/plugins/logging.spec +++ /dev/null @@ -1,81 +0,0 @@ -{ - "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": [] - } -} diff --git a/src/bin/cfgmgr/plugins/tests/Makefile.am b/src/bin/cfgmgr/plugins/tests/Makefile.am deleted file mode 100644 index a596bc0076..0000000000 --- a/src/bin/cfgmgr/plugins/tests/Makefile.am +++ /dev/null @@ -1,30 +0,0 @@ -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 - diff --git a/src/bin/cfgmgr/plugins/tests/datasrc_test.py b/src/bin/cfgmgr/plugins/tests/datasrc_test.py deleted file mode 100644 index 546e5340f9..0000000000 --- a/src/bin/cfgmgr/plugins/tests/datasrc_test.py +++ /dev/null @@ -1,245 +0,0 @@ -# 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() diff --git a/src/bin/cfgmgr/plugins/tests/logging_test.py b/src/bin/cfgmgr/plugins/tests/logging_test.py deleted file mode 100644 index 818a5964a7..0000000000 --- a/src/bin/cfgmgr/plugins/tests/logging_test.py +++ /dev/null @@ -1,135 +0,0 @@ -# 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() diff --git a/src/bin/cfgmgr/plugins/tests/tsig_keys_test.py b/src/bin/cfgmgr/plugins/tests/tsig_keys_test.py deleted file mode 100644 index 575879d547..0000000000 --- a/src/bin/cfgmgr/plugins/tests/tsig_keys_test.py +++ /dev/null @@ -1,103 +0,0 @@ -# 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() diff --git a/src/bin/cfgmgr/plugins/tsig_keys.py b/src/bin/cfgmgr/plugins/tsig_keys.py deleted file mode 100644 index d57e645930..0000000000 --- a/src/bin/cfgmgr/plugins/tsig_keys.py +++ /dev/null @@ -1,50 +0,0 @@ -# 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) diff --git a/src/bin/cfgmgr/plugins/tsig_keys.spec b/src/bin/cfgmgr/plugins/tsig_keys.spec deleted file mode 100644 index e558dd2b84..0000000000 --- a/src/bin/cfgmgr/plugins/tsig_keys.spec +++ /dev/null @@ -1,21 +0,0 @@ -{ - "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": [] - } -} diff --git a/src/bin/cfgmgr/tests/.gitignore b/src/bin/cfgmgr/tests/.gitignore deleted file mode 100644 index dcdab2d863..0000000000 --- a/src/bin/cfgmgr/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/b10-cfgmgr_test.py diff --git a/src/bin/cfgmgr/tests/Makefile.am b/src/bin/cfgmgr/tests/Makefile.am deleted file mode 100644 index 629357144a..0000000000 --- a/src/bin/cfgmgr/tests/Makefile.am +++ /dev/null @@ -1,33 +0,0 @@ -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) diff --git a/src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in b/src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in deleted file mode 100644 index c7c1081e02..0000000000 --- a/src/bin/cfgmgr/tests/b10-cfgmgr_test.py.in +++ /dev/null @@ -1,215 +0,0 @@ -# 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() - diff --git a/src/bin/cfgmgr/tests/testdata/plugins/testplugin.py b/src/bin/cfgmgr/tests/testdata/plugins/testplugin.py deleted file mode 100644 index a50eefe164..0000000000 --- a/src/bin/cfgmgr/tests/testdata/plugins/testplugin.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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) diff --git a/src/bin/cmdctl/.gitignore b/src/bin/cmdctl/.gitignore deleted file mode 100644 index e702e49ab8..0000000000 --- a/src/bin/cmdctl/.gitignore +++ /dev/null @@ -1,10 +0,0 @@ -/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 diff --git a/src/bin/cmdctl/Makefile.am b/src/bin/cmdctl/Makefile.am deleted file mode 100644 index a2d04a3622..0000000000 --- a/src/bin/cmdctl/Makefile.am +++ /dev/null @@ -1,90 +0,0 @@ -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) diff --git a/src/bin/cmdctl/README b/src/bin/cmdctl/README deleted file mode 100644 index cba200f9db..0000000000 --- a/src/bin/cmdctl/README +++ /dev/null @@ -1,22 +0,0 @@ -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 - - - - diff --git a/src/bin/cmdctl/TODO b/src/bin/cmdctl/TODO deleted file mode 100644 index 09827f5dac..0000000000 --- a/src/bin/cmdctl/TODO +++ /dev/null @@ -1,7 +0,0 @@ -. 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 diff --git a/src/bin/cmdctl/b10-certgen.cc b/src/bin/cmdctl/b10-certgen.cc deleted file mode 100644 index 59bba9ebb9..0000000000 --- a/src/bin/cmdctl/b10-certgen.cc +++ /dev/null @@ -1,429 +0,0 @@ -// 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 -#include -#include -#include -#include -#include -using namespace Botan; - -#include -#include -#include -#include - -// For cleaner 'does not exist or is not readable' output than -// botan provides -#include -#include - -// 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("certfile"), required_argument, NULL, 'c' }, - { const_cast("force"), no_argument, NULL, 'f' }, - { const_cast("help"), no_argument, NULL, 'h' }, - { const_cast("keyfile"), required_argument, NULL, 'k' }, - { const_cast("write"), no_argument, NULL, 'w' }, - { const_cast("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)); -} diff --git a/src/bin/cmdctl/b10-certgen.xml b/src/bin/cmdctl/b10-certgen.xml deleted file mode 100644 index d7fe100d76..0000000000 --- a/src/bin/cmdctl/b10-certgen.xml +++ /dev/null @@ -1,217 +0,0 @@ -]> - - - - - - November 15, 2012 - - - - b10-certgen - 1 - BIND10 - - - - b10-certgen - X509 Certificate generation tool for use with b10-cmdctl - - - - - 2012 - Internet Systems Consortium, Inc. ("ISC") - - - - - - b10-certgen - - - - - - - - DESCRIPTION - The b10-certgen tool validates, creates, or - updates a self-signed X509 certificate for use in b10-cmdctl. - - - - The connection between bindctl and - b10-cmdctl is done over HTTPS, and therefore - b10-cmdctl 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, - b10-certgen 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). - - - - - ARGUMENTS - - The arguments are as follows: - - - - - - , - - - - - File to read the certificate from, or write the certificate to. - If and are used, - is mandatory as well. - - - - - - , - - - - - Force updating of certificate when is used, - even if the existing certificate is still valid. - - - - - - , - - - - - Print the command line arguments and exit. - - - - - - , - - - - - File to write the private key to. This option is only valid when is used, and if this option is used, is mandatory as well. - - - - - - , - - - - - 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 - to force an update in that case). - - - - - - , - - - - - Don't print informational messages (only command-line errors are - printed). Useful in scripts when only the return code is needed. - - - - - - - - SEE ALSO - - - b10-cmdctl8 - , - - bindctl1 - , - BIND 10 Guide. - - - - - HISTORY - - The b10-certgen tool was first implemented - in November 2012 for the ISC BIND 10 project. - - - - - EXAMPLE - - To update an expired certificate in BIND 10 that has been installed to - /usr/local: - -$> 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) - - - - diff --git a/src/bin/cmdctl/b10-cmdctl.xml b/src/bin/cmdctl/b10-cmdctl.xml deleted file mode 100644 index c66837c879..0000000000 --- a/src/bin/cmdctl/b10-cmdctl.xml +++ /dev/null @@ -1,232 +0,0 @@ -]> - - - - - - February 28, 2012 - - - - b10-cmdctl - 8 - BIND10 - - - - b10-cmdctl - BIND 10 remote control daemon - - - - - - 2010-2012 - Internet Systems Consortium, Inc. ("ISC") - - - - - - b10-cmdctl - - - - - - - - - - - - - - - - - DESCRIPTION - The b10-cmdctl daemon provides an entry - for commands sent to the BIND 10 services. - For example, the bindctl user interface - communicates via b10-cmdctl. - - - - It is a lightweight HTTPS server with HTTP Digest Authentication - (username and password validation). - It offers a RESTful style interface. - - - - - - OPTIONS - - The arguments are as follows: - - - - - , - - - The IP address that b10-cmdctl will listen on. - The default is 127.0.0.1. - - - - - , - - - Display command usage. - - - - - , - - - The socket idle timeout for the HTTPS connection in seconds. - The default is 1200 seconds. - - - - - , - - - The port number b10-cmdctl will listen on. - The default is 8080. - - - - - - - - Enable verbose mode. - - - - - - - - Display the version number and exit. - - - - - - - - CONFIGURATION AND COMMANDS - - The configurable settings are: - - - - accounts_file defines the path to the - user accounts database. - The default is - /usr/local/etc/bind10/cmdctl-accounts.csv. - - - - cert_file defines the path to the - PEM certificate file. - The default is - /usr/local/etc/bind10/cmdctl-certfile.pem. - - - - key_file defines the path to the PEM private key - file. - The default is - /usr/local/etc/bind10/cmdctl-keyfile.pem. - - - - - The configuration command is: - - - - shutdown exits b10-cmdctl. - This has an optional pid argument to - select the process ID to stop. - (Note that the b10-init process may restart this service - if configured.) - - - - - - FILES - - - - - /usr/local/etc/bind10/cmdctl-accounts.csv - — account database containing the name, hashed password, - and the salt. - - - - - /usr/local/etc/bind10/cmdctl-keyfile.pem - — contains the Private key. - - /usr/local/etc/bind10/cmdctl-certfile.pem - — contains the Certificate. - - - - - SEE ALSO - - - b10-cfgmgr8 - , - - b10-init8 - , - - bind108 - , - - bindctl1 - . - - - - - AUTHORS - - The b10-cmdctl daemon was initially designed - and coded by Zhang Likun of CNNIC. - - - diff --git a/src/bin/cmdctl/cmdctl-accounts.csv b/src/bin/cmdctl/cmdctl-accounts.csv deleted file mode 100644 index dc1a052462..0000000000 --- a/src/bin/cmdctl/cmdctl-accounts.csv +++ /dev/null @@ -1 +0,0 @@ -root,6f0c73bd33101a5ec0294b3ca39fec90ef4717fe,"{5hV&$^(]!uV,3H>E~=f`I;,HgMl""`Eyao4^0l|Nlz|%R9Y0v)#/t'u@CzJ$U)" diff --git a/src/bin/cmdctl/cmdctl.py.in b/src/bin/cmdctl/cmdctl.py.in deleted file mode 100755 index 2e56629d34..0000000000 --- a/src/bin/cmdctl/cmdctl.py.in +++ /dev/null @@ -1,718 +0,0 @@ -#!@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) diff --git a/src/bin/cmdctl/cmdctl.spec.pre.in b/src/bin/cmdctl/cmdctl.spec.pre.in deleted file mode 100644 index 87aeb11602..0000000000 --- a/src/bin/cmdctl/cmdctl.spec.pre.in +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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 - } - ] - } - ] - } -} - - diff --git a/src/bin/cmdctl/cmdctl_messages.mes b/src/bin/cmdctl/cmdctl_messages.mes deleted file mode 100644 index 32afce3bfb..0000000000 --- a/src/bin/cmdctl/cmdctl_messages.mes +++ /dev/null @@ -1,94 +0,0 @@ -# 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. diff --git a/src/bin/cmdctl/run_b10-cmdctl.sh.in b/src/bin/cmdctl/run_b10-cmdctl.sh.in deleted file mode 100644 index 454e02c87d..0000000000 --- a/src/bin/cmdctl/run_b10-cmdctl.sh.in +++ /dev/null @@ -1,37 +0,0 @@ -#! /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 "$@" diff --git a/src/bin/cmdctl/tests/.gitignore b/src/bin/cmdctl/tests/.gitignore deleted file mode 100644 index ab9dfef647..0000000000 --- a/src/bin/cmdctl/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/cmdctl_test diff --git a/src/bin/cmdctl/tests/Makefile.am b/src/bin/cmdctl/tests/Makefile.am deleted file mode 100644 index 93bf040c86..0000000000 --- a/src/bin/cmdctl/tests/Makefile.am +++ /dev/null @@ -1,32 +0,0 @@ -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 diff --git a/src/bin/cmdctl/tests/b10-certgen_test.py b/src/bin/cmdctl/tests/b10-certgen_test.py deleted file mode 100644 index 8054aa1db9..0000000000 --- a/src/bin/cmdctl/tests/b10-certgen_test.py +++ /dev/null @@ -1,253 +0,0 @@ -# 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 - ), 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() - diff --git a/src/bin/cmdctl/tests/cmdctl_test.in b/src/bin/cmdctl/tests/cmdctl_test.in deleted file mode 100644 index 7cb5d7caa5..0000000000 --- a/src/bin/cmdctl/tests/cmdctl_test.in +++ /dev/null @@ -1,27 +0,0 @@ -#! /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 $* - diff --git a/src/bin/cmdctl/tests/cmdctl_test.py b/src/bin/cmdctl/tests/cmdctl_test.py deleted file mode 100644 index 7af8b0da83..0000000000 --- a/src/bin/cmdctl/tests/cmdctl_test.py +++ /dev/null @@ -1,790 +0,0 @@ -# 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() - - diff --git a/src/bin/cmdctl/tests/testdata/expired-certfile.pem b/src/bin/cmdctl/tests/testdata/expired-certfile.pem deleted file mode 100644 index 384a22223f..0000000000 --- a/src/bin/cmdctl/tests/testdata/expired-certfile.pem +++ /dev/null @@ -1,21 +0,0 @@ ------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----- diff --git a/src/bin/cmdctl/tests/testdata/mangled-certfile.pem b/src/bin/cmdctl/tests/testdata/mangled-certfile.pem deleted file mode 100644 index 7c47fda511..0000000000 --- a/src/bin/cmdctl/tests/testdata/mangled-certfile.pem +++ /dev/null @@ -1,21 +0,0 @@ ------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----- diff --git a/src/bin/cmdctl/tests/testdata/noca-certfile.pem b/src/bin/cmdctl/tests/testdata/noca-certfile.pem deleted file mode 100644 index 56368696cf..0000000000 --- a/src/bin/cmdctl/tests/testdata/noca-certfile.pem +++ /dev/null @@ -1,19 +0,0 @@ ------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----- diff --git a/src/bin/d2/tests/Makefile.am b/src/bin/d2/tests/Makefile.am index 2072be4d13..35a80b0afa 100644 --- a/src/bin/d2/tests/Makefile.am +++ b/src/bin/d2/tests/Makefile.am @@ -1,8 +1,3 @@ -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. @@ -11,16 +6,6 @@ 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: - 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 diff --git a/src/bin/dhcp4/tests/Makefile.am b/src/bin/dhcp4/tests/Makefile.am index c835aaa1bf..67b849e2d2 100644 --- a/src/bin/dhcp4/tests/Makefile.am +++ b/src/bin/dhcp4/tests/Makefile.am @@ -1,7 +1,3 @@ -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 @@ -11,16 +7,6 @@ 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: - 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 diff --git a/src/bin/dhcp4/tests/dhcp4_test.py b/src/bin/dhcp4/tests/dhcp4_test.py deleted file mode 100644 index 37e9629d6f..0000000000 --- a/src/bin/dhcp4/tests/dhcp4_test.py +++ /dev/null @@ -1,211 +0,0 @@ -# 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() diff --git a/src/bin/dhcp6/tests/Makefile.am b/src/bin/dhcp6/tests/Makefile.am index 75452db730..d82197dd45 100644 --- a/src/bin/dhcp6/tests/Makefile.am +++ b/src/bin/dhcp6/tests/Makefile.am @@ -1,5 +1,3 @@ -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. @@ -8,7 +6,7 @@ SHTESTS += dhcp6_reconfigure_test.sh 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 @@ -20,14 +18,6 @@ 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 ; \ - $(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); \ diff --git a/src/bin/msgq/.gitignore b/src/bin/msgq/.gitignore deleted file mode 100644 index ecd1bad2cb..0000000000 --- a/src/bin/msgq/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/b10-msgq -/msgq.py -/run_msgq.sh -/b10-msgq.8 diff --git a/src/bin/msgq/Makefile.am b/src/bin/msgq/Makefile.am deleted file mode 100644 index 185d47a85f..0000000000 --- a/src/bin/msgq/Makefile.am +++ /dev/null @@ -1,49 +0,0 @@ -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) diff --git a/src/bin/msgq/TODO b/src/bin/msgq/TODO deleted file mode 100644 index 0e926cb27a..0000000000 --- a/src/bin/msgq/TODO +++ /dev/null @@ -1,12 +0,0 @@ -== 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...) diff --git a/src/bin/msgq/msgq.py.in b/src/bin/msgq/msgq.py.in deleted file mode 100755 index c68567c49f..0000000000 --- a/src/bin/msgq/msgq.py.in +++ /dev/null @@ -1,886 +0,0 @@ -#!@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) diff --git a/src/bin/msgq/msgq.spec b/src/bin/msgq/msgq.spec deleted file mode 100644 index 4b388c52c3..0000000000 --- a/src/bin/msgq/msgq.spec +++ /dev/null @@ -1,20 +0,0 @@ -{ - "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" - } - ] - } - ] - } -} diff --git a/src/bin/msgq/msgq.xml b/src/bin/msgq/msgq.xml deleted file mode 100644 index 8cef10c07a..0000000000 --- a/src/bin/msgq/msgq.xml +++ /dev/null @@ -1,169 +0,0 @@ -]> - - - - - - June 25, 2012 - - - - b10-msgq - 8 - BIND10 - - - - b10-msgq - message routing daemon for the Command Channel - - - - - 2010 - Internet Systems Consortium, Inc. ("ISC") - - - - - - b10-msgq - - - - - - - - - DESCRIPTION - - The b10-msgq - daemon provides message routing for the Command Channel. - - - - 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 b10-msgq is - assigned a unique identifier -- this is the local name. - The commands it handles are: - - - - getlname - — receive local name. - - - - send - — send a message to defined subscribers. - - - - subscribe - — add a subscription. This means it is a listener - for messages for a specific group. - - - - unsubscribe - — remove a subscription. - - - - - - - The b10-msgq daemon may be cleanly stopped by - sending the SIGTERM signal to the process. - This shutdown does not notify the subscribers. - - - - - - OPTIONS - - The arguments are as follows: - - - - , - - - The UNIX domain socket file this daemon will use. - The default is - /usr/local/var/bind10/msg_socket. - - - - - - , - - Enabled verbose mode. This enables diagnostic messages to - STDERR. Displays more about what b10-msgq - is doing. - - - - - - - - - - SEE ALSO - - - b10-init8 - , - - bind108 - , - BIND 10 Guide. - - - - - - AUTHORS - - The b10-msgq daemon and Control Channel specification - were initially designed by Michael Graff of ISC. - - - - - HISTORY - - The python version was first coded in December 2009. - The C version with now deprecated wire format was coded in September - 2009. - - - - - diff --git a/src/bin/msgq/msgq_messages.mes b/src/bin/msgq/msgq_messages.mes deleted file mode 100644 index 0cc97cac96..0000000000 --- a/src/bin/msgq/msgq_messages.mes +++ /dev/null @@ -1,154 +0,0 @@ -# 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 -# /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. diff --git a/src/bin/msgq/run_msgq.sh.in b/src/bin/msgq/run_msgq.sh.in deleted file mode 100644 index 6b175d6571..0000000000 --- a/src/bin/msgq/run_msgq.sh.in +++ /dev/null @@ -1,46 +0,0 @@ -#! /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 "$@" diff --git a/src/bin/msgq/tests/.gitignore b/src/bin/msgq/tests/.gitignore deleted file mode 100644 index 70acfff4a6..0000000000 --- a/src/bin/msgq/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/msgq_test diff --git a/src/bin/msgq/tests/Makefile.am b/src/bin/msgq/tests/Makefile.am deleted file mode 100644 index 025b84db32..0000000000 --- a/src/bin/msgq/tests/Makefile.am +++ /dev/null @@ -1,28 +0,0 @@ -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 - diff --git a/src/bin/msgq/tests/msgq_run_test.py b/src/bin/msgq/tests/msgq_run_test.py deleted file mode 100644 index 5b0c7112fa..0000000000 --- a/src/bin/msgq/tests/msgq_run_test.py +++ /dev/null @@ -1,350 +0,0 @@ -# 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() diff --git a/src/bin/msgq/tests/msgq_test.py b/src/bin/msgq/tests/msgq_test.py deleted file mode 100644 index 210246d468..0000000000 --- a/src/bin/msgq/tests/msgq_test.py +++ /dev/null @@ -1,1121 +0,0 @@ -# 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() diff --git a/src/bin/stats/.gitignore b/src/bin/stats/.gitignore deleted file mode 100644 index 8dacd7e47a..0000000000 --- a/src/bin/stats/.gitignore +++ /dev/null @@ -1,6 +0,0 @@ -/b10-stats -/b10-stats-httpd -/stats.py -/stats_httpd.py -/b10-stats-httpd.8 -/b10-stats.8 diff --git a/src/bin/stats/Makefile.am b/src/bin/stats/Makefile.am deleted file mode 100644 index ef678d4181..0000000000 --- a/src/bin/stats/Makefile.am +++ /dev/null @@ -1,65 +0,0 @@ -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) diff --git a/src/bin/stats/b10-stats-httpd.xml b/src/bin/stats/b10-stats-httpd.xml deleted file mode 100644 index 4510110202..0000000000 --- a/src/bin/stats/b10-stats-httpd.xml +++ /dev/null @@ -1,223 +0,0 @@ -]> - - - - - - February 28, 2012 - - - - b10-stats-httpd - 8 - BIND10 - - - - b10-stats-httpd - BIND 10 HTTP server for HTTP/XML interface of statistics - - - - - 2011-2012 - Internet Systems Consortium, Inc. ("ISC") - - - - - b10-stats-httpd - | - - - - - DESCRIPTION - - b10-stats-httpd 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 (b10-stats). 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 - b10-stats 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 b10-stats for more details about the items of BIND - 10 statistics. The server uses CC session in communication - with b10-stats. CC session is provided - by b10-msgq which is started - by b10-init in advance. The server is implemented by - HTTP-server libraries included in Python 3. The server obtains the - configuration from the config manager (b10-cfgmgr) in - runtime. Please see below for more details about this spec file and - configuration of the server. - - - - - - - OPTIONS - The argument is as follow: - - - , - - - b10-stats-httpd switches to verbose mode and sends - verbose messages to STDOUT. - - - - - - - - FILES - - /usr/local/share/bind10/stats-httpd.spec - - — the spec file of b10-stats-httpd. This file - contains configurable settings - of b10-stats-httpd. This setting can be configured in - runtime via - bindctl1. Please - see the manual - of bindctl1 about - how to configure the settings. - - - /usr/local/share/bind10/stats-httpd-xml.tpl - - — the template file of XML document. - - - /usr/local/share/bind10/stats-httpd-xsd.tpl - - — the template file of XSD document. - - - /usr/local/share/bind10/stats-httpd-xsl.tpl - - — the template file of XSL document. - - - - - CONFIGURATION AND COMMANDS - - The configurable setting in - stats-httpd.spec is: - - - - listen_on - - - a list of pairs of address and port for - b10-stats-httpd to listen HTTP requests on. The - pair consists of the address string - and port 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 bindctl1. - - - - - - The commands in stats-httpd.spec are: - - - - status - - - shows the status of b10-stats-httpd with its - PID. - - - - - shutdown - - - exits the b10-stats-httpd process. - This has an optional pid argument to - select the process ID to stop. - (Note that the b10-init process may restart this service - if configured.) - - - - - - - - SEE ALSO - - - b10-cfgmgr8 - , - - b10-init8 - , - - b10-msgq8 - , - - b10-stats8 - , - - bind108 - , - - bindctl1 - , - BIND 10 Guide. - - - - - HISTORY - - b10-stats-httpd was designed and implemented by Naoki - Kambe of JPRS in March 2011. - - - diff --git a/src/bin/stats/b10-stats.xml b/src/bin/stats/b10-stats.xml deleted file mode 100644 index 83fdd36c22..0000000000 --- a/src/bin/stats/b10-stats.xml +++ /dev/null @@ -1,252 +0,0 @@ -]> - - - - - - June 20, 2012 - - - - b10-stats - 8 - BIND10 - - - - b10-stats - BIND 10 statistics module - - - - - 2010-2012 - Internet Systems Consortium, Inc. ("ISC") - - - - - - b10-stats - - - - - - - DESCRIPTION - - The b10-stats daemon collects statistics data - from each BIND 10 module. Its statistics information may be - reported via bindctl or - b10-stats-httpd. It is started by - b10-init and communicates by using the - Command Channel by b10-msgq with other - modules like b10-init, b10-auth - and so on. b10-stats periodically requests statistics - data from each module. The interval time can be configured - via bindctl. b10-stats cannot - accept any command from other modules for updating statistics data. The - stats module collects data and - aggregates it. b10-stats invokes an internal - command for b10-init after its initial - starting to make sure it collects statistics data from - b10-init. - - - - - OPTIONS - The arguments are as follows: - - - , - - - This enables maximum debug logging. - - - - - - - - CONFIGURATION AND COMMANDS - - - The only configurable setting in stats.spec - is: - - - - - poll-interval - - - is a time interval in seconds at which b10-stats - 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. - - - - - - - - The configuration commands are: - - - - show 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. - - - - showschema will send the schema of the statistics data - in JSON format. The output is equivalent to the statistics part - of stats.spec. - - - - shutdown will shutdown the - b10-stats process. - This has an optional pid argument to - select the process ID to stop. - (Note that the b10-init process may restart this service - if configured.) - - - - status simply indicates that the daemon is - running. - - - - - - STATISTICS DATA - - - The b10-stats daemon contains these - Stats statistics: - - - - - - - boot_time - 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 - b10-stats. - - - - - last_update_time - The date and time (in ISO 8601 format) - when this daemon last received data from another component. - - - - - lname - This is the name used for the - b10-msgq command-control channel. - (This is a constant which can't be reset except by restarting - b10-stats.) - - - - - report_time - The latest report date and time in - ISO 8601 format. - - - - start_time - This is the date and time (in ISO 8601 format) - when this daemon started collecting data. - - - - - timestamp - 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. - - - - - - See other manual pages for explanations for their statistics - that are kept track by b10-stats. - - - - - - FILES - /usr/local/share/bind10/stats.spec - - — This is a spec file for b10-stats. It - contains commands for b10-stats. They can be - invoked - via bindctl1. - - - - - SEE ALSO - - - b10-init8 - , - - b10-stats-httpd8 - , - - bind108 - , - - bindctl1 - , - BIND 10 Guide. - - - - - HISTORY - - The b10-stats daemon was initially designed - and implemented by Naoki Kambe of JPRS in October 2010. - - - diff --git a/src/bin/stats/stats-httpd-xml.tpl b/src/bin/stats/stats-httpd-xml.tpl deleted file mode 100644 index ed914230e4..0000000000 --- a/src/bin/stats/stats-httpd-xml.tpl +++ /dev/null @@ -1,3 +0,0 @@ - - -$xml_string \ No newline at end of file diff --git a/src/bin/stats/stats-httpd-xsd.tpl b/src/bin/stats/stats-httpd-xsd.tpl deleted file mode 100644 index 75f926ba6a..0000000000 --- a/src/bin/stats/stats-httpd-xsd.tpl +++ /dev/null @@ -1,97 +0,0 @@ - - - - XML schema of statistics data in BIND 10 - - - A set of statistics data - - - - - - - Identifier - Identifier of item - - - - - Value - Value of item - - - - - Owner - Owner module name - - - - - URI - URI of item - - - - - Name - Name of item - - - - - Type - Type of item - - - - - - - - - - - - - - - - - Description - Description of item - - - - - Title - Title of item - - - - - Optional - The item is optional or not - - - - - Default - Default of item - - - - - Format - Format of item value - - - - - - - - diff --git a/src/bin/stats/stats-httpd-xsl.tpl b/src/bin/stats/stats-httpd-xsl.tpl deleted file mode 100644 index 3dd3b90113..0000000000 --- a/src/bin/stats/stats-httpd-xsl.tpl +++ /dev/null @@ -1,56 +0,0 @@ - - - - - - - BIND 10 Statistics - - - -

BIND 10 Statistics

- - - -
- - - - - - - - - - - - -
IdentifierValueDescription
- - - - - - - - -
-
-
diff --git a/src/bin/stats/stats-httpd.spec b/src/bin/stats/stats-httpd.spec deleted file mode 100644 index 783611d33e..0000000000 --- a/src/bin/stats/stats-httpd.spec +++ /dev/null @@ -1,60 +0,0 @@ -{ - "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 - } - ] - } - ] - } -} diff --git a/src/bin/stats/stats.py.in b/src/bin/stats/stats.py.in deleted file mode 100755 index 38d0b029b6..0000000000 --- a/src/bin/stats/stats.py.in +++ /dev/null @@ -1,752 +0,0 @@ -#!@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) diff --git a/src/bin/stats/stats.spec b/src/bin/stats/stats.spec deleted file mode 100644 index dab10a24e1..0000000000 --- a/src/bin/stats/stats.spec +++ /dev/null @@ -1,116 +0,0 @@ -{ - "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" - } - ] - } -} diff --git a/src/bin/stats/stats_httpd.py.in b/src/bin/stats/stats_httpd.py.in deleted file mode 100755 index df0c04c993..0000000000 --- a/src/bin/stats/stats_httpd.py.in +++ /dev/null @@ -1,628 +0,0 @@ -#!@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) diff --git a/src/bin/stats/stats_httpd_messages.mes b/src/bin/stats/stats_httpd_messages.mes deleted file mode 100644 index 930a745111..0000000000 --- a/src/bin/stats/stats_httpd_messages.mes +++ /dev/null @@ -1,107 +0,0 @@ -# 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. diff --git a/src/bin/stats/stats_messages.mes b/src/bin/stats/stats_messages.mes deleted file mode 100644 index 38dddcc674..0000000000 --- a/src/bin/stats/stats_messages.mes +++ /dev/null @@ -1,94 +0,0 @@ -# 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. diff --git a/src/bin/stats/tests/Makefile.am b/src/bin/stats/tests/Makefile.am deleted file mode 100644 index 06d11a1d88..0000000000 --- a/src/bin/stats/tests/Makefile.am +++ /dev/null @@ -1,36 +0,0 @@ -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) diff --git a/src/bin/stats/tests/stats-httpd_test.py b/src/bin/stats/tests/stats-httpd_test.py deleted file mode 100644 index a37302d8bc..0000000000 --- a/src/bin/stats/tests/stats-httpd_test.py +++ /dev/null @@ -1,1128 +0,0 @@ -# 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( - '')) - # 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="", - 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() diff --git a/src/bin/stats/tests/stats_test.py b/src/bin/stats/tests/stats_test.py deleted file mode 100644 index 14e49f95a1..0000000000 --- a/src/bin/stats/tests/stats_test.py +++ /dev/null @@ -1,1668 +0,0 @@ -# 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() diff --git a/src/bin/stats/tests/test_utils.py b/src/bin/stats/tests/test_utils.py deleted file mode 100644 index 99cc980686..0000000000 --- a/src/bin/stats/tests/test_utils.py +++ /dev/null @@ -1,581 +0,0 @@ -# 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) diff --git a/src/bin/stats/tests/testdata/Makefile.am b/src/bin/stats/tests/testdata/Makefile.am deleted file mode 100644 index 8f9da09817..0000000000 --- a/src/bin/stats/tests/testdata/Makefile.am +++ /dev/null @@ -1 +0,0 @@ -EXTRA_DIST = b10-config.db diff --git a/src/bin/stats/tests/testdata/b10-config.db b/src/bin/stats/tests/testdata/b10-config.db deleted file mode 100644 index 7dd9dafd56..0000000000 --- a/src/bin/stats/tests/testdata/b10-config.db +++ /dev/null @@ -1,14 +0,0 @@ -{ "version": 2, - "Init": { - "components": { - "b10-auth": { - "kind": "needed", - "special": "auth" - }, - "b10-auth-2": { - "kind": "needed", - "special": "auth" - } - } - } -} diff --git a/src/bin/sysinfo/.gitignore b/src/bin/sysinfo/.gitignore deleted file mode 100644 index b5c759d611..0000000000 --- a/src/bin/sysinfo/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/isc-sysinfo -/isc-sysinfo.1 -/run_sysinfo.sh -/sysinfo.py diff --git a/src/bin/sysinfo/Makefile.am b/src/bin/sysinfo/Makefile.am deleted file mode 100644 index 1618535332..0000000000 --- a/src/bin/sysinfo/Makefile.am +++ /dev/null @@ -1,36 +0,0 @@ -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) diff --git a/src/bin/sysinfo/isc-sysinfo.xml b/src/bin/sysinfo/isc-sysinfo.xml deleted file mode 100644 index 919ed838e3..0000000000 --- a/src/bin/sysinfo/isc-sysinfo.xml +++ /dev/null @@ -1,106 +0,0 @@ -]> - - - - - - June 26, 2012 - - - - isc-sysinfo - 1 - BIND10 - - - - isc-sysinfo - BIND 10 system information display tool - - - - - 2012 - Internet Systems Consortium, Inc. ("ISC") - - - - - - isc-sysinfo - - - - - DESCRIPTION - - The isc-sysinfo 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. - - - - - ARGUMENTS - - - - - -h - - Displays usage instructions. - - - - - -o output-file - - If an output file is specified, the output - of isc-sysinfo is written to this file. By - default, the output is written to standard output. - - - - - - - - - SEE ALSO - - - bind108 - , - BIND 10 Guide. - - - - - HISTORY - - The isc-sysinfo daemon was initially - implemented by ISC staff in June, 2012. - - - diff --git a/src/bin/sysinfo/run_sysinfo.sh.in b/src/bin/sysinfo/run_sysinfo.sh.in deleted file mode 100755 index b5593b9396..0000000000 --- a/src/bin/sysinfo/run_sysinfo.sh.in +++ /dev/null @@ -1,27 +0,0 @@ -#! /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 "$@" diff --git a/src/bin/sysinfo/sysinfo.py.in b/src/bin/sysinfo/sysinfo.py.in deleted file mode 100755 index 477b9de466..0000000000 --- a/src/bin/sysinfo/sysinfo.py.in +++ /dev/null @@ -1,134 +0,0 @@ -#!@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 ]" % 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() diff --git a/src/bin/tests/.gitignore b/src/bin/tests/.gitignore deleted file mode 100644 index b39aa8685f..0000000000 --- a/src/bin/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/process_rename_test.py diff --git a/src/bin/tests/Makefile.am b/src/bin/tests/Makefile.am deleted file mode 100644 index 535257c315..0000000000 --- a/src/bin/tests/Makefile.am +++ /dev/null @@ -1,27 +0,0 @@ -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 diff --git a/src/bin/tests/README b/src/bin/tests/README deleted file mode 100644 index 1de18b4b7b..0000000000 --- a/src/bin/tests/README +++ /dev/null @@ -1,3 +0,0 @@ -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. diff --git a/src/bin/tests/process_rename_test.py.in b/src/bin/tests/process_rename_test.py.in deleted file mode 100644 index 7e6a9c9ab0..0000000000 --- a/src/bin/tests/process_rename_test.py.in +++ /dev/null @@ -1,77 +0,0 @@ -# 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() diff --git a/src/bin/usermgr/.gitignore b/src/bin/usermgr/.gitignore deleted file mode 100644 index 36e32a09be..0000000000 --- a/src/bin/usermgr/.gitignore +++ /dev/null @@ -1,4 +0,0 @@ -/b10-cmdctl-usermgr -/b10-cmdctl-usermgr.py -/run_b10-cmdctl-usermgr.sh -/b10-cmdctl-usermgr.8 diff --git a/src/bin/usermgr/Makefile.am b/src/bin/usermgr/Makefile.am deleted file mode 100644 index d356d20cb6..0000000000 --- a/src/bin/usermgr/Makefile.am +++ /dev/null @@ -1,34 +0,0 @@ -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) diff --git a/src/bin/usermgr/README b/src/bin/usermgr/README deleted file mode 100644 index e7d9835a92..0000000000 --- a/src/bin/usermgr/README +++ /dev/null @@ -1,6 +0,0 @@ -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'. - diff --git a/src/bin/usermgr/b10-cmdctl-usermgr.py.in b/src/bin/usermgr/b10-cmdctl-usermgr.py.in deleted file mode 100755 index b0fd30f8a0..0000000000 --- a/src/bin/usermgr/b10-cmdctl-usermgr.py.in +++ /dev/null @@ -1,244 +0,0 @@ -#!@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] [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()) - diff --git a/src/bin/usermgr/b10-cmdctl-usermgr.xml b/src/bin/usermgr/b10-cmdctl-usermgr.xml deleted file mode 100644 index 26940d73d0..0000000000 --- a/src/bin/usermgr/b10-cmdctl-usermgr.xml +++ /dev/null @@ -1,159 +0,0 @@ -]> - - - - - - March 17, 2010 - - - - b10-cmdctl-usermgr - 8 - BIND10 - - - - b10-cmdctl-usermgr - cmdctl user maintenance tool - - - - - 2010 - Internet Systems Consortium, Inc. ("ISC") - - - - - - b10-cmdctl-usermgr - - - - - - - command - - - - - - - - DESCRIPTION - The b10-cmdctl-usermgr tool may be used - to add and remove accounts with passwords for the - b10-cmdctl8 - daemon. - - - - By default, the accounts are saved in the - cmdctl-accounts.csv file in the system config - directory, unless the switch is used. - The entry is appended to or removed from the file. - - - - The tool can't replace existing entries, but this can easily be - accomplished by removing the entry and adding a new one. - - - - - OPTIONS - - The arguments are as follows: - - - command is either 'add' or 'delete', respectively to add or delete users. - - - - 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. - - - - - - - - - Report the usage statement and exit. - - - - - - - - Specify the accounts file to update. The default is - cmdctl-accounts.csv in the system config - directory. - - - - - - - Report the version and exit. - - - - - - - - - - - SEE ALSO - - - b10-cmdctl8 - , - BIND 10 Guide. - - - - - HISTORY - - The b10-cmdctl-usermgr tool - was implemented in January 2010 - by Zhang Likun of CNNIC for the ISC BIND 10 project. - - - diff --git a/src/bin/usermgr/run_b10-cmdctl-usermgr.sh.in b/src/bin/usermgr/run_b10-cmdctl-usermgr.sh.in deleted file mode 100755 index 8e8e743af1..0000000000 --- a/src/bin/usermgr/run_b10-cmdctl-usermgr.sh.in +++ /dev/null @@ -1,30 +0,0 @@ -#! /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 "$@" diff --git a/src/bin/usermgr/tests/Makefile.am b/src/bin/usermgr/tests/Makefile.am deleted file mode 100644 index 230f164580..0000000000 --- a/src/bin/usermgr/tests/Makefile.am +++ /dev/null @@ -1,22 +0,0 @@ -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 diff --git a/src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py b/src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py deleted file mode 100644 index 2da418d658..0000000000 --- a/src/bin/usermgr/tests/b10-cmdctl-usermgr_test.py +++ /dev/null @@ -1,511 +0,0 @@ -# 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] [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() - diff --git a/src/lib/config/tests/ccsession_unittests.cc b/src/lib/config/tests/ccsession_unittests.cc index b9c70d03b7..175c27b374 100644 --- a/src/lib/config/tests/ccsession_unittests.cc +++ b/src/lib/config/tests/ccsession_unittests.cc @@ -868,27 +868,6 @@ TEST_F(CCSessionTest, delayedStart) { 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(). diff --git a/src/lib/python/isc/bind10/tests/Makefile.am b/src/lib/python/isc/bind10/tests/Makefile.am index 12fb94878c..fbd0821048 100644 --- a/src/lib/python/isc/bind10/tests/Makefile.am +++ b/src/lib/python/isc/bind10/tests/Makefile.am @@ -1,7 +1,7 @@ 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) diff --git a/src/lib/python/isc/config/tests/Makefile.am b/src/lib/python/isc/config/tests/Makefile.am index 8c79c84e55..58037a7779 100644 --- a/src/lib/python/isc/config/tests/Makefile.am +++ b/src/lib/python/isc/config/tests/Makefile.am @@ -1,5 +1,5 @@ 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 diff --git a/src/lib/python/isc/config/tests/ccsession_test.py b/src/lib/python/isc/config/tests/ccsession_test.py deleted file mode 100644 index 9c33ef6512..0000000000 --- a/src/lib/python/isc/config/tests/ccsession_test.py +++ /dev/null @@ -1,1358 +0,0 @@ -# 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() - diff --git a/src/lib/python/isc/config/tests/cfgmgr_test.py b/src/lib/python/isc/config/tests/cfgmgr_test.py deleted file mode 100644 index ee6c98d9af..0000000000 --- a/src/lib/python/isc/config/tests/cfgmgr_test.py +++ /dev/null @@ -1,669 +0,0 @@ -# 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() diff --git a/src/lib/python/isc/log/tests/Makefile.am b/src/lib/python/isc/log/tests/Makefile.am index 14f10a2aa7..5a37d10cf7 100644 --- a/src/lib/python/isc/log/tests/Makefile.am +++ b/src/lib/python/isc/log/tests/Makefile.am @@ -1,6 +1,6 @@ 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) diff --git a/src/lib/python/isc/log/tests/log_test.py b/src/lib/python/isc/log/tests/log_test.py deleted file mode 100644 index 2059caa7a7..0000000000 --- a/src/lib/python/isc/log/tests/log_test.py +++ /dev/null @@ -1,226 +0,0 @@ -# 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()