Merge "Bruno automation test code upload: New test cases added: some networking and TR069 test cases."
diff --git a/.gitignore b/.gitignore
index 2f836aa..62523e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
*~
*.pyc
+/*.stamp
diff --git a/Makefile b/Makefile
index 4922c26..e3d52a3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,32 +1,97 @@
-PORT=/dev/ttyS0
+PORT=/dev/ttyS0 #TODO(apenwarr): don't hardcode this
PASSWORD=google
-PORTSH=./portsh/portsh $(PORT) -p'$(PASSWORD)'
-.NOTPARALLEL:
+PORTSH=./portsh/portsh -p'$(PASSWORD)'
+NMAP_SUBNETS=192.168.2.0/24 #TODO(apenwarr): auto-read the subnet from eth1?
default: all
-all:
- @echo "Nothing to do."
+all: map.stamp
-test: install-wvtest
+lint:
+ gpylint *.py configs/core.py
+
+test:
wvtest/wvtestrun $(MAKE) runtests
-runtests: $(addsuffix .run,$(wildcard [0-9]*.sh [0-9]*.py))
+#TODO(apenwarr): use a smarter allocator.
+# We could enable parallelism by depending on $(addsuffix ...) instead of
+# looping through them one by one. But then we end up running multiple
+# tests on the same device at once, which would cause conflicts. We need
+# a smarter allocator first.
+runtests: localtests install-wvtest
+ for d in $(addsuffix .run,$(wildcard [0-9]*.sh [0-9]*.py)); do \
+ $(MAKE) $$d; \
+ done
-install-wvtest:
- tar -cf - *.py *.sh wvtest/ | \
- $(PORTSH) \
+localtests:
+ ./wvtest/wvtest.py *_test.py configs/*_test.py
+
+install-wvtest: map.stamp *.py *.sh wvtest/*
+ @echo 'Testing "install-wvtest" in Makefile:'
+ tar -cf - $^ | \
+ $(PORTSH) $(PORT) \
'mkdir -p /tmp/tests && cd /tmp/tests && tar -xf -'
-%.sh.run: %.sh install-wvtest
+%.sh.run: %.sh
echo "Testing $<"
- $(PORTSH) \
+ $(PORTSH) $(PORT) \
'cd /tmp/tests && sh ./$<' </dev/null
-%.py.run: %.py install-wvtest
+%.py.run: %.py
echo "Testing $<"
- $(PORTSH) \
+ $(PORTSH) $(PORT) \
'cd /tmp/tests && python ./wvtest/wvtest.py $<' </dev/null
+# This rule regenerates the map *every* time. Run it when your node
+# configuration has changed.
+map: serialmap nmap
+ touch map.stamp
+
+# This rule just runs once; 'make map' or delete map.stamp to re-run.
+# Depend on this rule to ensure we have a map, without regenerating it
+# every time we run make.
+map.stamp:
+ $(MAKE) map
+
+nmap:
+ rm -f configs/nmap.*.tmp
+ $(MAKE) $$(nmap -n -p22 -oG - $(NMAP_SUBNETS) | \
+ grep ^Host: | \
+ awk '{printf "configs/nmap.%s.tmp\n", $$2}')
+ cat configs/header configs/nmap.*.tmp >configs/auto_nmap.py.new
+ mv configs/auto_nmap.py.new configs/auto_nmap.py
+
+serialmap:
+ rm -f configs/tty.*.tmp
+ $(MAKE) $$(cd /dev && /bin/ls ttyS*[0-9] ttyUSB*[0-9] | \
+ awk '{printf "configs/tty.%s.tmp\n", $$1}')
+ cat configs/header configs/tty.*.tmp >configs/auto_serial.py.new
+ mv configs/auto_serial.py.new configs/auto_serial.py
+
+
+PROBE_CMD=\
+ PATH=$$PATH:/bin:/usr/bin:/sbin:/usr/sbin; \
+ hnvram -r 1ST_SERIAL_NUMBER -r SERIAL_NO -r MAC_ADDR -r PLATFORM_NAME; \
+ [ -e /tmp/NFS ] && echo IS_NFSROOT=1; \
+ ip addr show br0; \
+ ip addr show eth0; \
+ ip addr show eth1; \
+ ip addr show eth2; \
+ cat /proc/cpuinfo
+
+configs/nmap.%.tmp:
+ ssh -oNumberOfPasswordPrompts=0 \
+ root@$* \
+ 'echo NET_OK=1; $(PROBE_CMD)' 2>&1 | \
+ ./parse-probe-data >$@.new
+ mv $@.new $@
+
+configs/tty.%.tmp:
+ $(PORTSH) $* \
+ 'echo SERIALPORT=$*; $(PROBE_CMD)' 2>&1 | \
+ ./parse-probe-data >$@.new
+ mv $@.new $@
+
clean:
- rm -f *~ .*~ */*~ */.*~ *.pyc */*.pyc
+ rm -f *~ .*~ */*~ */.*~ *.pyc */*.pyc *.stamp \
+ configs/*.tmp configs/*.new configs/auto_*.py
diff --git a/checkall b/checkall
new file mode 100755
index 0000000..bd5aa40
--- /dev/null
+++ b/checkall
@@ -0,0 +1,20 @@
+#!/bin/sh
+#
+# Display a one-line overview of whether each device is up.
+#
+mydir=$(dirname "$0")
+cd "$mydir"
+hosts=$(./config)
+
+out=
+./run $hosts -- 'ping -c1 -w1 -n $ip >/dev/null && echo yes || echo no' |
+sort | (
+ while read name ok; do
+ case "$ok" in
+ yes) out="$out${name%:} " ;;
+ no) out="$out^${name%:} " ;;
+ *) ( echo; echo "Unexpected: '$name' '$ok'" ) >&2 ;;
+ esac
+ done
+ echo "$out"
+)
diff --git a/config b/config
new file mode 120000
index 0000000..f85c6b1
--- /dev/null
+++ b/config
@@ -0,0 +1 @@
+config.py
\ No newline at end of file
diff --git a/config.py b/config.py
new file mode 100755
index 0000000..9f65977
--- /dev/null
+++ b/config.py
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+"""Test configurations. Includes a list of auto-probed bruno devices."""
+
+import re
+import sys
+from configs import hosts
+from configs.core import Host
+from configs.core import QueryError
+from portsh import options
+
+__author__ = 'Avery Pennarun (apenwarr@google.com)'
+
+
+optspec = """
+config [options...] <bool|^bool|key=value>...
+--
+random Choose random matches instead of the first ones
+max= Return no more than max= matches
+min= If less than min= matches, return an error code
+n,num= Return exactly num= matches
+v,verbose Display the entire config entry for each match, not just the name
+k,keys= If -v isn't given, show these comma/space-separated attrs [name]
+"""
+
+
+def main():
+ o = options.Options(optspec)
+ opt, _, extra = o.parse(sys.argv[1:])
+ if opt.num:
+ if opt.min or opt.max:
+ o.fatal("don't specify --num with --min/--max")
+ opt.min = opt.max = opt.num
+
+ kwargs = dict(is_alive=True)
+ for kvs in extra:
+ kv = kvs.split('=', 1) + [None]
+ key = kv[0]
+ value = kv[1]
+ if not hasattr(Host, key):
+ o.fatal('no key named %r; valid keys: %s'
+ % (key, ', '.join(Host.__slots__)))
+ sys.exit(1)
+ if key.startswith('^'):
+ kwargs[key[1:]] = False
+ elif value is not None:
+ kwargs[key] = value
+ else:
+ kwargs[key] = True
+ try:
+ matches = hosts.Query(mincount=opt.min, maxcount=opt.max,
+ randomize=opt.random, **kwargs)
+ except QueryError, e:
+ o.fatal(str(e))
+ if opt.verbose:
+ for match in matches:
+ print match
+ else:
+ keys = re.split(r'[,\s]', opt.keys)
+ for key in keys:
+ if not hasattr(Host, key):
+ o.fatal('no key named %r; valid keys: %s'
+ % (key, ', '.join(Host.__slots__)))
+ for match in matches:
+ print ','.join(str(getattr(match, key) or '') for key in keys)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/configs/.gitignore b/configs/.gitignore
new file mode 100644
index 0000000..f55461c
--- /dev/null
+++ b/configs/.gitignore
@@ -0,0 +1,3 @@
+/auto_*.py
+/*.tmp
+/*.new
diff --git a/configs/__init__.py b/configs/__init__.py
new file mode 100644
index 0000000..3b6954a
--- /dev/null
+++ b/configs/__init__.py
@@ -0,0 +1,18 @@
+from core import hosts
+
+try:
+ import manual
+except ImportError:
+ pass
+
+try:
+ import auto_nmap
+except ImportError:
+ pass
+
+try:
+ import auto_serial
+except ImportError:
+ pass
+
+
diff --git a/configs/core.py b/configs/core.py
new file mode 100644
index 0000000..f85e2fa
--- /dev/null
+++ b/configs/core.py
@@ -0,0 +1,151 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+"""Base classes for configuration objects."""
+
+__author__ = 'Avery Pennarun (apenwarr@google.com)'
+
+
+import random
+
+
+class QueryError(Exception):
+ pass
+
+
+class AttrSet(object):
+ """A simple set of key-value pairs represented as a class with members."""
+
+ __slots__ = ()
+
+ def __init__(self, **kwargs):
+ for key in self.__slots__:
+ setattr(self, key, None)
+ self.Set(**kwargs)
+
+ def Set(self, **kwargs):
+ for key, value in kwargs.iteritems():
+ setattr(self, key, value)
+
+ def Match(self, **kwargs):
+ for key, value in kwargs.iteritems():
+ ival = getattr(self, key)
+ # None and False can match each other
+ #gpylint: disable-msg=C6403
+ if ival == False: ival = None
+ if value == False: value = None
+ if value == True and ival:
+ continue # any nonempty value is true, so match it
+ if ival != value:
+ return False
+ return True
+
+ def _Items(self):
+ for key in self.__slots__:
+ value = getattr(self, key)
+ if value == True: #gpylint: disable-msg=C6403
+ yield key
+ elif value is not None and value != False: #gpylint: disable-msg=C6403
+ yield '%s=%r' % (key, value)
+
+ def __repr__(self):
+ return '<%s>' % ','.join(self._Items())
+
+ def __str__(self):
+ return '\n '.join(['%s:' % self.__class__.__name__]
+ + list(self._Items()))
+
+
+class Host(AttrSet):
+ __slots__ = (
+ 'name',
+ 'ether',
+ 'ip',
+ 'ip6',
+ 'cpu',
+ 'platform',
+ 'serialport',
+ 'serialno',
+ 'is_canary',
+ 'is_alive',
+ 'is_alive_net',
+ 'is_storage',
+ 'is_tv',
+ 'is_nfsroot',
+ 'has_ether',
+ 'has_moca',
+ 'has_wifi',
+ )
+
+
+class Hosts(list):
+ """A searchable/queryable list of Host objects."""
+
+ def FindAll(self, **kwargs):
+ return [i for i in self if i.Match(**kwargs)]
+
+ def _FindOne(self, **kwargs):
+ l = self.FindAll(**kwargs)
+ if l:
+ return l[0]
+
+ def FindOne(self, **kwargs):
+ v = self._FindOne(**kwargs)
+ if v is None:
+ raise KeyError(kwargs)
+ return v
+
+ def FindOrAdd(self, **kwargs):
+ h = self._FindOne(**kwargs)
+ if not h:
+ h = Host(**kwargs)
+ self.append(h)
+ if not h.name:
+ h.name = h.ether or h.serialno or h.ip or h.ip6
+ return h
+
+ def Query(self, mincount=None, maxcount=None, randomize=False, **kwargs):
+ """Query the hosts list according to the given parameters.
+
+ Args:
+ mincount: the minimum number of hosts to return.
+ maxcount: the maximum number of hosts to return.
+ randomize: return a random subset or the first maxcount matches.
+ **kwargs: key=value pairs to pass to hosts.FindAll().
+ Returns:
+ A list of matching Host objects.
+ Raises:
+ QueryError: if mincount > maxcount or other invalid options.
+ """
+ matches = self.FindAll(**kwargs)
+ if maxcount and maxcount < 0:
+ raise QueryError('max(%s) must be >= 0' % maxcount)
+ if maxcount and mincount > maxcount:
+ raise QueryError('min(%s) > max(%s)' % (mincount, maxcount))
+ if mincount > len(matches):
+ raise QueryError('not enough matches found (got=%d, wanted=%r)'
+ % (len(matches), mincount))
+ if randomize:
+ random.shuffle(matches)
+ else:
+ matches.sort(key=lambda i: (i.name, i.serialno, i.ether, i.ip6, i.ip))
+ if maxcount is not None and len(matches) > maxcount:
+ matches = matches[:maxcount]
+ return matches
+
+ def __str__(self):
+ return '\n'.join(str(i) for i in self)
+
+
+hosts = Hosts()
+
+
+def main():
+ print 'hello'
+ hosts.FindOrAdd(ether='11:22:33:44:55:66').Set(name='myhost',
+ ip='1.2.3.4')
+ print hosts
+
+
+if __name__ == '__main__':
+ main()
diff --git a/configs/core_test.py b/configs/core_test.py
new file mode 100644
index 0000000..21b1f5f
--- /dev/null
+++ b/configs/core_test.py
@@ -0,0 +1,46 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+# Disable some checks that aren't important for tests:
+#gpylint: disable-msg=E0602,C6409,C6111,C0111
+
+__author__ = 'Avery Pennarun (apenwarr@google.com)'
+
+from wvtest import *
+import core
+
+
+@wvtest
+def testConfigCore():
+ hosts = core.Hosts()
+ WVPASSEQ(len(hosts), 0)
+ h1 = hosts.FindOrAdd(ether='11:22:33:44:55:66')
+ h1.Set(name='testname', ip='1.2.3.4')
+ h1.platform = 'bob'
+ WVPASSEQ(len(hosts), 1)
+ h1b = hosts.FindOrAdd(ip='1.2.3.4')
+ WVPASSEQ(len(hosts), 1)
+ WVPASSEQ(h1b.name, 'testname')
+ WVPASSEQ(h1b.ether, '11:22:33:44:55:66')
+ WVPASSEQ(h1, h1b)
+
+ # test Host.Query()
+ WVPASSEQ(hosts.Query(ip=True), [h1])
+ WVPASSEQ(hosts.Query(ip=False), [])
+ WVPASSEQ(hosts.Query(ip='1.2.3.4'), [h1])
+ WVPASSEQ(hosts.Query(ip='1.2.3.5'), [])
+ WVPASSEQ(hosts.Query(ip='1.2.3.4', maxcount=0), [])
+ WVEXCEPT(core.QueryError, hosts.Query, ip='1.2.3.5', mincount=1)
+
+ # add a second host
+ h2 = hosts.FindOrAdd(ether='22:33:44:22:33:44')
+ h2.Set(name='test2')
+
+ WVPASSEQ(len(hosts), 2)
+ WVPASSEQ(hosts.Query(ip='1.2.3.4'), [h1])
+ WVPASSEQ(hosts.Query(ip=None), [h2])
+ WVPASSEQ(hosts.Query(name='test2'), [h2])
+ WVPASSEQ(hosts.Query(ip=True), [h1])
+ WVPASSEQ(sorted(hosts.Query(name=True)), sorted([h1, h2]))
+ WVPASSEQ(hosts.Query(ether=True, name='test2'), [h2])
+ WVPASSEQ(hosts.Query(ether=True, name='testname'), [h1])
diff --git a/configs/header b/configs/header
new file mode 100644
index 0000000..2a22f55
--- /dev/null
+++ b/configs/header
@@ -0,0 +1,4 @@
+#
+# AUTO-GENERATED by Makefile: do not edit!
+#
+from core import hosts
diff --git a/configs/manual.py b/configs/manual.py
new file mode 100644
index 0000000..99cf21b
--- /dev/null
+++ b/configs/manual.py
@@ -0,0 +1,26 @@
+# insert manual configuration here
+from core import hosts
+
+
+#
+# Avery's test network in NYC
+#
+
+h = hosts.FindOrAdd(ether='00:03:9a:33:44:66')
+h.name = 't0-1'
+h.is_canary = True
+
+h = hosts.FindOrAdd(ether='42:42:42:42:42:12')
+h.name = 't0-2'
+
+h = hosts.FindOrAdd(ether='42:21:06:66:aa:03')
+h.name = 't0-3'
+
+h = hosts.FindOrAdd(ether='00:03:42:21:03:69')
+h.name = 't0-4'
+
+h = hosts.FindOrAdd(ether='00:1a:11:30:63:77')
+h.name = 'b2-1'
+
+h = hosts.FindOrAdd(ether='00:1a:11:30:5e:72')
+h.name = 't2-3'
diff --git a/parse-probe-data b/parse-probe-data
new file mode 100755
index 0000000..d3ec307
--- /dev/null
+++ b/parse-probe-data
@@ -0,0 +1,76 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+
+import re
+import sys
+
+
+__author__ = 'Avery Pennarun (apenwarr@google.com)'
+
+
+def main():
+ data = sys.stdin.read().strip()
+ header_comment = re.sub(re.compile(r'^', re.M), '# ', data)
+ elements = []
+ ether = None
+
+ g = re.search(re.compile(r'^(?:1ST_SERIAL_NUMBER=|SERIAL_NO=)(.+)', re.M),
+ data)
+ if g:
+ elements.append('serialno=%r' % g.group(1))
+
+ g = re.search(re.compile(r'^PLATFORM_NAME=(.+)', re.M), data)
+ if g:
+ platform = g.group(1).upper()
+ elements.append('platform=%r' % platform)
+ if platform.startswith('GFMS'):
+ elements.append('is_storage=1')
+ elif platform.startswith('GFHD'):
+ elements.append('is_tv=1')
+
+ if re.search(re.compile(r'^NET_OK=1', re.M), data):
+ elements.append('is_alive=1')
+ elements.append('is_alive_net=1')
+
+ if re.search(re.compile(r'^IS_NFSROOT=1', re.M), data):
+ elements.append('is_nfsroot=1')
+
+ g = re.search(re.compile(r'^SERIALPORT=(.+)', re.M), data)
+ if g:
+ elements.append('is_alive=1')
+ elements.append('serialport=%r' % g.group(1))
+
+ g = re.search(r'inet ([\d\.]+)', data)
+ if g:
+ elements.append('ip=%r' % g.group(1))
+
+ g = re.search(r'inet6 ([a-fA-F0-9:]+)', data)
+ if g:
+ elements.append('ip6=%r' % g.group(1).lower())
+
+ g = re.search(r'link/ether ([a-fA-F0-9:]+)', data)
+ if g:
+ ether = g.group(1)
+ elements.append('ether=%r' % g.group(1).lower())
+
+ if re.search(r'eth0:.*LOWER_UP', data):
+ elements.append('has_ether=1')
+ if re.search(r'eth1:.*LOWER_UP', data):
+ elements.append('has_moca=1')
+ if re.search(r'eth2:.*LOWER_UP', data):
+ elements.append('has_wifi=1')
+
+ g = re.search(r'system type\s*:\s*(\S+)', data)
+ if g:
+ elements.append('cpu=%r' % g.group(1).upper())
+
+ sys.stdout.write('\n%s\n' % header_comment)
+ if elements and ether:
+ sys.stdout.write('h = hosts.FindOrAdd(ether=%r)\n' % ether)
+ for e in elements:
+ sys.stdout.write('h.%s\n' % e)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/portsh/__init__.py b/portsh/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/portsh/__init__.py
diff --git a/rinstall b/rinstall
new file mode 100755
index 0000000..906aac7
--- /dev/null
+++ b/rinstall
@@ -0,0 +1,90 @@
+#!/bin/sh
+#
+# Upgrade multiple bruno devices at once. Returns 0 if they were all
+# successful.
+#
+mydir=$(dirname "$0")
+imgfile=$1
+imgbase=$(basename "$imgfile" .gi)
+shift
+hosts="$*"
+
+log()
+{
+ printf '\x1b[1m%s\x1b[m\n' "$*" >&2
+}
+
+
+die()
+{
+ log "FAILED"
+ exit 1
+}
+
+
+run()
+{
+ "$mydir/run" $hosts -- "$@" || die
+}
+
+
+if [ ! -e "$imgfile" ] || [ -z "$hosts" ]; then
+ log "Usage: $0 <filename.gi> [hostnames...]"
+ exit 1
+fi
+
+log "Checking running versions..."
+versions=$(run ssh root@\$ip cat /etc/version) || die
+echo "$versions"
+newhosts=$(
+ echo "$versions" |
+ while IFS=': ' read name version; do
+ if [ "$version" != "$imgbase" ]; then
+ echo "$name"
+ else
+ log "'$name' is up to date; skipping."
+ fi
+ done
+)
+if [ -z "$newhosts" ]; then
+ log "No upgrades needed."
+ exit 0
+fi
+log "Will upgrade:" $newhosts
+hosts=$newhosts
+
+log "Copying image files..."
+imgfile=$imgfile run 'scp "$imgfile" root@$ip:/tmp/img.gi && echo ok'
+
+log "Installing..."
+run 'ssh root@$ip ginstall -t /tmp/img.gi -p other && echo ok'
+
+log "Rebooting..."
+run ssh root@\$ip '(sleep 2; reboot) &'
+sleep 5
+
+log "Waiting for hosts to come back up..."
+run '
+ for i in $(seq 5 5 90); do
+ if ping -c1 -w5 $ip >/dev/null; then
+ echo "ok!"
+ sleep 5
+ exit 0
+ fi
+ done
+ exit 1
+'
+
+log "Checking running versions (expect: $imgbase)..."
+versions=$(run ssh root@\$ip cat /etc/version) || die
+echo "$versions"
+echo "$versions" | (
+ rv=0
+ while IFS=': ' read name version; do
+ if [ "$version" != "$imgbase" ]; then
+ log "$name: failed to upgrade!"
+ rv=1
+ fi
+ done
+ exit $rv
+) || die
diff --git a/run b/run
new file mode 120000
index 0000000..e56fbdd
--- /dev/null
+++ b/run
@@ -0,0 +1 @@
+run.py
\ No newline at end of file
diff --git a/run.py b/run.py
new file mode 100755
index 0000000..1130dc7
--- /dev/null
+++ b/run.py
@@ -0,0 +1,130 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+"""Run the given shell command for each of the given hosts."""
+
+import os
+import re
+import select
+import subprocess
+import sys
+import config
+from portsh import options
+
+__author__ = 'Avery Pennarun (apenwarr@google.com)'
+
+
+optspec = """
+run [options...] <hosts> -- <command string...>
+--
+r,raw Raw output (don't redirect stdin/stdout/stderr)
+q,quiet Quiet: don't print to stderr for nonzero exit codes
+"""
+
+
+def _FixNull(s):
+ if s is None:
+ return ''
+ return str(s)
+
+
+def _ArgSub(argv, host):
+ for arg in argv:
+ argout = []
+ for part in re.split(r'(\$\w+)', arg):
+ if part.startswith('$'):
+ for var in config.Host.__slots__:
+ if part[1:] == var:
+ part = _FixNull(getattr(host, var))
+ break
+ argout.append(part)
+ yield ''.join(argout)
+
+
+class Handler(object):
+ """When data is received from the given file, print it line by line."""
+
+ def __init__(self, prefix, fileobj):
+ self.prefix = prefix
+ self.fileobj = fileobj
+ self.buf = ''
+
+ def __del__(self):
+ if self.buf and not self.buf.endswith('\n'):
+ self.buf += '\n'
+ self.PrintBuf()
+
+ def fileno(self): #gpylint: disable-msg=C6409
+ return self.fileobj.fileno()
+
+ def PrintBuf(self):
+ while '\n' in self.buf:
+ line, self.buf = self.buf.split('\n', 1)
+ print '%s: %s' % (self.prefix, line)
+
+ def Run(self):
+ """Read from this handler and write its results to stdout."""
+ got = os.read(self.fileobj.fileno(), 65536)
+ self.buf += got
+ self.PrintBuf()
+ return got
+
+
+def main():
+ o = options.Options(optspec)
+ args = []
+ cmd = []
+ for i in range(1, len(sys.argv)):
+ if sys.argv[i] == '--':
+ args = sys.argv[1:i]
+ cmd = sys.argv[i+1:]
+ break
+ opt, _, hostnames = o.parse(args)
+ if not hostnames or not cmd:
+ o.fatal('you must specify hosts, --, and a command string')
+
+ try:
+ hosts = [config.hosts.FindOne(name=i) for i in hostnames]
+ except KeyError, e:
+ o.fatal('host not configured: %s' % e)
+
+ procs = []
+ handlers = []
+ for host in hosts:
+ env = dict(os.environ)
+ env.update(dict((key, _FixNull(getattr(host, key)))
+ for key in config.Host.__slots__))
+ if len(cmd) > 1:
+ argv = list(_ArgSub(cmd, host))
+ shell = False
+ else:
+ argv = cmd[0]
+ shell = True
+ if opt.raw:
+ p = subprocess.Popen(argv, env=env, shell=shell)
+ else:
+ p = subprocess.Popen(argv, env=env, shell=shell,
+ stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ handlers.append(Handler(host.name, p.stdout))
+ handlers.append(Handler(host.name, p.stderr))
+ procs.append((host, p))
+
+ while handlers:
+ r, _, _ = select.select(handlers, [], [])
+ for handler in r:
+ if not handler.Run():
+ handlers.remove(handler)
+
+ final_rv = 0
+ for host, p in procs:
+ rv = p.wait()
+ if rv != 0:
+ if not opt.quiet:
+ sys.stderr.write('-- %s: exited with error code %d\n'
+ % (host.name, rv))
+ final_rv = 1
+ return final_rv
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/sshto b/sshto
new file mode 100755
index 0000000..8c4f7e2
--- /dev/null
+++ b/sshto
@@ -0,0 +1,44 @@
+#!/bin/sh
+#
+# ssh (or, if no network, portsh) into the given device.
+#
+mydir=$(dirname "$0")
+cd "$mydir"
+
+name=$1
+shift
+
+log()
+{
+ echo "$@" >&2
+}
+
+
+die()
+{
+ log "$@"
+ exit 99
+}
+
+
+if [ -z "$name" ]; then
+ die "Usage: $0 <configname> [command...]"
+fi
+
+cmd=$(args="$*" ./run --quiet --raw "$name" -- '
+ if [ -n "$ip6" ] && ping6 -n -c1 -w2 $ip6 >/dev/null 2>&1; then
+ echo "ssh root@$ip6"
+ elif [ -n "$ip" ] && ping -n -c1 -w2 $ip >/dev/null 2>&1; then
+ echo "ssh root@$ip"
+ elif [ -n "$serialport" ]; then
+ if [ -n "$args" ]; then
+ echo "./portsh/portsh -pgoogle $serialport"
+ else
+ echo "./portsh/port $serialport"
+ fi
+ else
+ exit 1
+ fi
+') || die "Fatal: host '$name' has no live IP or serial port"
+log ">>> $cmd --" "$@"
+exec $cmd -- "$@"