#!/usr/bin/python # # test-nut.py quality assurance test script # Copyright (C) 2008-2011 Arnaud Quette # Copyright (C) 2012 Jamie Strandboge # # 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 . # ''' *** 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 # The behaviour of the upstream systemd services has changed breaking the # existing tests. This is a quick fix for the tests so they use the sysv # initscript instead of the systemd services. # The tests should probably be adjusted in the future. os.environ['SYSTEMCTL_SKIP_REDIRECT'] = '1' use_private = True try: from private.qrt.nut import PrivateNutTest except ImportError: class PrivateNutTest(object): '''Empty class''' print("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=0o755) # 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.assertEqual(expected, rc, result + report) time.sleep(10) 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.assertEqual(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.assertEqual(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.assertEqual(expected, rc, result + report) def _testDaemons(self, daemons): '''Daemons running''' for d in daemons: # A note on the driver pid file: its name is # -.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) @unittest.skip("Skip flaky test. See #1023530 and LP: #1998481 for further reference.") 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''' @unittest.skip("Skip flaky test. See #1023530 and LP: #1998481 for further reference.") 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=0o755) 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)