423 lines
15 KiB
Python
423 lines
15 KiB
Python
#!/usr/bin/python
|
|
#
|
|
# test-nut.py quality assurance test script
|
|
# Copyright (C) 2008-2011 Arnaud Quette <aquette@debian.org>
|
|
# Copyright (C) 2012 Jamie Strandboge <jamie@canonical.com>
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License version 3,
|
|
# as published by the Free Software Foundation.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
'''
|
|
*** IMPORTANT ***
|
|
DO NOT RUN ON A PRODUCTION SERVER.
|
|
*** IMPORTANT ***
|
|
|
|
How to run (xenial+):
|
|
$ sudo apt-get -y install nut-server nut-client python
|
|
$ sudo ./test-nut.py -v
|
|
|
|
|
|
NOTE:
|
|
- NUT architecture (helps understanding):
|
|
http://www.networkupstools.org/docs/developer-guide.chunked/ar01s02.html#_the_layering
|
|
|
|
- These tests only validate the NUT software framework itself (communication
|
|
between the drivers, server and client layers ; events propagation and
|
|
detection). The critical part of NUT, Ie the driver layer which
|
|
communicate with actual devices, can only be tested with real hardware!
|
|
|
|
- These tests use the NUT simulation driver (dummy-ups) to emulate real
|
|
hardware behavior, and generate events (power failure, low battery, ...).
|
|
|
|
TODO:
|
|
- improve test duration, by reworking NutTestCommon._setUp() and the way
|
|
daemons are started (ie, always)
|
|
- more events testing (upsmon / upssched)
|
|
- test syslog and wall output
|
|
- test UPS redundancy
|
|
- test Powerchain (once available!)
|
|
- test AppArmor (once available!)
|
|
- add hardware testing as Private tests?
|
|
- load a .dev file, and test a full output
|
|
|
|
QA INFORMATION:
|
|
- NUT provides "make check" and "make distcheck" in its source distribution
|
|
- NUT provides Quality Assurance information, to track all efforts:
|
|
http://www.networkupstools.org/nut-qa.html
|
|
'''
|
|
|
|
# QRT-Packages: netcat-openbsd psmisc python
|
|
# QRT-Alternates: nut-server nut
|
|
# QRT-Alternates: nut-client nut
|
|
# nut-dev is needed for the dummy driver on hardy
|
|
# QRT-Alternates: nut-dev
|
|
# QRT-Privilege: root
|
|
# QRT-Depends:
|
|
|
|
|
|
import unittest, subprocess, sys, os, time
|
|
import tempfile
|
|
import testlib
|
|
|
|
use_private = True
|
|
try:
|
|
from private.qrt.nut import PrivateNutTest
|
|
except ImportError:
|
|
class PrivateNutTest(object):
|
|
'''Empty class'''
|
|
print >>sys.stdout, "Skipping private tests"
|
|
|
|
|
|
class NutTestCommon(testlib.TestlibCase):
|
|
'''Common functions'''
|
|
|
|
# FIXME: initscript will be splitted into nut-server and nut-client
|
|
# (Debian bug #634858)
|
|
initscript = "/etc/init.d/nut-server"
|
|
hosts_file = "/etc/hosts"
|
|
powerdownflag = "/etc/killpower"
|
|
shutdowncmd = "/tmp/shutdowncmd"
|
|
notifyscript = "/tmp/nutifyme"
|
|
notifylog = "/tmp/notify.log"
|
|
|
|
def _setUp(self):
|
|
'''Set up prior to each test_* function'''
|
|
'''We generate a NUT config using the dummmy-ups driver
|
|
and standard settings for local monitoring
|
|
'''
|
|
self.tmpdir = ""
|
|
self.rundir = "/run/nut"
|
|
testlib.cmd(['/bin/rm -f' + self.powerdownflag])
|
|
|
|
testlib.config_replace('/etc/nut/ups.conf', '''
|
|
[dummy-dev1]
|
|
driver = dummy-ups
|
|
port = dummy.dev
|
|
desc = "simulation device"
|
|
''')
|
|
|
|
if self.lsb_release['Release'] <= 8.04:
|
|
testlib.config_replace('/etc/nut/upsd.conf', '''
|
|
ACL dummy-net 127.0.0.1/8
|
|
ACL dummy-net2 ::1/64
|
|
ACL all 0.0.0.0/0
|
|
ACCEPT dummy-net dummy-net2
|
|
REJECT all
|
|
''')
|
|
else:
|
|
testlib.config_replace('/etc/nut/upsd.conf', '''# just to touch the file''')
|
|
|
|
extra_cfgs = ''
|
|
if self.lsb_release['Release'] <= 8.04:
|
|
extra_cfgs = ''' allowfrom = dummy-net dummy-net2
|
|
'''
|
|
testlib.config_replace('/etc/nut/upsd.users', '''
|
|
[admin]
|
|
password = dummypass
|
|
actions = SET
|
|
instcmds = ALL
|
|
%s
|
|
[monuser]
|
|
password = dummypass
|
|
upsmon master
|
|
%s ''' %(extra_cfgs, extra_cfgs))
|
|
|
|
testlib.config_replace('/etc/nut/upsmon.conf', '''
|
|
MONITOR dummy-dev1@localhost 1 monuser dummy-pass master
|
|
MINSUPPLIES 1
|
|
SHUTDOWNCMD "/usr/bin/touch ''' + self.shutdowncmd + '"\n'
|
|
'''POWERDOWNFLAG ''' + self.powerdownflag + '\n'
|
|
'''
|
|
NOTIFYCMD ''' + self.notifyscript + '\n'
|
|
'''
|
|
NOTIFYFLAG ONLINE SYSLOG+EXEC
|
|
NOTIFYFLAG ONBATT SYSLOG+EXEC
|
|
NOTIFYFLAG LOWBATT SYSLOG+EXEC
|
|
NOTIFYFLAG FSD SYSLOG+EXEC
|
|
# NOTIFYFLAG COMMOK SYSLOG+EXEC
|
|
# NOTIFYFLAG COMMBAD SYSLOG+EXEC
|
|
NOTIFYFLAG SHUTDOWN SYSLOG+EXEC
|
|
# NOTIFYFLAG REPLBATT SYSLOG+EXEC
|
|
# NOTIFYFLAG NOCOMM SYSLOG+EXEC
|
|
# NOTIFYFLAG NOPARENT SYSLOG+EXEC
|
|
|
|
# Shorten test duration by:
|
|
# Speeding up polling frequency
|
|
POLLFREQ 2
|
|
# And final wait delay
|
|
FINALDELAY 0
|
|
'''
|
|
)
|
|
|
|
testlib.create_fill(self.notifyscript, '''
|
|
#! /bin/bash
|
|
echo "$*" > ''' + self.notifylog + '\n', mode=0755)
|
|
|
|
# dummy-ups absolutely needs a data file, even if empty
|
|
testlib.config_replace('/etc/nut/dummy.dev', '''
|
|
ups.mfr: Dummy Manufacturer
|
|
ups.model: Dummy UPS
|
|
ups.status: OL
|
|
# Set a big enough timer to avoid value reset, due to reading loop
|
|
TIMER 600
|
|
''')
|
|
|
|
testlib.config_replace('/etc/nut/nut.conf', '''MODE=standalone''')
|
|
|
|
# Add known friendly IP names for localhost v4 and v6
|
|
# FIXME: find a way to determine if v4 / v6 are enabled, and a way to
|
|
# get v4 / v6 names
|
|
testlib.config_replace(self.hosts_file, '''#
|
|
127.0.0.1 localv4
|
|
::1 localv6
|
|
''', append=True)
|
|
|
|
if self.lsb_release['Release'] <= 8.04:
|
|
testlib.config_replace('/etc/default/nut', '''#
|
|
START_UPSD=yes
|
|
UPSD_OPTIONS=""
|
|
START_UPSMON=yes
|
|
UPSMON_OPTIONS=""
|
|
''', append=False)
|
|
|
|
# Start the framework
|
|
self._restart()
|
|
|
|
def _tearDown(self):
|
|
'''Clean up after each test_* function'''
|
|
self._stop()
|
|
time.sleep(2)
|
|
os.unlink('/etc/nut/ups.conf')
|
|
os.unlink('/etc/nut/upsd.conf')
|
|
os.unlink('/etc/nut/upsd.users')
|
|
os.unlink('/etc/nut/upsmon.conf')
|
|
os.unlink('/etc/nut/dummy.dev')
|
|
os.unlink('/etc/nut/nut.conf')
|
|
testlib.config_restore('/etc/nut/ups.conf')
|
|
testlib.config_restore('/etc/nut/upsd.conf')
|
|
testlib.config_restore('/etc/nut/upsd.users')
|
|
testlib.config_restore('/etc/nut/upsmon.conf')
|
|
testlib.config_restore('/etc/nut/dummy.dev')
|
|
testlib.config_restore('/etc/nut/nut.conf')
|
|
if os.path.exists(self.notifyscript):
|
|
os.unlink(self.notifyscript)
|
|
if os.path.exists(self.shutdowncmd):
|
|
os.unlink(self.shutdowncmd)
|
|
testlib.config_restore(self.hosts_file)
|
|
if self.lsb_release['Release'] <= 8.04:
|
|
testlib.config_restore('/etc/default/nut')
|
|
|
|
if os.path.exists(self.tmpdir):
|
|
testlib.recursive_rm(self.tmpdir)
|
|
|
|
# this is needed because of the potentially hung upsd process in the
|
|
# CVE-2012-2944 test
|
|
testlib.cmd(['killall', 'upsd'])
|
|
testlib.cmd(['killall', '-9', 'upsd'])
|
|
|
|
def _start(self):
|
|
'''Start NUT'''
|
|
rc, report = testlib.cmd([self.initscript, 'start'])
|
|
expected = 0
|
|
result = 'Got exit code %d, expected %d\n' % (rc, expected)
|
|
self.assertEquals(expected, rc, result + report)
|
|
time.sleep(2)
|
|
|
|
def _stop(self):
|
|
'''Stop NUT'''
|
|
rc, report = testlib.cmd([self.initscript, 'stop'])
|
|
expected = 0
|
|
result = 'Got exit code %d, expected %d\n' % (rc, expected)
|
|
self.assertEquals(expected, rc, result + report)
|
|
|
|
def _reload(self):
|
|
'''Reload NUT'''
|
|
rc, report = testlib.cmd([self.initscript, 'force-reload'])
|
|
expected = 0
|
|
result = 'Got exit code %d, expected %d\n' % (rc, expected)
|
|
self.assertEquals(expected, rc, result + report)
|
|
|
|
def _restart(self):
|
|
'''Restart NUT'''
|
|
self._stop()
|
|
time.sleep(2)
|
|
self._start()
|
|
|
|
def _status(self):
|
|
'''NUT Status'''
|
|
rc, report = testlib.cmd([self.initscript, 'status'])
|
|
expected = 0
|
|
if self.lsb_release['Release'] <= 8.04:
|
|
self._skipped("init script does not support status command")
|
|
expected = 1
|
|
result = 'Got exit code %d, expected %d\n' % (rc, expected)
|
|
self.assertEquals(expected, rc, result + report)
|
|
|
|
def _testDaemons(self, daemons):
|
|
'''Daemons running'''
|
|
for d in daemons:
|
|
# A note on the driver pid file: its name is
|
|
# <ups.conf section name>-<driver name>.pid
|
|
# ex: dummy-dev1-dummy-ups.pid
|
|
if d == 'dummy-ups' :
|
|
pidfile = os.path.join(self.rundir, 'dummy-ups-dummy-dev1.pid')
|
|
else :
|
|
pidfile = os.path.join(self.rundir, d + '.pid')
|
|
warning = "Could not find pidfile '" + pidfile + "'"
|
|
self.assertTrue(os.path.exists(pidfile), warning)
|
|
self.assertTrue(testlib.check_pidfile(d, pidfile), d + ' is not running')
|
|
|
|
def _nut_setvar(self, var, value):
|
|
'''Test upsrw'''
|
|
rc, report = testlib.cmd(['/bin/upsrw', '-s', var + '=' + value,
|
|
'-u', 'admin' , '-p', 'dummypass', 'dummy-dev1@localhost'])
|
|
self.assertTrue(rc == 0, 'upsrw: ' + report)
|
|
return rc,report
|
|
|
|
|
|
class BasicTest(NutTestCommon, PrivateNutTest):
|
|
'''Test basic NUT functionalities'''
|
|
|
|
def setUp(self):
|
|
'''Setup mechanisms'''
|
|
NutTestCommon._setUp(self)
|
|
|
|
def tearDown(self):
|
|
'''Shutdown methods'''
|
|
NutTestCommon._tearDown(self)
|
|
|
|
def test_daemons_service(self):
|
|
'''Test daemons using "service status"'''
|
|
self._status()
|
|
|
|
def test_daemons_pid(self):
|
|
'''Test daemons using PID files'''
|
|
# upsmon does not work because ups-client is still missing
|
|
daemons = [ 'dummy-ups', 'upsd']
|
|
self._testDaemons(daemons)
|
|
|
|
def test_upsd_IPv4(self):
|
|
'''Test upsd IPv4 reachability'''
|
|
rc, report = testlib.cmd(['/bin/upsc', '-l', 'localv4'])
|
|
self.assertTrue('dummy-dev1' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
|
|
|
|
def test_upsd_IPv6(self):
|
|
'''Test upsd IPv6 reachability'''
|
|
rc, report = testlib.cmd(['/bin/upsc', '-l', 'localv6'])
|
|
self.assertTrue('dummy-dev1' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
|
|
|
|
def test_upsc_device_list(self):
|
|
'''Test NUT client interface (upsc): device(s) listing'''
|
|
rc, report = testlib.cmd(['/bin/upsc', '-L'])
|
|
self.assertTrue('dummy-dev1: simulation device' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
|
|
|
|
def _test_upsc_status(self):
|
|
'''Test NUT client interface (upsc): data access'''
|
|
rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1', 'ups.status'])
|
|
self.assertTrue('OL' in report, 'UPS Status: ' + report + 'should be OL')
|
|
|
|
#def test_upsc_powerchain(self):
|
|
# '''Test NUT client interface (upsc): Powerchain(s) listing'''
|
|
# rc, report = testlib.cmd(['/bin/upsc', '-p'])
|
|
# Result == Main ; dummy-dev1 ; $hostname
|
|
# self.assertTrue('dummy-dev1' in report, 'dummy-dev1 should be present in device(s) listing: ' + report)
|
|
|
|
def test_upsrw(self):
|
|
'''Test upsrw'''
|
|
# Set ups.status to OB (On Battery)...
|
|
self._nut_setvar('ups.model', 'Test')
|
|
time.sleep(2)
|
|
# and check the result on the client side
|
|
rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1@localhost', 'ups.model'])
|
|
self.assertTrue('Test' in report, 'UPS Model: ' + report + 'should be Test')
|
|
|
|
# FIXME: need a simulation counterpart, not yet implemented
|
|
#def test_upscmd(self):
|
|
# '''Test upscmd'''
|
|
|
|
def test_upsmon_notif(self):
|
|
'''Test upsmon notifications'''
|
|
# Set ups.status to OB (On Battery)...
|
|
self._nut_setvar('ups.status', 'OB')
|
|
time.sleep(1)
|
|
# and check the result on the client side
|
|
rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1@localhost', 'ups.status'])
|
|
self.assertTrue('OB' in report, 'UPS Status: ' + report + 'should be OB')
|
|
|
|
#def test_upsmon_shutdown(self):
|
|
# '''Test upsmon basic shutdown (single UPS, low battery status)'''
|
|
# self._nut_setvar('ups.status', 'OB LB')
|
|
# time.sleep(2)
|
|
# # and check the result on the client side
|
|
# rc, report = testlib.cmd(['/bin/upsc', 'dummy-dev1@localhost', 'ups.status'])
|
|
# self.assertTrue('OB LB' in report, 'UPS Status: ' + report + 'should be OB LB')
|
|
# # FIXME: improve with a 2 sec loop * 5 tries
|
|
# time.sleep(3)
|
|
# # Check for powerdownflag and shutdowncmd (needed for halt!)
|
|
# # FIXME: replace by a call to 'upsmon -K'
|
|
# self.assertTrue(os.path.exists(self.powerdownflag), 'POWERDOWNFLAG has not been set!')
|
|
# self.assertTrue(os.path.exists(self.shutdowncmd), 'SHUTDOWNCMD has not been executed!')
|
|
|
|
def test_CVE_2012_2944(self):
|
|
'''Test CVE-2012-2944'''
|
|
self.tmpdir = tempfile.mkdtemp(dir='/tmp', prefix="testlib-")
|
|
# First send bad input. We need to do this in a script because python
|
|
# functions don't like our embedded NULs
|
|
script = os.path.join(self.tmpdir, 'script.sh')
|
|
contents = '''#!/bin/sh
|
|
printf '\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\n' | nc -q 1 127.0.0.1 3493
|
|
sleep 1
|
|
dd if=/dev/urandom count=64 | nc -q 1 127.0.0.1 3493
|
|
'''
|
|
testlib.create_fill(script, contents, mode=0755)
|
|
rc, report = testlib.cmd([script])
|
|
|
|
# It should not have crashed. Let's see if it did
|
|
self._testDaemons(['upsd'])
|
|
self.assertTrue('ERR UNKNOWN-COMMAND' in report, "Could not find 'ERR UNKNOWN-COMMAND' in:\n%s" % report)
|
|
|
|
# This CVE may also result in a hung upsd. Try to kill it, if it is
|
|
# still around, it is hung
|
|
testlib.cmd(['killall', 'upsd'])
|
|
pidfile = os.path.join(self.rundir, 'upsd.pid')
|
|
timeout = 50
|
|
while timeout > 0 and os.path.exists(pidfile):
|
|
time.sleep(0.1)
|
|
timeout -= 1
|
|
self.assertFalse(os.path.exists(pidfile), "Found %s" % pidfile)
|
|
self.assertFalse(testlib.check_pidfile('upsd', pidfile), 'upsd is hung')
|
|
#subprocess.call(['bash'])
|
|
|
|
# FIXME
|
|
#class AdvancedTest(NutTestCommon, PrivateNutTest):
|
|
# '''Test advanced NUT functionalities'''
|
|
|
|
if __name__ == '__main__':
|
|
|
|
suite = unittest.TestSuite()
|
|
# more configurable
|
|
if (len(sys.argv) == 1 or sys.argv[1] == '-v'):
|
|
suite.addTest(unittest.TestLoader().loadTestsFromTestCase(BasicTest))
|
|
|
|
# Pull in private tests
|
|
#if use_private:
|
|
# suite.addTest(unittest.TestLoader().loadTestsFromTestCase(MyPrivateTest))
|
|
|
|
else:
|
|
print '''Usage:
|
|
test-nut.py [-v] basic tests
|
|
'''
|
|
sys.exit(1)
|
|
rc = unittest.TextTestRunner(verbosity=2).run(suite)
|
|
if not rc.wasSuccessful():
|
|
sys.exit(1)
|