Merge "Some tools for auto-probing attached bruno devices."
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..d56c8f3 100644
--- a/Makefile
+++ b/Makefile
@@ -1,32 +1,92 @@
-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
+
+lint:
+	gpylint *.py configs/core.py
 
 test: install-wvtest
 	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:
+	for d in $(addsuffix .run,$(wildcard [0-9]*.sh [0-9]*.py)); do \
+	  $(MAKE) $$d; \
+	done
 
-install-wvtest:
+install-wvtest: map.stamp
 	tar -cf - *.py *.sh wvtest/ | \
-	$(PORTSH) \
+	$(PORTSH) $(PORT) \
 		'mkdir -p /tmp/tests && cd /tmp/tests && tar -xf -'
 
 %.sh.run: %.sh install-wvtest
 	echo "Testing $<"
-	$(PORTSH) \
+	$(PORTSH) $(PORT) \
 		'cd /tmp/tests && sh ./$<' </dev/null
 
 %.py.run: %.py install-wvtest
 	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; \
+  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/config.py b/config.py
new file mode 100644
index 0000000..701d05c
--- /dev/null
+++ b/config.py
@@ -0,0 +1,16 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+"""Test configurations.  Includes a list of auto-probed bruno devices."""
+
+from configs import hosts
+
+__author__ = 'Avery Pennarun (apenwarr@google.com)'
+
+
+def main():
+  print hosts
+
+
+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..2dc6b72
--- /dev/null
+++ b/configs/core.py
@@ -0,0 +1,104 @@
+#!/usr/bin/python
+# Copyright 2012 Google Inc. All Rights Reserved.
+#
+"""Base classes for configuration objects."""
+
+__author__ = 'Avery Pennarun (apenwarr@google.com)'
+
+
+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 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',
+  )
+
+
+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 FindOrAdd(self, **kwargs):
+    h = self.FindOne(**kwargs)
+    if not h:
+      h = Host(**kwargs)
+      self.append(h)
+    return h
+
+  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/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..512fdb3
--- /dev/null
+++ b/parse-probe-data
@@ -0,0 +1,74 @@
+#!/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')
+
+  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()