| 1 | #!/usr/bin/python |
|---|
| 2 | # -*- coding: utf-8 -*- |
|---|
| 3 | # Copyright © 2009 by Karl Ramm |
|---|
| 4 | # |
|---|
| 5 | # All rights reserved. |
|---|
| 6 | # |
|---|
| 7 | # Permission to use, copy, modify, and distribute this software and |
|---|
| 8 | # its documentation for any purpose and without fee is hereby granted, |
|---|
| 9 | # provided that the above copyright notice appear in all copies and |
|---|
| 10 | # that both that copyright notice and this permission notice appear in |
|---|
| 11 | # supporting documentation, and that the name of Karl Ramm not be used |
|---|
| 12 | # in advertising or publicity pertaining to distribution of the |
|---|
| 13 | # software without specific, written prior permission. |
|---|
| 14 | # |
|---|
| 15 | # KARL RAMM DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, |
|---|
| 16 | # INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN |
|---|
| 17 | # NO EVENT SHALL KARL RAMM BE LIABLE FOR ANY SPECIAL, INDIRECT OR |
|---|
| 18 | # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS |
|---|
| 19 | # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, |
|---|
| 20 | # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION |
|---|
| 21 | # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. |
|---|
| 22 | |
|---|
| 23 | import os |
|---|
| 24 | import shlex |
|---|
| 25 | from os import execvp, _exit, dup2, fork, waitpid, close, chdir, \ |
|---|
| 26 | environ, WNOHANG, getgroups, kill |
|---|
| 27 | |
|---|
| 28 | from time import sleep, time |
|---|
| 29 | from sys import exit |
|---|
| 30 | from pty import openpty |
|---|
| 31 | from itertools import count |
|---|
| 32 | from threading import Thread |
|---|
| 33 | from subprocess import Popen, PIPE |
|---|
| 34 | from logging import getLogger, basicConfig, DEBUG |
|---|
| 35 | from tempfile import NamedTemporaryFile |
|---|
| 36 | from grp import getgrnam |
|---|
| 37 | from signal import SIGTERM |
|---|
| 38 | |
|---|
| 39 | environ['PAGER'] = 'cat' |
|---|
| 40 | |
|---|
| 41 | basicConfig(level=DEBUG, |
|---|
| 42 | format='%(asctime)s %(name)s.%(funcName)s:%(lineno)d %(message)s') |
|---|
| 43 | log = getLogger('vm') |
|---|
| 44 | |
|---|
| 45 | runseq = count().next |
|---|
| 46 | def runcmd(cmd): |
|---|
| 47 | this = runseq() |
|---|
| 48 | if hasattr(cmd, 'split'): |
|---|
| 49 | cmd = cmd.split() |
|---|
| 50 | log.info('(%d) starting %s', this, cmd) |
|---|
| 51 | p = Popen(cmd, stdout=PIPE, stderr=PIPE, close_fds=True) |
|---|
| 52 | stdout, stderr = p.communicate() |
|---|
| 53 | retcode = p.wait() |
|---|
| 54 | log.info('(%d) returned %d', this, retcode) |
|---|
| 55 | if stdout and stdout.strip(): |
|---|
| 56 | log.info('(%d) stdout = [%s]', this, stdout.strip()) |
|---|
| 57 | if stderr and stderr.strip(): |
|---|
| 58 | log.info('(%d) stderr = [%s]', this, stderr.strip()) |
|---|
| 59 | return retcode, stdout, stderr |
|---|
| 60 | |
|---|
| 61 | vmcmd = 'qemu' |
|---|
| 62 | # probe for kvm_ |
|---|
| 63 | retval, stdout, stderr = runcmd(['sh','-c','lsmod | grep kvm_']) |
|---|
| 64 | if retval == 0: |
|---|
| 65 | try: |
|---|
| 66 | gid = getgrnam('kvm').gr_gid |
|---|
| 67 | if gid in getgroups(): |
|---|
| 68 | vmcmd='kvm' |
|---|
| 69 | except: |
|---|
| 70 | pass |
|---|
| 71 | |
|---|
| 72 | class testmachine(object): |
|---|
| 73 | testmachines = {} |
|---|
| 74 | initparams = [ |
|---|
| 75 | ('basedir', os.getcwd()), |
|---|
| 76 | ('domain', 'example.com'), |
|---|
| 77 | ('hostname', 'test-%(index)d.%(domain)s'), |
|---|
| 78 | ('base_image', '%(basedir)s/master.img'), |
|---|
| 79 | ('image', '%(basedir)s/%(hostname)s.img'), |
|---|
| 80 | ('mkimage', 'qemu-img create -b %(base_image)s -f qcow2 %(image)s'), |
|---|
| 81 | ('qemu_binary', vmcmd), |
|---|
| 82 | ('qemuopts', '-nographic'), |
|---|
| 83 | ('memory', 256), |
|---|
| 84 | ('netargs', '-net nic,vlan=1,macaddr=02:00:00:00:00:%(index)02d -net vde,vlan=1,sock=/var/run/vde2/tap0.ctl'), |
|---|
| 85 | ('diskargs', '-hda %(image)s'), |
|---|
| 86 | ('qemu', '%(qemu_binary)s -m %(memory)s %(netargs)s %(diskargs)s %(qemuopts)s'), |
|---|
| 87 | ('logdir', '%(basedir)s'), |
|---|
| 88 | ('logfile', '%(logdir)s/%(hostname)s.log'), |
|---|
| 89 | ] |
|---|
| 90 | def __init__(self, index=None, **kw): |
|---|
| 91 | if index is not None: |
|---|
| 92 | if index in self.testmachines: |
|---|
| 93 | raise Exception('Test Machine #%d already exists' % index) |
|---|
| 94 | else: |
|---|
| 95 | for index in count(): |
|---|
| 96 | if index not in self.testmachines: |
|---|
| 97 | break |
|---|
| 98 | self.testmachines[index] = self |
|---|
| 99 | |
|---|
| 100 | self.params = {'index': index} |
|---|
| 101 | self.params.update(kw) |
|---|
| 102 | for (name, template) in self.initparams: |
|---|
| 103 | if name not in self.params: |
|---|
| 104 | if isinstance(template, basestring): # we can't substitute, say, ints |
|---|
| 105 | self.params[name] = template % self.params |
|---|
| 106 | else: |
|---|
| 107 | self.params[name] = template |
|---|
| 108 | |
|---|
| 109 | self.dead = False |
|---|
| 110 | |
|---|
| 111 | status, out, err = runcmd(self.params['mkimage']) |
|---|
| 112 | if status != 0: |
|---|
| 113 | raise Exception('Error creating image') |
|---|
| 114 | |
|---|
| 115 | self.qemuproc = loggedproc(self.params['qemu'], self.params['logfile']) |
|---|
| 116 | self.boottime = time() |
|---|
| 117 | |
|---|
| 118 | def wait(self): |
|---|
| 119 | if self.dead: |
|---|
| 120 | raise Exception('dead testmachine') |
|---|
| 121 | code = self.qemuproc.wait() |
|---|
| 122 | self.dead = True |
|---|
| 123 | log.debug('vm returned %d', code) |
|---|
| 124 | del testmachine.testmachines[self.params['index']] |
|---|
| 125 | return code |
|---|
| 126 | |
|---|
| 127 | def alive(self): |
|---|
| 128 | if self.dead: |
|---|
| 129 | return False |
|---|
| 130 | alive = self.qemuproc.alive() |
|---|
| 131 | if not alive: |
|---|
| 132 | self.dead = True |
|---|
| 133 | return alive |
|---|
| 134 | |
|---|
| 135 | def shoot(self): |
|---|
| 136 | self.qemuproc.murder(SIGTERM) |
|---|
| 137 | sleep(1) |
|---|
| 138 | if self.qemuproc.alive(): |
|---|
| 139 | self.qemuproc.murder(SIGKILL) |
|---|
| 140 | |
|---|
| 141 | class sshtestmachine(testmachine): |
|---|
| 142 | initparams = testmachine.initparams + [ |
|---|
| 143 | ('sshsock', '%(basedir)s/ssh.%%h.%%r.%%p'), |
|---|
| 144 | ('sshtarget', 'root@%(hostname)s'), |
|---|
| 145 | ('sshmaster', 'ssh -NMS %(sshsock)s -o stricthostkeychecking=no -o userknownhostsfile=./known-hosts -i ./ssh_key %(sshtarget)s'), |
|---|
| 146 | ('sshopt', '-o controlpath=%(sshsock)s -o stricthostkeychecking=no -o userknownhostsfile=./known-hosts -i ./ssh_key'), |
|---|
| 147 | ('ssh', 'ssh %(sshopt)s %(sshtarget)s'), |
|---|
| 148 | ('scp', 'scp %(sshopt)s'), |
|---|
| 149 | ] |
|---|
| 150 | |
|---|
| 151 | def __init__(self, index=None, **kw): |
|---|
| 152 | super(sshtestmachine, self).__init__(index, **kw) |
|---|
| 153 | |
|---|
| 154 | self.sshstate = 'go' |
|---|
| 155 | self.sshrunning = None |
|---|
| 156 | self.sshstart = None |
|---|
| 157 | if self.params['sshmaster']: |
|---|
| 158 | self.sshthread = Thread(target=self.sshmaster) |
|---|
| 159 | self.sshthread.start() |
|---|
| 160 | |
|---|
| 161 | def sshmaster(self): |
|---|
| 162 | log.info('starting sshmaster thread') |
|---|
| 163 | wait = 20 |
|---|
| 164 | log.debug('waiting for %d seconds', wait) |
|---|
| 165 | sleep(wait) |
|---|
| 166 | while self.sshstate == 'go': |
|---|
| 167 | self.sshstart = time() |
|---|
| 168 | self.sshrunning = True |
|---|
| 169 | runcmd(self.params['sshmaster']) |
|---|
| 170 | self.sshrunning = False |
|---|
| 171 | log.debug('sshmaster duration = %f, relative start = %f', |
|---|
| 172 | time() - self.sshstart, |
|---|
| 173 | self.sshstart - self.boottime) |
|---|
| 174 | log.debug('pausing for a second') |
|---|
| 175 | sleep(1) |
|---|
| 176 | |
|---|
| 177 | def run(self, cmd): |
|---|
| 178 | log.debug('%s: running %s', self.params['hostname'], cmd) |
|---|
| 179 | return runcmd(self.params['ssh'] + ' ' + cmd) |
|---|
| 180 | |
|---|
| 181 | def _scp(self, source, dest): |
|---|
| 182 | retval, stdout, stderr = runcmd(' '.join([self.params['scp'], |
|---|
| 183 | source, dest])) |
|---|
| 184 | if retval != 0: |
|---|
| 185 | raise Exception('scp failed', retval, stdout, stderr) |
|---|
| 186 | |
|---|
| 187 | def putfile(self, local, remote): |
|---|
| 188 | log.debug('%s: copying local %s to remote %s', self.params['hostname'], local, remote) |
|---|
| 189 | self._scp(local, '%s:%s' % (self.params['sshtarget'], remote)) |
|---|
| 190 | |
|---|
| 191 | def getfile(self, remote, local): |
|---|
| 192 | log.debug('%s: copying remote %s to local %s', self.params['hostname'], remote, local) |
|---|
| 193 | self._scp('%s:%s' % (self.params['sshtarget'], remote), local) |
|---|
| 194 | |
|---|
| 195 | def putstr(self, remote, string): |
|---|
| 196 | log.debug("%s: putting [%s] in remote %s", self.params['hostname'], string, remote) |
|---|
| 197 | fp = NamedTemporaryFile() |
|---|
| 198 | if string[-1] != '\n': |
|---|
| 199 | string += '\n' |
|---|
| 200 | fp.write(string) |
|---|
| 201 | fp.flush() |
|---|
| 202 | return self.putfile(fp.name, remote) |
|---|
| 203 | |
|---|
| 204 | def doscript(self, script): |
|---|
| 205 | if hasattr(script, 'splitlines'): |
|---|
| 206 | script = script.splitlines() |
|---|
| 207 | for cmd in script: |
|---|
| 208 | cmd = cmd.lstrip() |
|---|
| 209 | if cmd and cmd[0] != '#': |
|---|
| 210 | yield (cmd,) + self.run(cmd) |
|---|
| 211 | |
|---|
| 212 | def script(self, script, bail=True): |
|---|
| 213 | results=[] |
|---|
| 214 | for cmd, retval, stdout, stderr in self.doscript(script): |
|---|
| 215 | if retval: |
|---|
| 216 | raise(Exception(self.params['hostname'], cmd, retval, stdout, stderr)) |
|---|
| 217 | results.append((cmd, retval, stdout, stderr)) |
|---|
| 218 | return results #I'm not sure we should even bother |
|---|
| 219 | |
|---|
| 220 | def shutdown(self): |
|---|
| 221 | log.debug('shutting down machine') |
|---|
| 222 | self.sshstate = 'stop' |
|---|
| 223 | self.run('poweroff') |
|---|
| 224 | |
|---|
| 225 | def shoot(self): |
|---|
| 226 | self.sshstate='stop' |
|---|
| 227 | super(sshtestmachine, self).shoot() |
|---|
| 228 | |
|---|
| 229 | def ready(self): |
|---|
| 230 | t = time() |
|---|
| 231 | log.debug('sshrunning = %s sshstart = %s t = %f', |
|---|
| 232 | self.sshrunning, self.sshstart, t) |
|---|
| 233 | return self.sshrunning and self.sshstart < (t - 15.0) |
|---|
| 234 | |
|---|
| 235 | class proc(object): |
|---|
| 236 | def __init__(self, cmd): |
|---|
| 237 | if hasattr(cmd, 'split'): |
|---|
| 238 | self.cmdlist = shlex.split(cmd) |
|---|
| 239 | else: |
|---|
| 240 | self.cmdlist = cmd |
|---|
| 241 | self.status = None |
|---|
| 242 | self.start() |
|---|
| 243 | def alive(self): |
|---|
| 244 | if self.status is not None: |
|---|
| 245 | return False |
|---|
| 246 | pid, status = waitpid(self.pid, WNOHANG) |
|---|
| 247 | if pid: |
|---|
| 248 | log.debug('%d returned %d in check', pid, status) |
|---|
| 249 | self.status = status |
|---|
| 250 | return False |
|---|
| 251 | return True |
|---|
| 252 | def start(self): |
|---|
| 253 | log.debug('starting %s', self.cmdlist) |
|---|
| 254 | self.pid = fork() |
|---|
| 255 | if self.pid == 0: |
|---|
| 256 | self._launch() |
|---|
| 257 | def _launch(self): |
|---|
| 258 | execvp(self.cmdlist[0], self.cmdlist) |
|---|
| 259 | _exit(99) |
|---|
| 260 | def wait(self): |
|---|
| 261 | if self.status is None: |
|---|
| 262 | pid, self.status = waitpid(self.pid, 0) |
|---|
| 263 | return self.status |
|---|
| 264 | def murder(self, sig=SIGTERM): |
|---|
| 265 | kill(self.pid, sig) |
|---|
| 266 | |
|---|
| 267 | class loggedproc(proc): |
|---|
| 268 | def __init__(self, cmdlist, logfile): |
|---|
| 269 | self.logfile = logfile |
|---|
| 270 | self.master = None |
|---|
| 271 | self.slave = None |
|---|
| 272 | super(loggedproc, self).__init__(cmdlist) |
|---|
| 273 | def start(self): |
|---|
| 274 | self.master, self.slave = openpty() |
|---|
| 275 | self.logfp = open(self.logfile, 'w') |
|---|
| 276 | super(loggedproc, self).start() |
|---|
| 277 | close(self.slave) |
|---|
| 278 | self.logfp.close() |
|---|
| 279 | def _launch(self): |
|---|
| 280 | close(self.master) |
|---|
| 281 | dup2(self.slave, 0) # stdin |
|---|
| 282 | dup2(self.logfp.fileno(), 1) # stdout |
|---|
| 283 | dup2(self.logfp.fileno(), 2) # stderr |
|---|
| 284 | self.logfp.close() |
|---|
| 285 | close(self.slave) |
|---|
| 286 | super(loggedproc, self)._launch() |
|---|
| 287 | |
|---|
| 288 | def main(): |
|---|
| 289 | pass |
|---|
| 290 | |
|---|
| 291 | if __name__ == '__main__': |
|---|
| 292 | main() |
|---|