1#!/usr/bin/env python 2# 3# This file is Copyright (c) 2010 by the GPSD project 4# BSD terms apply: see the file COPYING in the distribution root for details. 5# 6"""\ 7flocktest - shepherd script for the GPSD test flock 8 9usage: flocktest [-c] [-q] [-d subdir] [-k key] -v [-x exclude] [-?] 10 11The -? makes flocktest prints this help and exits. 12 13The -c option dumps flocktest's configuration and exits 14 15The -k mode installs a specified file of ssh public keys on all machines 16 17Otherwise, the remote flockdriver script is executed on each machine. 18 19The -d option passes it a name for the remote test subdirectory 20If you do not specify a subdirectory name, the value of $LOGNAME will be used. 21 22The -q option suppresses CIA notifications 23 24The -v option shows all ssh commands issued, runs flockdriver with -x set 25and causes logs to be echoed even on success. 26 27The -x option specifies a comma-separated list of items that are 28either remote hostnames or architecture tags. Matching sites are 29excluded. You may wish to use this to avoid doing remote tests that 30are redundant with your local ones. 31 32Known bug: The -k has no atomicity check. Running it from two 33flocktest instances concurrently could result in a scrambled keyfile. 34""" 35# This code runs compatibly under Python 2 and 3.x for x >= 2. 36# Preserve this property! 37from __future__ import absolute_import, print_function, division 38 39import os, sys, getopt, socket, threading, time 40 41try: 42 import configparser # Python 2 43except ImportError: 44 import ConfigParser as configparser # Python 3 45 46try: 47 import commands # Python 2 48except ImportError: 49 import subprocess as commands # Python 3 50 51flockdriver = ''' 52#!/bin/sh 53# 54# flockdriver - conduct regression tests as an agent for a remote flocktest 55# 56# This file was generated at %(date)s. Do not hand-hack. 57 58quiet=no 59while getopts dq opt 60do 61 case $opt in 62 d) subdir=$2; shift; shift ;; 63 q) quiet=yes; shift ;; 64 esac 65done 66 67# Fully qualified domain name of the repo host. You can hardwire this 68# to make the script faster. The -f option works under Linux and FreeBSD, 69# but not OpenBSD and NetBSD. But under OpenBSD and NetBSD, 70# hostname without options gives the FQDN. 71if hostname -f >/dev/null 2>&1 72then 73 site=`hostname -f` 74else 75 site=`hostname` 76fi 77 78if [ -f "flockdriver.lock" ] 79then 80 logmessage="A test was already running when you initiated this one." 81 cd $subdir 82else 83 echo "Test begins: "`date` 84 85 echo "Site: $site" 86 echo "Directory: ${PWD}/${subdir}" 87 88 # Check the origin against the repo origin. If they do not match, 89 # force a re-clone of the repo 90 if [ -d $subdir ] 91 then 92 repo_origin=`(cd $subdir; git config remote.origin.url)` 93 if [ $repo_origin != "%(origin)s" ] 94 then 95 echo "Forced re-clone." 96 rm -fr $subdir 97 fi 98 fi 99 100 # Set up or update the repo 101 if [ ! -d $subdir ] 102 then 103 git clone %(origin)s $subdir 104 cd $subdir 105 else 106 cd $subdir; 107 git pull 108 fi 109 110 # Scripts in the test directory need to be able to run binaries in same 111 PATH="$PATH:." 112 113 # Perform the test 114 if ( %(regression)s ) 115 then 116 logmessage="Regression test succeeded." 117 status=0 118 else 119 logmessage="Regression test failed." 120 status=1 121 fi 122 123 echo "Test ends: "`date` 124fi 125 126# Here is where we abuse CIA to do our notifications for us. 127 128# Addresses for the e-mail 129from="FLOCKDRIVER-NOREPLY@${site}" 130to="cia@cia.navi.cx" 131 132# SMTP client to use 133sendmail="sendmail -t -f ${from}" 134 135# Should include all places sendmail is likely to lurk. 136PATH="$PATH:/usr/sbin/" 137 138# Identify what just succeeded or failed 139merged=$(git rev-parse HEAD) 140rev=$(git describe ${merged} 2>/dev/null) 141[ -z ${rev} ] && rev=${merged} 142refname=$(git symbolic-ref HEAD 2>/dev/null) 143refname=${refname##refs/heads/} 144 145# And the git version 146gitver=$(git --version) 147gitver=${gitver##* } 148 149if [ $quiet = no ] 150then 151 ${sendmail} << EOM 152Message-ID: <${merged}.${subdir}.blip@%(project)s> 153From: ${from} 154To: ${to} 155Content-type: text/xml 156Subject: DeliverXML 157 158<message> 159 <generator> 160 <name>%(project)s Remote Test Flock Driver</name> 161 <version>${gitver}</version> 162 <url>${origin}/flockdriver</url> 163 </generator> 164 <source> 165 <project>%(project)s</project> 166 <branch>${refname}@${site}</branch> 167 </source> 168 <timestamp>`date`</timestamp> 169 <body> 170 <commit> 171 <author>${subdir}</author> 172 <revision>${rev}</revision> 173 <log>${logmessage}</log> 174 </commit> 175 </body> 176</message> 177EOM 178fi 179 180exit $status 181# End. 182''' 183 184class FlockThread(threading.Thread): 185 def __init__(self, site, command): 186 threading.Thread.__init__(self) 187 self.site = site 188 self.command = command 189 def run(self): 190 (self.status, self.output) = commands.getstatusoutput(self.command) 191 192class TestSite(object): 193 "Methods for performing tests on a single remote site." 194 def __init__(self, fqdn, config, execute=True): 195 self.fqdn = fqdn 196 self.config = config 197 self.execute = execute 198 self.me = self.config["login"] + "@" + self.fqdn 199 def error(self, msg): 200 "Report an error while executing a remote command." 201 sys.stderr.write("%s: %s\n" % (self.fqdn, msg)) 202 def do_remote(self, remote): 203 "Execute a command on a specified remote host." 204 command = "ssh " 205 if "port" in self.config: 206 command += "-p %s " % self.config["port"] 207 command += "%s '%s'" % (self.me, remote) 208 if self.verbose: 209 print(command) 210 self.thread = FlockThread(self, command) 211 self.thread.start() 212 def update_keys(self, filename): 213 "Upload a specified file to replace the remote authorized keys." 214 if 'debian.org' in self.me: 215 self.error("updating keys on debian.org machines makes no sense.") 216 return 1 217 command = "scp '%s' %s:~/.ssh/.authorized_keys" % (os.path.expanduser(filename), self.me) 218 if self.verbose: 219 print(command) 220 status = os.system(command) 221 if status: 222 self.error("copy with '%s' failed" % command) 223 return status 224 def do_append(self, filename, string): 225 "Append a line to a specified remote file, in foreground." 226 self.do_remote("echo \"%s\" >>%s" % (string.strip(), filename)) 227 def do_flockdriver(self, agent, invocation): 228 "Copy flockdriver to the remote site and run it." 229 self.starttime = time.time() 230 if self.config.get("quiet", "no") == "yes": 231 invocation += " -q" 232 uploader = "ssh -p %s %s 'cat >%s'" \ 233 % (self.config.get("port", "22"), self.me, agent) 234 if self.verbose: 235 print(uploader) 236 ofp = os.popen(uploader, "w") 237 self.config['date'] = time.ctime() 238 ofp.write(flockdriver % self.config) 239 if ofp.close(): 240 print("flocktest: agent upload failed", file=sys.stderr) 241 else: 242 self.do_remote(invocation) 243 self.elapsed = time.time() - self.starttime 244 245class TestFlock(object): 246 "Methods for performing parallel tests on a flock of remote sites." 247 ssh_options = "no-port-forwarding,no-X11-forwarding," \ 248 "no-agent-forwarding,no-pty " 249 def __init__(self, sitelist, verbose=False): 250 self.sitelist = sitelist 251 self.verbose = verbose 252 def update_remote(self, filename): 253 "Copy a specified file to the remote home on all machines." 254 for site in self.sitelist: 255 site.update_remote(filename) 256 def do_remotes(self, agent, invocation): 257 "Execute a command on all machines in the flock." 258 slaves = [] 259 print("== testing at: %s ==" % flock.listdump()) 260 starttime = time.time() 261 for site in self.sitelist: 262 site.do_flockdriver(agent, invocation) 263 for site in sites: 264 site.thread.join() 265 failed = 0 266 for site in sites: 267 if site.thread.status: 268 print("== %s test FAILED in %.2f seconds, status %d ==" % (site.fqdn, site.elapsed, site.thread.status)) 269 failed += 1 270 print(site.thread.output) 271 else: 272 print("== %s test succeeded in %.2f seconds ==" % (site.fqdn, site.elapsed)) 273 if self.verbose: 274 print(site.thread.output) 275 elapsed = time.time() - starttime 276 print("== %d tests completed in %.2f seconds: %d failed ==" % (len(sites), elapsed, failed)) 277 def exclude(self, exclusions): 278 "Delete matching sites." 279 self.sitelist = [x for x in self.sitelist if x.fqdn not in exclusions and x.config["arch"] not in exclusions] 280 def update_keys(self, keyfile): 281 "Copy the specified public key file to all sites." 282 for site in self.sitelist: 283 site.update_keys(keyfile) 284 def listdump(self): 285 "Return a dump of the site list." 286 return ", ".join([x.fqdn for x in self.sitelist]) 287 288if __name__ == '__main__': 289 try: 290 (options, arguments) = getopt.getopt(sys.argv[1:], "cd:kqvx:?") 291 except getopt.GetoptError as msg: 292 print("flocktest: " + str(msg)) 293 raise SystemExit(1) 294 295 exclusions = [] 296 subdir = None 297 copykeys = None 298 verbose = False 299 dumpconf = False 300 cianotify = True 301 for (switch, val) in options: 302 if switch == '-c': # Dump flocktest configuration 303 dumpconf = True 304 elif switch == '-d': # Set the test subdirectory name 305 subdir = val 306 elif switch == '-k': # Install the access keys 307 copykeys = True 308 elif switch == '-q': # Suppress CIA notifications 309 cianotify = False 310 elif switch == '-v': # Display build log even when no error 311 verbose = True 312 elif switch == '-x': # Exclude specified sites or architectures 313 exclusions = [x.strip() for x in val.split(",")] 314 else: # switch == '-?': 315 print(__doc__) 316 sys.exit(0) 317 318 config = configparser.RawConfigParser() 319 config.read(["flocktest.ini", ".flocktest.ini"]) 320 if arguments: 321 config.set("DEFAULT", "origin", arguments[0]) 322 if not config.has_option("DEFAULT", "origin"): 323 print("flocktest: repository required.", file=sys.stderr) 324 sys.exit(1) 325 sites = [] 326 for site in config.sections(): 327 newsite = TestSite(site, dict(config.items(site))) 328 newsite.verbose = verbose 329 if newsite.config["status"].lower() == "up": 330 sites.append(newsite) 331 flock = TestFlock(sites, verbose) 332 if exclusions: 333 flock.exclude(exclusions) 334 if dumpconf: 335 config.write(sys.stdout) 336 elif copykeys: 337 keyfile = config.get("DEFAULT", "sshkeys") 338 flock.update_keys(keyfile) 339 else: 340 if not subdir: 341 subdir = os.getenv("LOGNAME") 342 if not subdir: 343 print("flocktest: you don't exist, go away!") 344 sys.exit(1) 345 agent = "flockdriver.%s" % subdir 346 invocation = "sh flockdriver.%s -d %s" % (subdir, subdir,) 347 if not cianotify: 348 invocation += " -q" 349 if verbose > 1: 350 invocation = "sh -x " + invocation 351 flock.do_remotes(agent, invocation) 352 353# The following sets edit modes for GNU EMACS 354# Local Variables: 355# mode:python 356# End: 357