mac-address: Add a script to generate MAC addresses and store them

This allows us to use sane MAC addresses for wifi and bluetooth persistently.

Change-Id: Id6a23b32167c83af2d340195ff712216369e2f18
diff --git a/debian/changelog b/debian/changelog
index 7b8b957..e6188ef 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,4 +1,10 @@
-aiy-board-tweaks (0.1) UNRELEASED; urgency=medium
+aiy-board-tweaks (0.2) UNRELEASED; urgency=medium
+
+  * Add in a MAC address generation and storage script.
+
+ -- AIY Projects <support-aiyprojects@google.com>  Fri, 04 Jan 2019 13:38:00 -0800
+
+aiy-board-tweaks (0.1) animal; urgency=medium
 
   * Initial release.
 
diff --git a/debian/control b/debian/control
index 6df8bb9..8ddbfb4 100644
--- a/debian/control
+++ b/debian/control
@@ -9,13 +9,13 @@
 Priority: optional
 Architecture: all
 Depends: e2fsprogs(>=1.43), adduser, sudo, openssh-server, locales, avahi-daemon,
-  bluez, network-manager, passwd, ${misc:Depends}
+  bluez, network-manager, passwd, python2.7, ${misc:Depends}
 Description: Performs initial system setup work
  This package contains the initial "run once" systemd service that performs
  initial startup work such as resizing the root filesystem to match the emmc
  size, adding in known users, enabling services that users will likely want by
  default, and other housekeeping behaviors (such as forcing a regeneration of
  ssh host keys).
- . 
+ .
  This package is mostly empty, save for the runonce systemd service, so
  removing it should have little effect on a running system.
diff --git a/debian/postinst b/debian/postinst
index 67a90fb..4914e9f 100755
--- a/debian/postinst
+++ b/debian/postinst
@@ -17,18 +17,22 @@
 echo LANG=en_US.UTF-8 >/etc/locale.conf
 
 # Add the aiy user and give them all the access they need.
-adduser aiy --home /home/aiy --shell /bin/bash --disabled-password --gecos "" || true
-mkdir -p /home/aiy
-chown aiy:aiy /home/aiy
-echo 'aiy:aiy' |chpasswd
+if ! grep -qE '^aiy:' /etc/passwd; then
+    adduser aiy --home /home/aiy --shell /bin/bash --disabled-password --gecos ""
+    mkdir -p /home/aiy
+    chown aiy:aiy /home/aiy
+    echo 'aiy:aiy' |chpasswd
+fi
 
 # Create group apex to give aiy user r/w privileges to apex devices
-groupadd apex || true
+if ! grep -qE '^apex:' /etc/group; then
+    groupadd apex
+fi
 
 GROUPS="adm audio bluetooth games i2c input plugdev staff sudo users video netdev systemd-journal apex"
 
-for group in $GROUPS; do \
-	adduser aiy $group; \
+for group in $GROUPS; do
+	adduser aiy $group
 done
 
 if ! grep -q aiy /etc/sudoers; then
diff --git a/debian/runonce.service b/debian/runonce.service
index 399e0dd..0187236 100644
--- a/debian/runonce.service
+++ b/debian/runonce.service
@@ -1,6 +1,6 @@
 [Unit]
 Description=Scripts that should be run only once
-Before=basic.target
+Before=basic.target network-pre.target
 After=sysinit.target local-fs.target
 DefaultDependencies=no
 
diff --git a/etc/runonce.d/10-set-mac-addresses b/etc/runonce.d/10-set-mac-addresses
new file mode 100755
index 0000000..a5dbbe8
--- /dev/null
+++ b/etc/runonce.d/10-set-mac-addresses
@@ -0,0 +1,155 @@
+#!/usr/bin/python2.7
+
+'''This script generates the wifi and bluetooth MAC addresses assigned to a
+board, given the MAC address of it's primary ethernet interface. It's primary
+purpose is to store the MAC addresses in files on the root filesystem after
+system first start because we cannot burn these addresses into the peripherals,
+other than the ethernet PHY.
+
+This script only works in the case where the MAC addresses have been allocated
+in a monotonically adjacent style for all devices, and that wifi/bluetooth
+follows the ethernet MAC in series.
+'''
+
+
+import struct
+import sys
+
+
+MAC_VENDOR_PREFIX_STR = '70:20:84'
+
+BD_ADDR_PATH = '/etc/bluetooth/.bt_nv.bin'
+WIFI_ADDR_PATH = '/lib/firmware/wlan/wlan_mac.bin'
+
+BD_NVITEM = 0x02
+BD_RDWR_PROT = 0x00
+BD_NVITEM_SIZE = 0x06
+BD_ADDR_HEADER_STRUCT = r'BBB'
+BD_ADDR_MAC_STRUCT = r'BBBBBB'
+
+WIFI_CONFIG_TEMPLATE = '''\
+Intf0MacAddress={0}
+Intf1MacAddress={0}
+Intf2MacAddress={0}
+Intf3MacAddress={0}
+END
+'''
+
+ETHERNET_DEVICE = 'eth0'
+
+
+def GenerateNextMac(mac_address_str):
+    '''Given a MAC address string, generate the next MAC address in sequence.
+
+    Note: this strips off the vendor prefix and increments as though the suffix
+    was an integer. If the device suffix rolls over, there is no error thrown or
+    any other checking done.
+
+    Parameters:
+      mac_address_str: string. Colon-separated six-octet hexidecimal MAC address.
+
+    Returns:
+      string. The next MAC address in the sequence.
+    '''
+    device_suffix = GetMacDeviceSuffixString(mac_address_str)
+    suffix_number = MacDeviceSuffixToNumber(device_suffix)
+    suffix_string = MacDeviceSuffixNumberToString(suffix_number + 1)
+    next_mac = MAC_VENDOR_PREFIX_STR + ':' + suffix_string
+    return next_mac
+
+
+def MacDeviceSuffixNumberToString(device_suffix_number):
+    '''Given a device suffix number, generate a colon-separated hexidecimal
+    representation.
+
+    Parameters:
+      device_suffix_number: integer. The number to convert into a MAC suffix.
+
+    Returns:
+      string. The colon-separated hexidecimal version of the number passed in.
+    '''
+    suffix_string = '%06x' % device_suffix_number
+    suffix_array = []
+
+    for idx in range(0, len(suffix_string), 2):
+        suffix_array.append(suffix_string[idx] + suffix_string[idx + 1])
+
+    return ':'.join(suffix_array)
+
+
+def MacDeviceSuffixToNumber(mac_suffix):
+    '''Converts a given a three-octet colon separated hexidecimal MAC device suffix
+    into an integer.'''
+    mac_array = mac_suffix.split(':')
+    flatmac = ''.join(mac_array)
+    return int(flatmac, 16)
+
+
+def GetMacDeviceSuffixString(mac_address):
+    '''Strip the vendor prefix from a given a full six-octet colon separated
+    hexidecimal MAC.'''
+    mac_array = mac_address.split(':')
+    return ':'.join(mac_array[3:])
+
+
+def FindEthernetMacString(device):
+    '''Returns the MAC address string for a given network device from sysfs.
+
+    If an error occurs attempting to read from sysfs, or runs into an EOF
+    prematurely, returns None.
+    '''
+    sysfs_path = '/sys/class/net/%s/address' % (device)
+
+    try:
+        with open(sysfs_path, 'r') as fp:
+            address = fp.readline()
+        if len(address) == 0:
+            return None
+        address = address[:-1]
+        return address
+    except Exception as e:
+        print('Error reading %s: %s' % (sysfs_path, e))
+        return None
+
+
+def WriteWifiMacAddress(next_mac):
+    '''Writes the given MAC address string to the wifi configuration files.'''
+    try:
+        with open(WIFI_ADDR_PATH, 'w') as fp:
+            fp.write(WIFI_CONFIG_TEMPLATE.format(next_mac))
+        return True
+    except Exception as e:
+        print('Error writing wifi configuration to %s: %s' % (WIFI_ADDR_PATH, e))
+        return False
+
+
+def WriteBluetoothMacAddress(next_mac):
+    '''Writes the given MAC address string to a binary file readable by Bluez.'''
+    mac_bytes = [int(x, 16) for x in next_mac.split(':')]
+
+    try:
+        with open(BD_ADDR_PATH, 'w') as fp:
+            fp.write(struct.pack(BD_ADDR_HEADER_STRUCT, BD_NVITEM, BD_RDWR_PROT,
+                                 BD_NVITEM_SIZE))
+            fp.write(struct.pack(BD_ADDR_MAC_STRUCT, *mac_bytes))
+        return True
+    except Exception as e:
+        print('Error writing bluetooth configuration to %s: %s' % (BD_ADDR_PATH, e))
+
+
+def Main():
+    base_mac_address = FindEthernetMacString(ETHERNET_DEVICE)
+
+    if base_mac_address == None:
+        sys.stderr.write('Unable to find MAC address for device %s\n' % (ETHERNET_DEVICE))
+        sys.exit(1)
+
+    next_mac = GenerateNextMac(base_mac_address)
+    print('Wifi/Bt MAC address will be %s' % next_mac)
+
+    WriteWifiMacAddress(next_mac)
+    WriteBluetoothMacAddress(next_mac)
+
+
+if __name__ == '__main__':
+    Main()