Merge "taxonomy: restore pcap test cases."
diff --git a/Makefile b/Makefile
index 7c3f167..2f0e4a6 100644
--- a/Makefile
+++ b/Makefile
@@ -70,6 +70,10 @@
DIRS+=diags
endif
+ifeq ($(BUILD_CONMAN),y)
+DIRS+=conman
+endif
+
PREFIX=/usr
BINDIR=$(DESTDIR)$(PREFIX)/bin
LIBDIR=$(DESTDIR)$(PREFIX)/lib
diff --git a/conman/Makefile b/conman/Makefile
new file mode 100644
index 0000000..8ff1719
--- /dev/null
+++ b/conman/Makefile
@@ -0,0 +1,47 @@
+INSTALL?=install
+BINDIR=$(DESTDIR)/bin
+LIBDIR=$(DESTDIR)/usr/conman
+PYTHON?=python
+GPYLINT=$(shell \
+ if which gpylint >/dev/null; then \
+ echo gpylint; \
+ else \
+ echo 'echo "(gpylint-missing)" >&2'; \
+ fi \
+)
+
+all:
+
+%.test: %_test.py
+ echo ./$<
+ PYTHONPATH=..:./test/fake_wpactrl:$(PYTHONPATH) ./$<
+
+runtests: \
+ $(patsubst %_test.py,%.test,$(wildcard *_test.py))
+
+lint: $(filter-out options.py,$(wildcard *.py))
+ $(GPYLINT) $^
+
+test_only: all
+ ./wvtest/wvtestrun $(MAKE) runtests
+
+# Use a submake here, only because otherwise GNU make (3.81) will not print
+# an error about 'test' itself failing if one of the two sub-targets fails.
+# Without such output, 'lint' could fail long before test_only fails, and
+# the test_only output could scroll off the top of the screen, leaving the
+# misleading impression that everything tested successfully.
+test: all
+ $(MAKE) test_only lint
+
+install:
+ mkdir -p $(LIBDIR) $(BINDIR)
+ $(INSTALL) -m 0644 $(filter-out %_test.py, $(wildcard *.py)) $(LIBDIR)/
+ $(INSTALL) -m 0755 main.py $(LIBDIR)/
+ rm -f $(BINDIR)/conman
+ ln -s /usr/conman/main.py $(BINDIR)/conman
+
+install-libs:
+ @echo "No libs to install."
+
+clean:
+ rm -rf *~ .*~ *.pyc *.tmp */*~
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
new file mode 100755
index 0000000..9bbfb78
--- /dev/null
+++ b/conman/connection_manager.py
@@ -0,0 +1,608 @@
+#!/usr/bin/python
+
+"""Manages a device's wired and wireless connections to the LAN."""
+
+import collections
+import errno
+import glob
+import json
+import logging
+import os
+import re
+import subprocess
+import time
+
+import pyinotify
+
+import cycler
+import interface
+import iw
+
+GFIBER_OUIS = ['f4:f5:e8']
+VENDOR_IE_FEATURE_ID_AUTOPROVISIONING = '01'
+
+
+class FileChangeHandler(pyinotify.ProcessEvent):
+ """Connects pyinotify events to ConnectionManager."""
+
+ def __init__(self, connection_manager, *args, **kwargs):
+ self._connection_manager = connection_manager
+ super(FileChangeHandler, self).__init__(*args, **kwargs)
+
+ def process_IN_DELETE(self, event):
+ self._connection_manager.handle_event(event.path, event.name, True)
+
+ def process_IN_CLOSE_WRITE(self, event):
+ self._connection_manager.handle_event(event.path, event.name, False)
+
+ def process_IN_MOVED_TO(self, event):
+ self.process_IN_CLOSE_WRITE(event)
+
+ def process_IN_MOVED_FROM(self, event):
+ self.process_IN_DELETE(event)
+
+ def check_events(self, *args, **kwargs):
+ result = super(FileChangeHandler, self).check_events(*args, **kwargs)
+ return result
+
+
+class WLANConfiguration(object):
+ """Represents a WLAN configuration from cwmpd."""
+
+ WIFI_STOPAP = ['wifi', 'stopap']
+ WIFI_SETCLIENT = ['wifi', 'setclient']
+ WIFI_STOPCLIENT = ['wifi', 'stopclient']
+
+ def __init__(self, band, wifi, command_lines):
+ self.band = band
+ self.wifi = wifi
+ self.command = command_lines.splitlines()
+ self.access_point_up = False
+ self.client_up = False
+ self.ssid = None
+ self.passphrase = None
+ self.interface_suffix = None
+ self.access_point = None
+
+ binwifi_option_attrs = {
+ '-s': 'ssid',
+ '--ssid': 'ssid',
+ '-S': 'interface_suffix',
+ '--interface_suffix': 'interface_suffix',
+ }
+ attr = None
+ for line in self.command:
+ if attr is not None:
+ setattr(self, attr, line)
+ attr = None
+ continue
+
+ attr = binwifi_option_attrs.get(line, None)
+
+ if line.startswith('WIFI_PSK='):
+ self.passphrase = line.split('WIFI_PSK=')[-1]
+
+ if self.ssid is None:
+ raise ValueError('Command file does not specify SSID')
+
+ def start_access_point(self):
+ """Start an access point."""
+
+ if not self.access_point or self.access_point_up:
+ return
+
+ # Since we only run an access point if we have a wired connection anyway, we
+ # don't want to constrain the AP's channel to the channel of the AP we are
+ # connected to. This line should go away when we have wireless repeaters.
+ self.stop_client()
+
+ try:
+ subprocess.check_call(self.command, stderr=subprocess.STDOUT)
+ self.access_point_up = True
+ except subprocess.CalledProcessError as e:
+ logging.error('Failed to start access point: %s', e.output)
+
+ def stop_access_point(self):
+ if not self.access_point_up:
+ return
+
+ command = self.WIFI_STOPAP + ['--band', self.band]
+ if self.interface_suffix:
+ command += ['--interface_suffix', self.interface_suffix]
+
+ try:
+ subprocess.check_call(command, stderr=subprocess.STDOUT)
+ self.access_point_up = False
+ except subprocess.CalledProcessError as e:
+ logging.error('Failed to stop access point: %s', e.output)
+ return
+
+ def start_client(self):
+ """Join the WLAN as a client."""
+ if self.client_up:
+ return
+
+ self.wifi.detach_wpa_control()
+
+ command = self.WIFI_SETCLIENT + ['--ssid', self.ssid, '--band', self.band]
+ env = dict(os.environ)
+ if self.passphrase:
+ env['WIFI_CLIENT_PSK'] = self.passphrase
+ try:
+ subprocess.check_call(command, stderr=subprocess.STDOUT, env=env)
+ self.client_up = True
+ logging.info('Successfully joined WLAN')
+ except subprocess.CalledProcessError as e:
+ logging.error('Failed to start wifi client: %s', e.output)
+
+ def stop_client(self):
+ if not self.client_up:
+ return
+
+ self.wifi.detach_wpa_control()
+
+ try:
+ subprocess.check_call(self.WIFI_STOPCLIENT + ['-b', self.band],
+ stderr=subprocess.STDOUT)
+ self.client_up = False
+ except subprocess.CalledProcessError as e:
+ logging.error('Failed to stop wifi client: %s', e.output)
+
+
+class ConnectionManager(object):
+ """Manages wired and wireless connections to the LAN."""
+
+ # pylint: disable=invalid-name
+ Bridge = interface.Bridge
+ Wifi = interface.Wifi
+ WLANConfiguration = WLANConfiguration
+
+ ETHERNET_STATUS_FILE = 'eth0'
+ WLAN_FILE_REGEXP_FMT = r'^%s((?P<interface_suffix>.*)\.)?(?P<band>2\.4|5)$'
+ COMMAND_FILE_PREFIX = 'command.'
+ ACCESS_POINT_FILE_PREFIX = 'access_point.'
+ COMMAND_FILE_REGEXP = WLAN_FILE_REGEXP_FMT % COMMAND_FILE_PREFIX
+ ACCESS_POINT_FILE_REGEXP = WLAN_FILE_REGEXP_FMT % ACCESS_POINT_FILE_PREFIX
+ GATEWAY_FILE_PREFIX = 'gateway.'
+ MOCA_NODE_FILE_PREFIX = 'node'
+ WIFI_SETCLIENT = ['wifi', 'setclient']
+ IFUP = ['ifup']
+ IP_LINK = ['ip', 'link']
+ IFPLUGD_ACTION = ['/etc/ifplugd/ifplugd.action']
+
+ def __init__(self,
+ bridge_interface='br0',
+ status_dir='/tmp/conman',
+ moca_status_dir='/tmp/cwmp/monitoring/moca2',
+ wpa_control_interface='/var/run/wpa_supplicant',
+ run_duration_s=1, interface_update_period=5,
+ wifi_scan_period_s=120, wlan_retry_s=15, acs_update_wait_s=10):
+
+ self.bridge = self.Bridge(bridge_interface, '10')
+
+ # If we have multiple wcli interfaces, 5 GHz-only < both < 2.4 GHz-only.
+ def metric_for_bands(bands):
+ if '5' in bands:
+ if '2.4' in bands:
+ return interface.METRIC_24GHZ_5GHZ
+ return interface.METRIC_5GHZ
+ return interface.METRIC_24GHZ
+
+ self.wifi = sorted([self.Wifi(interface_name, metric_for_bands(bands),
+ # Prioritize 5 GHz over 2.4.
+ bands=sorted(bands, reverse=True))
+ for interface_name, bands
+ in get_client_interfaces().iteritems()],
+ key=lambda w: w.metric)
+
+ self._status_dir = status_dir
+ self._interface_status_dir = os.path.join(status_dir, 'interfaces')
+ self._moca_status_dir = moca_status_dir
+ self._wpa_control_interface = wpa_control_interface
+ self._run_duration_s = run_duration_s
+ self._interface_update_period = interface_update_period
+ self._wifi_scan_period_s = wifi_scan_period_s
+ for wifi in self.wifi:
+ wifi.last_wifi_scan_time = -self._wifi_scan_period_s
+ self._wlan_retry_s = wlan_retry_s
+ self._acs_update_wait_s = acs_update_wait_s
+
+ self._wlan_configuration = {}
+
+ # It is very important that we know whether ethernet is up. So if the
+ # ethernet file doesn't exist for any reason when conman starts, check
+ # explicitly.
+ if os.path.exists(os.path.join(self._interface_status_dir,
+ self.ETHERNET_STATUS_FILE)):
+ self._ethernet_file_exists = True
+ self._process_file(self._interface_status_dir, self.ETHERNET_STATUS_FILE)
+ else:
+ self._ethernet_file_exists = False
+ self.bridge.ethernet = self.is_ethernet_up()
+
+ for path, prefix in ((self._moca_status_dir, self.MOCA_NODE_FILE_PREFIX),
+ (self._status_dir, self.COMMAND_FILE_PREFIX),
+ (self._status_dir, self.GATEWAY_FILE_PREFIX)):
+ for filepath in glob.glob(os.path.join(path, prefix + '*')):
+ self._process_file(path, os.path.split(filepath)[-1])
+
+ wm = pyinotify.WatchManager()
+ wm.add_watch(self._status_dir,
+ pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO |
+ pyinotify.IN_DELETE | pyinotify.IN_MOVED_FROM)
+ wm.add_watch(self._interface_status_dir,
+ pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO)
+ wm.add_watch(self._moca_status_dir,
+ pyinotify.IN_CLOSE_WRITE | pyinotify.IN_MOVED_TO)
+ self.notifier = pyinotify.Notifier(wm, FileChangeHandler(self), timeout=0)
+
+ self._interface_update_counter = 0
+ self._try_wlan_after = {'5': 0, '2.4': 0}
+
+ def is_ethernet_up(self):
+ """Explicitly check whether ethernet is up.
+
+ Only used on startup, and only if ifplugd file is missing.
+
+ Returns:
+ Whether the ethernet link is up.
+ """
+ try:
+ lines = subprocess.check_output(self.IP_LINK).splitlines()
+ except subprocess.CalledProcessError as e:
+ raise EnvironmentError('Failed to call "ip link": %r', e.message)
+
+ for line in lines:
+ if 'eth0' in line and 'LOWER_UP' in line:
+ return True
+
+ return False
+
+ def run(self):
+ """Run the main loop, and clean up when done."""
+ try:
+ while True:
+ self.run_once()
+ finally:
+ for wifi in self.wifi:
+ wifi.detach_wpa_control()
+ self.notifier.stop()
+
+ def run_once(self):
+ """Run one iteration of the main loop.
+
+ This includes the following:
+
+ 1. Process any changes in watched files.
+ 2. Check interfaces for changed connectivity, if
+ update_interfaces_and_routes is true.
+ 3. Start, stop, or restart access points as appropriate. If running an
+ access point, skip all remaining wifi steps for that band.
+ 3. Handle any wpa_supplicant events.
+ 4. Periodically, perform a wifi scan.
+ 5. If not connected to the WLAN or to the ACS, try to connect to something.
+ 6. If connected to the ACS but not the WLAN, and enough time has passed
+ since connecting that we should expect a current WLAN configuration, try
+ to join the WLAN again.
+ 7. Sleep for the rest of the duration of _run_duration_s.
+ """
+ start_time = time.time()
+ self.notifier.process_events()
+ while self.notifier.check_events():
+ self.notifier.read_events()
+ self.notifier.process_events()
+
+ self._interface_update_counter += 1
+ if self._interface_update_counter == self._interface_update_period:
+ self._interface_update_counter = 0
+ self._update_interfaces_and_routes()
+
+ for wifi in self.wifi:
+ continue_wifi = False
+ if not wifi.attached():
+ logging.debug('Attempting to attach to wpa control interface for %s',
+ wifi.name)
+ wifi.attach_wpa_control(self._wpa_control_interface)
+ wifi.handle_wpa_events()
+
+ # Only one wlan_configuration per interface will have access_point ==
+ # True. Try 5 GHz first, then 2.4 GHz. If both bands are supported by
+ # the same radio, and the enabled band is changed, then hostapd will be
+ # updated by the wifi command, but we should tell any other
+ # wlan_configurations that their APs have been disabled.
+ for band in wifi.bands:
+ wlan_configuration = self._wlan_configuration.get(band, None)
+ if continue_wifi:
+ if wlan_configuration:
+ wlan_configuration.access_point_up = False
+ continue
+
+ # Make sure access point is running iff it should be.
+ if wlan_configuration:
+ self._update_access_point(wlan_configuration)
+ # If we are running an AP on this interface, we don't want to make any
+ # client connections on it.
+ if wlan_configuration.access_point_up:
+ continue_wifi = True
+
+ if continue_wifi:
+ logging.debug('Running AP on %s, nothing else to do.', wifi.name)
+ continue
+
+ # If this interface is connected to the user's WLAN, there is nothing else
+ # to do.
+ if self._connected_to_wlan(wifi):
+ logging.debug('Connected to WLAN on %s, nothing else to do.', wifi.name)
+ return
+
+ # This interface is not connected to the WLAN, so scan for potential
+ # routes to the ACS for provisioning.
+ if (not self.acs() and
+ time.time() > wifi.last_wifi_scan_time + self._wifi_scan_period_s):
+ logging.debug('Performing scan on %s.', wifi.name)
+ self._wifi_scan(wifi)
+
+ # Periodically retry rejoining the WLAN. If the WLAN configuration is
+ # updated then _update_wlan_configuration will do this immediately, but
+ # maybe the credentials didn't change and we were just waiting for the AP
+ # to restart or something. Try both bands, so we can fall back to 2.4 in
+ # case 5 is unavailable for some reason.
+ for band in wifi.bands:
+ wlan_configuration = self._wlan_configuration.get(band, None)
+ if wlan_configuration and time.time() > self._try_wlan_after[band]:
+ logging.debug('Trying to join WLAN on %s.', wifi.name)
+ wlan_configuration.start_client()
+ if self._connected_to_wlan(wifi):
+ logging.debug('Joined WLAN on %s.', wifi.name)
+ self._try_wlan_after[band] = 0
+ break
+ else:
+ logging.error('Failed to connect to WLAN on %s.', wifi.name)
+ self._try_wlan_after[band] = time.time() + self._wlan_retry_s
+ else:
+ # If we are aren't on the WLAN, can ping the ACS, and haven't gotten a
+ # new WLAN configuration yet, there are two possibilities:
+ #
+ # 1) The configuration didn't change, and we should retry connecting.
+ # 2) cwmpd isn't writing a configuration, possibly because the device
+ # isn't registered to any accounts.
+ logging.debug('Unable to join WLAN on %s', wifi.name)
+ if self.acs():
+ logging.debug('Connected to ACS on %s', wifi.name)
+ now = time.time()
+ if (self._wlan_configuration and
+ hasattr(wifi, 'waiting_for_acs_since')):
+ if now - wifi.waiting_for_acs_since > self._acs_update_wait_s:
+ logging.info('ACS has not updated WLAN configuration; will retry '
+ ' with old config.')
+ for w in self.wifi:
+ for b in w.bands:
+ self._try_wlan_after[b] = now
+ continue
+ # We don't want to want to log this spammily, so do exponential
+ # backoff.
+ elif (hasattr(wifi, 'complain_about_acs_at')
+ and now >= wifi.complain_about_acs_at):
+ wait = wifi.complain_about_acs_at - wifi.waiting_for_acs_since
+ logging.info('Can ping ACS, but no WLAN configuration for %ds.',
+ wait)
+ wifi.complain_about_acs_at += wait
+ # If we didn't manage to join the WLAN and we don't have an ACS
+ # connection, we should try to establish one.
+ else:
+ logging.debug('Not connected to ACS on %s', wifi.name)
+ self._try_next_bssid(wifi)
+
+ time.sleep(max(0, self._run_duration_s - (time.time() - start_time)))
+
+ def acs(self):
+ return self.bridge.acs() or any(wifi.acs() for wifi in self.wifi)
+
+ def internet(self):
+ return self.bridge.internet() or any(wifi.internet() for wifi in self.wifi)
+
+ def _update_interfaces_and_routes(self):
+ # If ifplugd has never written an ethernet status file, poll manually before
+ # updating routes.
+ if not self._ethernet_file_exists:
+ self.bridge.ethernet = self.is_ethernet_up()
+
+ self.bridge.update_routes()
+ for wifi in self.wifi:
+ wifi.update_routes()
+
+ def handle_event(self, path, filename, deleted):
+ if deleted:
+ self._handle_deleted_file(path, filename)
+ else:
+ self._process_file(path, filename)
+
+ def _handle_deleted_file(self, path, filename):
+ if path == self._status_dir:
+ match = re.match(self.COMMAND_FILE_REGEXP, filename)
+ if match:
+ band = match.group('band')
+ if band in self._wlan_configuration:
+ self._remove_wlan_configuration(band)
+ return
+
+ match = re.match(self.ACCESS_POINT_FILE_REGEXP, filename)
+ if match:
+ band = match.group('band')
+ if band in self._wlan_configuration:
+ self._wlan_configuration[band].access_point = False
+ return
+
+ def _remove_wlan_configuration(self, band):
+ config = self._wlan_configuration[band]
+ config.stop_client()
+ config.stop_access_point()
+ del self._wlan_configuration[band]
+
+ def _process_file(self, path, filename):
+ """Process or ignore an updated file in a watched directory."""
+ filepath = os.path.join(path, filename)
+ try:
+ contents = open(filepath).read().strip()
+ except IOError as e:
+ if e.errno == errno.ENOENT:
+ # Logging about failing to open .tmp files results in spammy logs.
+ if not filename.endswith('.tmp'):
+ logging.error('Not a file: %s', filepath)
+ return
+ else:
+ raise
+
+ if path == self._interface_status_dir:
+ if filename == self.ETHERNET_STATUS_FILE:
+ try:
+ self.bridge.ethernet = bool(int(contents))
+ self._ethernet_file_exists = True
+ except ValueError:
+ logging.error('Status file contents should be 0 or 1, not %s',
+ contents)
+ return
+
+ if path == self._status_dir:
+ if filename.startswith(self.COMMAND_FILE_PREFIX):
+ match = re.match(self.COMMAND_FILE_REGEXP, filename)
+ if match:
+ band = match.group('band')
+ wifi = next(w for w in self.wifi if band in w.bands)
+ self._update_wlan_configuration(
+ self.WLANConfiguration(band, wifi, contents))
+ elif filename.startswith(self.ACCESS_POINT_FILE_PREFIX):
+ match = re.match(self.ACCESS_POINT_FILE_REGEXP, filename)
+ if match:
+ band = match.group('band')
+ wifi = next(w for w in self.wifi if band in w.bands)
+ if band in self._wlan_configuration:
+ self._wlan_configuration[band].access_point = True
+ elif filename.startswith(self.GATEWAY_FILE_PREFIX):
+ interface_name = filename.split(self.GATEWAY_FILE_PREFIX)[-1]
+ ifc = self.interface_by_name(interface_name)
+ if ifc:
+ ifc.set_gateway_ip(contents)
+ logging.info('Received gateway %r for interface %s', contents,
+ ifc.name)
+
+ elif path == self._moca_status_dir:
+ match = re.match(r'^%s\d+$' % self.MOCA_NODE_FILE_PREFIX, filename)
+ if match:
+ try:
+ json_contents = json.loads(contents)
+ except ValueError:
+ logging.error('Cannot parse %s as JSON.', filepath)
+ return
+ node = json_contents['NodeId']
+ had_moca = self.bridge.moca
+ if json_contents.get('RxNBAS', 0) == 0:
+ self.bridge.remove_moca_station(node)
+ else:
+ self.bridge.add_moca_station(node)
+ has_moca = self.bridge.moca
+ if had_moca != has_moca:
+ self.tell_ifplugd_about_moca(has_moca)
+
+ def interface_by_name(self, interface_name):
+ for ifc in [self.bridge] + self.wifi:
+ if ifc.name == interface_name:
+ return ifc
+
+ def tell_ifplugd_about_moca(self, up):
+ subprocess.call(self.IFPLUGD_ACTION + ['moca0', 'up' if up else 'down'])
+
+ def _wifi_scan(self, wifi):
+ """Perform a wifi scan and update wifi.cycler."""
+ logging.info('Scanning on %s...', wifi.name)
+ wifi.last_wifi_scan_time = time.time()
+ subprocess.call(self.IFUP + [wifi.name])
+ with_ie, without_ie = self._find_bssids(wifi.name)
+ logging.info('Done scanning on %s', wifi.name)
+ items = [(bss_info, 3) for bss_info in with_ie]
+ items += [(bss_info, 1) for bss_info in without_ie]
+ wifi.cycler = cycler.AgingPriorityCycler(cycle_length_s=30, items=items)
+
+ def _find_bssids(self, wcli):
+ def supports_autoprovisioning(oui, vendor_ie):
+ if oui not in GFIBER_OUIS:
+ return False
+
+ return vendor_ie.startswith(VENDOR_IE_FEATURE_ID_AUTOPROVISIONING)
+
+ return iw.find_bssids(wcli, supports_autoprovisioning, False)
+
+ def _try_next_bssid(self, wifi):
+ """Attempt to connect to the next BSSID in wifi's BSSID cycler.
+
+ Args:
+ wifi: The wifi interface to use.
+
+ Returns:
+ Whether connecting to the network succeeded.
+ """
+ if not hasattr(wifi, 'cycler'):
+ return False
+
+ bss_info = wifi.cycler.next()
+ if bss_info is not None:
+ connected = subprocess.call(self.WIFI_SETCLIENT +
+ ['--ssid', bss_info.ssid,
+ '--band', wifi.bands[0],
+ '--bssid', bss_info.bssid]) == 0
+ if connected:
+ now = time.time()
+ wifi.waiting_for_acs_since = now
+ wifi.complain_about_acs_at = now + 5
+ logging.info('Attempting to provision via SSID %s', bss_info.ssid)
+ return connected
+
+ return False
+
+ def _connected_to_wlan(self, wifi):
+ return (wifi.wpa_supplicant and
+ any(config.client_up for band, config
+ in self._wlan_configuration.iteritems()
+ if band in wifi.bands))
+
+ def _update_wlan_configuration(self, wlan_configuration):
+ band = wlan_configuration.band
+ current = self._wlan_configuration.get(band, None)
+ if current is None or wlan_configuration.command != current.command:
+ if current is not None:
+ wlan_configuration.access_point = current.access_point
+ else:
+ ap_file = os.path.join(
+ self._status_dir, self.ACCESS_POINT_FILE_PREFIX +
+ (('%s.' % wlan_configuration.interface_suffix)
+ if wlan_configuration.interface_suffix else '') + band)
+ wlan_configuration.access_point = os.path.exists(ap_file)
+ self._wlan_configuration[band] = wlan_configuration
+ self._update_access_point(wlan_configuration)
+
+ def _update_access_point(self, wlan_configuration):
+ if self.bridge.internet() and wlan_configuration.access_point:
+ wlan_configuration.start_access_point()
+ else:
+ wlan_configuration.stop_access_point()
+ wlan_configuration.start_client()
+
+
+def _wifi_show():
+ try:
+ return subprocess.check_output(['wifi', 'show'])
+ except subprocess.CalledProcessError as e:
+ logging.error('Failed to call "wifi show": %s', e)
+
+
+def get_client_interfaces():
+ current_band = None
+ result = collections.defaultdict(set)
+ for line in _wifi_show().splitlines():
+ if line.startswith('Band:'):
+ current_band = line.split()[1]
+ elif line.startswith('Client Interface:'):
+ result[line.split()[2]].add(current_band)
+
+ return result
+
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
new file mode 100755
index 0000000..5388b22
--- /dev/null
+++ b/conman/connection_manager_test.py
@@ -0,0 +1,714 @@
+#!/usr/bin/python
+
+"""Tests for connection_manager.py."""
+
+import logging
+import os
+import shutil
+import tempfile
+
+import connection_manager
+import interface_test
+import iw
+from wvtest import wvtest
+
+logging.basicConfig(level=logging.DEBUG)
+
+FAKE_MOCA_NODE1_FILE = """{
+ "NodeId": 1,
+ "RxNBAS": 25
+}
+"""
+
+FAKE_MOCA_NODE1_FILE_DISCONNECTED = """{
+ "NodeId": 1,
+ "RxNBAS": 0
+}
+"""
+
+WIFI_SHOW_OUTPUT_ONE_RADIO = """Band: 2.4
+RegDomain: US
+Interface: wlan0 # 2.4 GHz ap
+Channel: 149
+BSSID: f4:f5:e8:81:1b:a0
+AutoChannel: True
+AutoType: NONDFS
+Station List for band: 2.4
+
+Client Interface: wcli0 # 2.4 GHz client
+Client BSSID: f4:f5:e8:81:1b:a1
+
+Band: 5
+RegDomain: US
+Interface: wlan0 # 5 GHz ap
+Channel: 149
+BSSID: f4:f5:e8:81:1b:a0
+AutoChannel: True
+AutoType: NONDFS
+Station List for band: 5
+
+Client Interface: wcli0 # 5 GHz client
+Client BSSID: f4:f5:e8:81:1b:a1
+"""
+
+WIFI_SHOW_OUTPUT_TWO_RADIOS = """Band: 2.4
+RegDomain: US
+Interface: wlan0 # 2.4 GHz ap
+Channel: 149
+BSSID: f4:f5:e8:81:1b:a0
+AutoChannel: True
+AutoType: NONDFS
+Station List for band: 2.4
+
+Client Interface: wcli0 # 2.4 GHz client
+Client BSSID: f4:f5:e8:81:1b:a1
+
+Band: 5
+RegDomain: US
+Interface: wlan1 # 5 GHz ap
+Channel: 149
+BSSID: f4:f5:e8:81:1b:a0
+AutoChannel: True
+AutoType: NONDFS
+Station List for band: 5
+
+Client Interface: wcli1 # 5 GHz client
+Client BSSID: f4:f5:e8:81:1b:a1
+"""
+
+IW_SCAN_OUTPUT = """BSS 00:11:22:33:44:55(on wcli0)
+ SSID: s1
+ Vendor specific: OUI f4:f5:e8, data: 01
+BSS 66:77:88:99:aa:bb(on wcli0)
+ SSID: s1
+ Vendor specific: OUI f4:f5:e8, data: 01
+BSS 01:23:45:67:89:ab(on wcli0)
+ SSID: s2
+"""
+
+
+@wvtest.wvtest
+def get_client_interfaces_test():
+ """Test get_client_interfaces."""
+ # pylint: disable=protected-access
+ original_wifi_show = connection_manager._wifi_show
+ connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_ONE_RADIO
+ wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
+ {'wcli0': set(['2.4', '5'])})
+ connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_TWO_RADIOS
+ wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
+ {'wcli0': set(['2.4']), 'wcli1': set(['5'])})
+ connection_manager._wifi_show = original_wifi_show
+
+
+class WLANConfiguration(connection_manager.WLANConfiguration):
+ """WLANConfiguration subclass for testing."""
+
+ WIFI_STOPAP = ['echo', 'stopap']
+ WIFI_SETCLIENT = ['echo', 'setclient']
+ WIFI_STOPCLIENT = ['echo', 'stopclient']
+
+ def start_client(self):
+ if not self.client_up:
+ self.wifi.set_connection_check_result('succeed')
+
+ if self.wifi.attached():
+ self.wifi.add_connected_event()
+ else:
+ open(self._socket(), 'w')
+
+ # Normally, wpa_supplicant would bring up wcli*, which would trigger
+ # ifplugd, which would run ifplugd.action, which would do two things:
+ #
+ # 1) Write an interface status file.
+ # 2) Call run-dhclient, which would call dhclient-script, which would
+ # write a gateway file.
+ #
+ # Fake both of these things instead.
+ self.write_interface_status_file('1')
+ self.write_gateway_file()
+
+ super(WLANConfiguration, self).start_client()
+
+ def stop_client(self):
+ if self.client_up:
+ self.wifi.add_terminating_event()
+ os.unlink(self._socket())
+ self.wifi.set_connection_check_result('fail')
+
+ # See comments in start_client.
+ self.write_interface_status_file('0')
+
+ super(WLANConfiguration, self).stop_client()
+
+ def _socket(self):
+ return os.path.join(self._wpa_control_interface, self.wifi.name)
+
+ def write_gateway_file(self):
+ gateway_file = os.path.join(self.status_dir,
+ self.gateway_file_prefix + self.wifi.name)
+ with open(gateway_file, 'w') as f:
+ # This value doesn't matter to conman, so it's fine to hard code it here.
+ f.write('192.168.1.1')
+
+ def write_interface_status_file(self, value):
+ status_file = os.path.join(self.interface_status_dir, self.wifi.name)
+ with open(status_file, 'w') as f:
+ # This value doesn't matter to conman, so it's fine to hard code it here.
+ f.write(value)
+
+
+class Wifi(interface_test.Wifi):
+
+ def __init__(self, *args, **kwargs):
+ super(Wifi, self).__init__(*args, **kwargs)
+ # Whether wpa_supplicant is connected to a network.
+ self._initially_connected = True
+ self.wifi_scan_counter = 0
+
+
+class ConnectionManager(connection_manager.ConnectionManager):
+ """ConnectionManager subclass for testing."""
+
+ # pylint: disable=invalid-name
+ Bridge = interface_test.Bridge
+ Wifi = Wifi
+ WLANConfiguration = WLANConfiguration
+
+ WIFI_SETCLIENT = ['echo', 'setclient']
+ IFUP = ['echo', 'ifup']
+ IFPLUGD_ACTION = ['echo', 'ifplugd.action']
+
+ def __init__(self, *args, **kwargs):
+ super(ConnectionManager, self).__init__(*args, **kwargs)
+ self.scan_has_results = False
+
+ def _update_access_point(self, wlan_configuration):
+ client_was_up = wlan_configuration.client_up
+ super(ConnectionManager, self)._update_access_point(wlan_configuration)
+ if wlan_configuration.access_point_up:
+ if client_was_up:
+ wifi = self.wifi_for_band(wlan_configuration.band)
+ wifi.add_terminating_event()
+
+ def _try_next_bssid(self, wifi):
+ if hasattr(wifi, 'cycler'):
+ bss_info = wifi.cycler.peek()
+ if bss_info:
+ self.last_provisioning_attempt = bss_info
+
+ super(ConnectionManager, self)._try_next_bssid(wifi)
+
+ socket = os.path.join(self._wpa_control_interface, wifi.name)
+
+ if bss_info and bss_info.ssid == 's1':
+ if wifi.attached():
+ wifi.add_connected_event()
+ else:
+ open(socket, 'w')
+ wifi.set_connection_check_result('fail')
+ self.write_interface_status_file(wifi.name, '1')
+ return True
+
+ if bss_info and bss_info.ssid == 's2':
+ if wifi.attached():
+ wifi.add_connected_event()
+ else:
+ open(socket, 'w')
+ wifi.set_connection_check_result('restricted')
+ self.write_interface_status_file(wifi.name, '1')
+ self.write_gateway_file(wifi.name)
+ return True
+
+ return False
+
+ def _wifi_stopclient(self, band):
+ super(ConnectionManager, self)._wifi_stopclient(band)
+ self.wifi_for_band(band).add_terminating_event()
+
+ # pylint: disable=unused-argument,protected-access
+ def _find_bssids(self, wcli):
+ # Only the 5 GHz scan finds anything.
+ if wcli == 'wcli0' and self.scan_has_results:
+ iw._scan = lambda interface: IW_SCAN_OUTPUT
+ else:
+ iw._scan = lambda interface: ''
+ return super(ConnectionManager, self)._find_bssids(wcli)
+
+ def _update_wlan_configuration(self, wlan_configuration):
+ wlan_configuration.command.insert(0, 'echo')
+ wlan_configuration._wpa_control_interface = self._wpa_control_interface
+ wlan_configuration.status_dir = self._status_dir
+ wlan_configuration.interface_status_dir = self._interface_status_dir
+ wlan_configuration.gateway_file_prefix = self.GATEWAY_FILE_PREFIX
+
+ super(ConnectionManager, self)._update_wlan_configuration(
+ wlan_configuration)
+
+ # Just looking for last_wifi_scan_time to change doesn't work because the
+ # tests run too fast.
+ def _wifi_scan(self, wifi):
+ super(ConnectionManager, self)._wifi_scan(wifi)
+ wifi.wifi_scan_counter += 1
+
+ def tell_ifplugd_about_moca(self, up):
+ # Typically, when moca comes up, conman calls ifplugd.action, which writes
+ # this file.
+ self.write_interface_status_file('moca0', '1' if up else '0')
+
+ # Non-overrides
+
+ def wifi_for_band(self, band):
+ for wifi in self.wifi:
+ if band in wifi.bands:
+ return wifi
+
+ def access_point_up(self, band):
+ if band not in self._wlan_configuration:
+ return False
+
+ return self._wlan_configuration[band].access_point_up
+
+ def client_up(self, band):
+ if band not in self._wlan_configuration:
+ return False
+
+ return self._wlan_configuration[band].client_up
+
+ # Test methods
+
+ def wlan_config_filename(self, band):
+ return os.path.join(self._status_dir, 'command.%s' % band)
+
+ def access_point_filename(self, band):
+ return os.path.join(self._status_dir, 'access_point.%s' % band)
+
+ def delete_wlan_config(self, band):
+ os.unlink(self.wlan_config_filename(band))
+
+ def write_wlan_config(self, band, ssid, psk, atomic=False):
+ final_filename = self.wlan_config_filename(band)
+ filename = final_filename + ('.tmp' if atomic else '')
+ with open(filename, 'w') as f:
+ f.write('\n'.join(['env', 'WIFI_PSK=%s' % psk,
+ 'wifi', 'set', '-b', band, '--ssid', ssid]))
+ if atomic:
+ os.rename(filename, final_filename)
+
+ def enable_access_point(self, band):
+ open(self.access_point_filename(band), 'w')
+
+ def disable_access_point(self, band):
+ ap_filename = self.access_point_filename(band)
+ if os.path.isfile(ap_filename):
+ os.unlink(ap_filename)
+
+ def write_gateway_file(self, interface_name):
+ gateway_file = os.path.join(self._status_dir,
+ self.GATEWAY_FILE_PREFIX + interface_name)
+ with open(gateway_file, 'w') as f:
+ # This value doesn't matter to conman, so it's fine to hard code it here.
+ f.write('192.168.1.1')
+
+ def write_interface_status_file(self, interface_name, value):
+ status_file = os.path.join(self._interface_status_dir, interface_name)
+ with open(status_file, 'w') as f:
+ # This value doesn't matter to conman, so it's fine to hard code it here.
+ f.write(value)
+
+ def set_ethernet(self, up):
+ self.write_interface_status_file('eth0', '1' if up else '0')
+
+ if up:
+ # On the real system, ifplugd would run dhclient, which would write a
+ # gateway file.
+ self.write_gateway_file('br0')
+
+ def set_moca(self, up):
+ moca_node1_file = os.path.join(self._moca_status_dir,
+ self.MOCA_NODE_FILE_PREFIX + '1')
+ with open(moca_node1_file, 'w') as f:
+ f.write(FAKE_MOCA_NODE1_FILE if up else
+ FAKE_MOCA_NODE1_FILE_DISCONNECTED)
+
+ if up:
+ # On the real system, ifplugd would run dhclient, which would write a
+ # gateway file.
+ self.write_gateway_file('br0')
+
+ def run_until_interface_update(self):
+ while self._interface_update_counter == 0:
+ self.run_once()
+ while self._interface_update_counter != 0:
+ self.run_once()
+
+ def run_until_scan(self, band):
+ wifi = self.wifi_for_band(band)
+ wifi_scan_counter = wifi.wifi_scan_counter
+ while wifi_scan_counter == wifi.wifi_scan_counter:
+ self.run_once()
+
+
+def connection_manager_test(radio_config):
+ """Returns a decorator that does ConnectionManager test boilerplate."""
+ def inner(f):
+ """The actual decorator."""
+ def actual_test():
+ """The actual test function."""
+ run_duration_s = .01
+ interface_update_period = 5
+ wifi_scan_period = 5
+ wifi_scan_period_s = run_duration_s * wifi_scan_period
+
+ # pylint: disable=protected-access
+ original_wifi_show = connection_manager._wifi_show
+ connection_manager._wifi_show = lambda: radio_config
+
+ try:
+ # No initial state.
+ status_dir = tempfile.mkdtemp()
+ os.mkdir(os.path.join(status_dir, 'interfaces'))
+ moca_status_dir = tempfile.mkdtemp()
+ wpa_control_interface = tempfile.mkdtemp()
+
+ c = ConnectionManager(status_dir=status_dir,
+ moca_status_dir=moca_status_dir,
+ wpa_control_interface=wpa_control_interface,
+ run_duration_s=run_duration_s,
+ interface_update_period=interface_update_period,
+ wifi_scan_period_s=wifi_scan_period_s)
+
+ c.test_interface_update_period = interface_update_period
+ c.test_wifi_scan_period = wifi_scan_period
+
+ f(c)
+
+ finally:
+ shutil.rmtree(status_dir)
+ shutil.rmtree(moca_status_dir)
+ shutil.rmtree(wpa_control_interface)
+ # pylint: disable=protected-access
+ connection_manager._wifi_show = original_wifi_show
+
+ actual_test.func_name = f.func_name
+ return actual_test
+
+ return inner
+
+
+def connection_manager_test_radio_independent(c):
+ """Test ConnectionManager for things independent of radio configuration.
+
+ To verify that these things are both independent, this function is called
+ twice below, once with each radio configuration. Those wrappers have the
+ relevant test decorators.
+
+ Args:
+ c: A ConnectionManager set up by @connection_manager_test.
+ """
+
+ # Initially, no access.
+ wvtest.WVFAIL(c.acs())
+ wvtest.WVFAIL(c.internet())
+
+ c.run_once()
+
+ # Bring up ethernet, access.
+ c.set_ethernet(True)
+ c.run_once()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVPASS(c.internet())
+ wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # Take down ethernet, no access.
+ c.set_ethernet(False)
+ c.run_once()
+ wvtest.WVFAIL(c.acs())
+ wvtest.WVFAIL(c.internet())
+ wvtest.WVFAIL(c.bridge.current_route())
+
+ # Bring up moca, access.
+ c.set_moca(True)
+ c.run_once()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVPASS(c.internet())
+ wvtest.WVPASS(c.bridge.current_route())
+
+ # Bring up ethernet, access via both moca and ethernet.
+ c.set_ethernet(True)
+ c.run_once()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVPASS(c.internet())
+ wvtest.WVPASS(c.bridge.current_route())
+
+ # Bring down moca, still have access via ethernet.
+ c.set_moca(False)
+ c.run_once()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVPASS(c.internet())
+ wvtest.WVPASS(c.bridge.current_route())
+
+ # The bridge interfaces are up, but they can't reach anything.
+ c.bridge.set_connection_check_result('fail')
+ c.run_until_interface_update()
+ wvtest.WVFAIL(c.acs())
+ wvtest.WVFAIL(c.internet())
+ wvtest.WVFAIL(c.bridge.current_route())
+
+ # Now c connects to a restricted network.
+ c.bridge.set_connection_check_result('restricted')
+ c.run_until_interface_update()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVFAIL(c.internet())
+ wvtest.WVPASS(c.bridge.current_route())
+
+ # Now the wired connection goes away.
+ c.set_ethernet(False)
+ c.set_moca(False)
+ c.run_once()
+ wvtest.WVFAIL(c.acs())
+ wvtest.WVFAIL(c.internet())
+ wvtest.WVFAIL(c.bridge.current_route())
+
+ # Now there are some scan results.
+ c.scan_has_results = True
+ # Wait for a scan, plus 3 cycles, so that s2 will have been tried.
+ c.run_until_scan('2.4')
+ for _ in range(3):
+ c.run_once()
+ wvtest.WVPASSEQ(c.last_provisioning_attempt.ssid, 's2')
+ wvtest.WVPASSEQ(c.last_provisioning_attempt.bssid, '01:23:45:67:89:ab')
+ # Wait for the connection to be processed.
+ c.run_once()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVFAIL(c.internet())
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+
+ # Now, create a WLAN configuration which should be connected to. Also, test
+ # that atomic writes/renames work.
+ ssid = 'wlan'
+ psk = 'password'
+ c.write_wlan_config('2.4', ssid, psk, atomic=True)
+ c.disable_access_point('2.4')
+ c.run_once()
+ wvtest.WVPASS(c.client_up('2.4'))
+ wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+
+ # Now enable the AP. Since we have no wired connection, this should have no
+ # effect.
+ c.enable_access_point('2.4')
+ c.run_once()
+ wvtest.WVPASS(c.client_up('2.4'))
+ wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.bridge.current_route())
+
+ # Now bring up the bridge. We should remove the wifi connection and start
+ # an AP.
+ c.set_ethernet(True)
+ c.bridge.set_connection_check_result('succeed')
+ c.run_until_interface_update()
+ wvtest.WVPASS(c.access_point_up('2.4'))
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.bridge.current_route())
+
+ # Now move (rather than delete) the configuration file. The AP should go
+ # away, and we should not be able to join the WLAN. Routes should not be
+ # affected.
+ filename = c.wlan_config_filename('2.4')
+ other_filename = filename + '.bak'
+ os.rename(filename, other_filename)
+ c.run_once()
+ wvtest.WVFAIL(c.access_point_up('2.4'))
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.bridge.current_route())
+
+ # Now move it back, and the AP should come back.
+ os.rename(other_filename, filename)
+ c.run_once()
+ wvtest.WVPASS(c.access_point_up('2.4'))
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.bridge.current_route())
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO)
+def connection_manager_test_radio_independent_one_radio(c):
+ connection_manager_test_radio_independent(c)
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_TWO_RADIOS)
+def connection_manager_test_radio_independent_two_radios(c):
+ connection_manager_test_radio_independent(c)
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_TWO_RADIOS)
+def connection_manager_test_two_radios(c):
+ """Test ConnectionManager for devices with two radios.
+
+ This test should be kept roughly parallel to the one-radio test.
+
+ Args:
+ c: The ConnectionManager set up by @connection_manager_test.
+ """
+ # Bring up ethernet, access.
+ c.set_ethernet(True)
+ c.run_once()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVPASS(c.internet())
+
+ ssid = 'my ssid'
+ psk = 'passphrase'
+
+ # Bring up both access points.
+ c.write_wlan_config('2.4', ssid, psk)
+ c.enable_access_point('2.4')
+ c.write_wlan_config('5', ssid, psk)
+ c.enable_access_point('5')
+ c.run_once()
+ wvtest.WVPASS(c.access_point_up('2.4'))
+ wvtest.WVPASS(c.access_point_up('5'))
+ wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # Disable the 2.4 GHz AP, make sure the 5 GHz AP stays up. 2.4 GHz should
+ # join the WLAN.
+ c.disable_access_point('2.4')
+ c.run_until_interface_update()
+ wvtest.WVFAIL(c.access_point_up('2.4'))
+ wvtest.WVPASS(c.access_point_up('5'))
+ wvtest.WVPASS(c.client_up('2.4'))
+ wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # Delete the 2.4 GHz WLAN configuration; it should leave the WLAN but nothing
+ # else should change.
+ c.delete_wlan_config('2.4')
+ c.run_until_interface_update()
+ wvtest.WVFAIL(c.access_point_up('2.4'))
+ wvtest.WVPASS(c.access_point_up('5'))
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # Disable the wired connection and remove the WLAN configurations. Both
+ # radios should scan. Wait for 5 GHz to scan, then enable scan results for
+ # 2.4. This should lead to ACS access.
+ c.delete_wlan_config('5')
+ c.set_ethernet(False)
+ c.run_once()
+ wvtest.WVFAIL(c.acs())
+ wvtest.WVFAIL(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # The 5 GHz scan has no results.
+ c.run_until_scan('5')
+ c.run_once()
+ c.run_until_interface_update()
+ wvtest.WVFAIL(c.acs())
+ wvtest.WVFAIL(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # The next 2.4 GHz scan will have results.
+ c.scan_has_results = True
+ c.run_until_scan('2.4')
+ # Now run 3 cycles, so that s2 will have been tried.
+ for _ in range(3):
+ c.run_once()
+ c.run_until_interface_update()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVFAIL(c.bridge.current_route())
+ wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+
+@wvtest.wvtest
+@connection_manager_test(WIFI_SHOW_OUTPUT_ONE_RADIO)
+def connection_manager_test_one_radio(c):
+ """Test ConnectionManager for devices with one radio.
+
+ This test should be kept roughly parallel to the two-radio test.
+
+ Args:
+ c: The ConnectionManager set up by @connection_manager_test.
+ """
+ # Bring up ethernet, access.
+ c.set_ethernet(True)
+ c.run_once()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVPASS(c.internet())
+
+ ssid = 'my ssid'
+ psk = 'passphrase'
+
+ # Enable both access points. Only 5 should be up.
+ c.write_wlan_config('2.4', ssid, psk)
+ c.enable_access_point('2.4')
+ c.write_wlan_config('5', ssid, psk)
+ c.enable_access_point('5')
+ c.run_once()
+ wvtest.WVFAIL(c.access_point_up('2.4'))
+ wvtest.WVPASS(c.access_point_up('5'))
+ wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # Disable the 2.4 GHz AP; nothing should change. The 2.4 GHz client should
+ # not be up because the same radio is being used to run a 5 GHz AP.
+ c.disable_access_point('2.4')
+ c.run_until_interface_update()
+ wvtest.WVFAIL(c.access_point_up('2.4'))
+ wvtest.WVPASS(c.access_point_up('5'))
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # Delete the 2.4 GHz WLAN configuration; nothing should change.
+ c.delete_wlan_config('2.4')
+ c.run_once()
+ wvtest.WVFAIL(c.access_point_up('2.4'))
+ wvtest.WVPASS(c.access_point_up('5'))
+ wvtest.WVFAIL(c.client_up('2.4'))
+ wvtest.WVPASS(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # Disable the wired connection and remove the WLAN configurations. There
+ # should be a single scan that leads to ACS access. (It doesn't matter which
+ # band we specify in run_until_scan, since both bands point to the same
+ # interface.)
+ c.delete_wlan_config('5')
+ c.set_ethernet(False)
+ c.run_once()
+ wvtest.WVFAIL(c.acs())
+ wvtest.WVFAIL(c.bridge.current_route())
+ wvtest.WVFAIL(c.wifi_for_band('2.4').current_route())
+ wvtest.WVFAIL(c.wifi_for_band('5').current_route())
+
+ # The wcli0 scan will have results that will lead to ACS access.
+ c.scan_has_results = True
+ c.run_until_scan('5')
+ for _ in range(3):
+ c.run_once()
+ c.run_until_interface_update()
+ wvtest.WVPASS(c.acs())
+ wvtest.WVFAIL(c.bridge.current_route())
+ wvtest.WVPASS(c.wifi_for_band('2.4').current_route())
+ wvtest.WVPASS(c.wifi_for_band('5').current_route())
+
+
+if __name__ == '__main__':
+ wvtest.wvtest_main()
diff --git a/conman/cycler.py b/conman/cycler.py
new file mode 100755
index 0000000..ff65bfc
--- /dev/null
+++ b/conman/cycler.py
@@ -0,0 +1,81 @@
+#!/usr/bin/python -S
+
+"""Utility for attempting to join a list of BSSIDs."""
+
+import time
+
+
+class AgingPriorityCycler(object):
+ """A modified priority queue.
+
+ 1) Items are not removed from the queue, but automatically reinserted (thus
+ "cycler" rather than "queue").
+ 2) Baseline priority is multiplied by time in queue.
+
+ This data structure is not efficient and may not scale well. Don't use it for
+ anything big.
+
+ As a minor optimization, items must be hashable. This restriction could be
+ removed, but we don't have a use case for non-hashable values yet.
+ """
+
+ def __init__(self, cycle_length_s=0, items=()):
+ """Initializes the queue.
+
+ Args:
+ cycle_length_s: The minimum amount of time an item will spend in the
+ queue after being automatically reinserted.
+ items: Initial items for the queue, as tuples of (item, priority).
+ """
+ t = time.time()
+ self._items = {item: [priority, t] for item, priority in items}
+ self._min_time_in_queue_s = cycle_length_s
+
+ def empty(self):
+ return not self._items
+
+ def insert(self, item, priority):
+ """Insert a new item, or update the priority of an existing item."""
+ try:
+ self._items[item][0] = priority
+ except KeyError:
+ self._items[item] = [priority, time.time()]
+
+ def remove(self, item):
+ if item in self._items:
+ self._items.pop(item)
+
+ def peek(self):
+ """Return the next item in the queue, but do not cycle it."""
+ return self._find_next(cycle=False)
+
+ def next(self):
+ """Return the next item in the queue.
+
+ Also resets that item's age to now + cycle_length_s.
+
+ Returns:
+ The next item in the queue.
+ """
+ return self._find_next(True)
+
+ def _find_next(self, cycle=False):
+ """Implementation of peek and next."""
+ if self.empty():
+ return
+
+ now = time.time()
+
+ def aged_priority(key_value):
+ _, (priority, birth) = key_value
+ return priority * (now - birth)
+
+ result, value = max(self._items.iteritems(), key=aged_priority)
+ if value[1] > now:
+ return
+
+ if cycle:
+ value[1] = now + self._min_time_in_queue_s
+
+ return result
+
diff --git a/conman/cycler_test.py b/conman/cycler_test.py
new file mode 100755
index 0000000..c4e498b
--- /dev/null
+++ b/conman/cycler_test.py
@@ -0,0 +1,45 @@
+#!/usr/bin/python
+
+"""Tests for cycler.py."""
+
+import time
+
+import cycler
+from wvtest import wvtest
+
+
+@wvtest.wvtest
+def cycler_test():
+ c = cycler.AgingPriorityCycler()
+ wvtest.WVPASS(c.next() is None)
+
+ cycle_length_s = .01
+ c = cycler.AgingPriorityCycler(cycle_length_s=cycle_length_s,
+ items=(('A', 10), ('B', 5), ('C', 1)))
+
+ # We should get all three in order, since they all have the same insertion
+ # time. They will all get slightly different insertion times, but next()
+ # should be fast enough that the differences don't make much difference.
+ wvtest.WVPASS(c.peek() == 'A')
+ wvtest.WVPASS(c.next() == 'A')
+ wvtest.WVPASS(c.next() == 'B')
+ wvtest.WVPASS(c.next() == 'C')
+ wvtest.WVPASS(c.peek() is None)
+ wvtest.WVPASS(c.next() is None)
+ wvtest.WVPASS(c.next() is None)
+
+ # Now, wait for items to be ready again and just cycle one of them.
+ time.sleep(cycle_length_s)
+ wvtest.WVPASS(c.next() == 'A')
+
+ # Now, if we wait 1.9 cycles, the aged priorities will be as follows:
+ # A: 0.9 * 10 = 9
+ # B: 1.9 * 5 = 9.5
+ # C: 1.9 * 1 = 1.9
+ time.sleep(cycle_length_s * 1.9)
+ wvtest.WVPASS(c.next() == 'B')
+ wvtest.WVPASS(c.next() == 'A')
+ wvtest.WVPASS(c.next() == 'C')
+
+if __name__ == '__main__':
+ wvtest.wvtest_main()
diff --git a/conman/interface.py b/conman/interface.py
new file mode 100755
index 0000000..c2de318
--- /dev/null
+++ b/conman/interface.py
@@ -0,0 +1,330 @@
+#!/usr/bin/python
+
+"""Models wired and wireless interfaces."""
+
+import logging
+import os
+import re
+import subprocess
+
+import wpactrl
+
+METRIC_5GHZ = 20
+METRIC_24GHZ_5GHZ = 21
+METRIC_24GHZ = 22
+METRIC_TEMPORARY_CONNECTION_CHECK = 99
+
+
+class Interface(object):
+ """Represents an interface.
+
+ Base class for more specific interface types.
+ """
+
+ CONNECTION_CHECK = 'connection_check'
+ IP_ROUTE = ['ip', 'route']
+
+ def __init__(self, name, metric):
+ self.name = name
+
+ # Currently connected links for this interface, e.g. ethernet.
+ self.links = set()
+
+ # Whether [ACS, internet] access is currently available via this interface.
+ self._has_acs = None
+ self._has_internet = None
+
+ # The gateway IP for this interface.
+ self._gateway_ip = None
+ self.metric = metric
+
+ def _connection_check(self, check_acs):
+ """Check this interface's connection status.
+
+ Args:
+ check_acs: If true, check for ACS access rather than internet access.
+
+ Returns:
+ Whether the connection is working.
+ """
+ if not self.links:
+ return False
+
+ logging.debug('gateway ip for %s is %s', self.name, self._gateway_ip)
+ if self._gateway_ip is None:
+ return False
+
+ # Temporarily add a route to make sure the connection check can be run.
+ # Give it a high metric so that it won't interfere with normal default
+ # routes.
+ added_temporary_route = False
+ if not self.current_route():
+ logging.debug('Adding temporary connection check route for dev %s',
+ self.name)
+ self._ip_route('add', 'default',
+ 'via', self._gateway_ip,
+ 'dev', self.name,
+ 'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
+
+ cmd = [self.CONNECTION_CHECK, '-I', self.name]
+ if check_acs:
+ cmd.append('-a')
+
+ with open(os.devnull, 'w') as devnull:
+ result = subprocess.call(cmd, stdout=devnull, stderr=devnull) == 0
+
+ # Delete the temporary route.
+ if added_temporary_route:
+ logging.debug('Deleting temporary connection check route for dev %s',
+ self.name)
+ self._ip_route('del', 'default',
+ 'dev', self.name,
+ 'metric', str(METRIC_TEMPORARY_CONNECTION_CHECK))
+
+ return result
+
+ def acs(self):
+ if self._has_acs is None:
+ self._has_acs = self._connection_check(check_acs=True)
+
+ return self._has_acs
+
+ def internet(self):
+ if self._has_internet is None:
+ self._has_internet = self._connection_check(check_acs=False)
+
+ return self._has_internet
+
+ def add_route(self):
+ """Adds a default route for this interface.
+
+ First, checks whether an equivalent route already exists, and if so,
+ returns.
+ """
+ if self.metric is None:
+ logging.info('Cannot add route for %s without a metric.', self.name)
+ return
+
+ if self._gateway_ip is None:
+ logging.info('Cannot add route for %s without a gateway IP.', self.name)
+ return
+
+ # If the current default route is the same, there is nothing to do. If it
+ # exists but is different, delete it before adding an updated one.
+ current = self.current_route()
+ if current:
+ if (current.get('via', None) == self._gateway_ip and
+ current.get('metric', None) == str(self.metric)):
+ return
+ else:
+ self.delete_route()
+
+ logging.debug('Adding default route for dev %s', self.name)
+ self._ip_route('add', 'default',
+ 'via', self._gateway_ip,
+ 'dev', self.name,
+ 'metric', str(self.metric))
+
+ def delete_route(self):
+ while self.current_route():
+ logging.debug('Deleting default route for dev %s', self.name)
+ self._ip_route('del', 'default',
+ 'dev', self.name)
+
+ def current_route(self):
+ """Read the current default route for this interface.
+
+ Returns:
+ A dict containing the gateway [and metric] of the route, or an empty dict
+ if there is currently no default route for this interface.
+ """
+ result = {}
+ for line in self._ip_route().splitlines():
+ if line.startswith('default') and 'dev %s' % self.name in line:
+ key = None
+ for token in line.split():
+ if token in ['via', 'metric']:
+ key = token
+ elif key:
+ result[key] = token
+ key = None
+
+ return result
+
+ def _ip_route(self, *args):
+ try:
+ logging.debug('%s calling ip route %s', self.name, ' '.join(args))
+ return subprocess.check_output(self.IP_ROUTE + list(args))
+ except subprocess.CalledProcessError as e:
+ logging.error('Failed to call "ip route" with args %r: %s', args,
+ e.message)
+ return ''
+
+ def set_gateway_ip(self, gateway_ip):
+ logging.debug('New gateway IP %s for %s', gateway_ip, self.name)
+ self._gateway_ip = gateway_ip
+ self.update_routes()
+
+ def _set_link_status(self, link, is_up):
+ """Set whether a link is up or not."""
+ if is_up == (link in self.links):
+ return
+
+ had_links = bool(self.links)
+
+ if is_up:
+ logging.debug('%s gained link %s', self.name, link)
+ self.links.add(link)
+ else:
+ logging.debug('%s lost link %s', self.name, link)
+ self.links.remove(link)
+
+ # If a link goes away, we may have lost access to something but not gained
+ # it, and vice versa.
+ if is_up != self._has_acs:
+ self._has_acs = None
+
+ if is_up != self._has_internet:
+ self._has_internet = None
+
+ if had_links != bool(self.links):
+ self.update_routes(expire_cache=False)
+
+ def expire_connection_status_cache(self):
+ logging.debug('Expiring connection status cache for %s', self.name)
+ self._has_internet = self._has_acs = None
+
+ def update_routes(self, expire_cache=True):
+ """Update this interface's routes.
+
+ If the interface has gained ACS or internet access, add a route. If it had
+ either and now has neither, delete the route.
+
+ Args:
+ expire_cache: If true, force a recheck of connection status before
+ deciding whether to add or remove routes.
+ """
+ logging.debug('Updating routes for %s', self.name)
+ # We care about the distinction between None (unknown) and False (known
+ # inaccessible) here.
+ # pylint: disable=g-explicit-bool-comparison
+ maybe_had_acs = self._has_acs != False
+ maybe_had_internet = self._has_internet != False
+
+ if expire_cache:
+ self.expire_connection_status_cache()
+
+ has_acs = self.acs()
+ has_internet = self.internet()
+ if ((not maybe_had_acs and has_acs) or
+ (not maybe_had_internet and has_internet)):
+ self.add_route()
+ elif ((maybe_had_acs or maybe_had_internet) and not
+ (has_acs or has_internet)):
+ self.delete_route()
+
+
+class Bridge(Interface):
+ """Represents the wired bridge."""
+
+ def __init__(self, *args, **kwargs):
+ super(Bridge, self).__init__(*args, **kwargs)
+ self._moca_stations = set()
+
+ @property
+ def moca(self):
+ return bool(self._moca_stations)
+
+ @moca.setter
+ def moca(self, is_up):
+ self._set_link_status('moca', is_up)
+
+ @property
+ def ethernet(self):
+ return 'ethernet' in self.links
+
+ @ethernet.setter
+ def ethernet(self, is_up):
+ self._set_link_status('ethernet', is_up)
+
+ def add_moca_station(self, node_id):
+ if node_id not in self._moca_stations:
+ self._moca_stations.add(node_id)
+ self.moca = True
+
+ def remove_moca_station(self, node_id):
+ if node_id in self._moca_stations:
+ self._moca_stations.remove(node_id)
+ self.moca = bool(self._moca_stations)
+
+
+class Wifi(Interface):
+ """Represents the wireless interface."""
+
+ WPA_EVENT_RE = re.compile(r'<\d+>CTRL-EVENT-(?P<event>[A-Z\-]+).*')
+
+ def __init__(self, *args, **kwargs):
+ self.bands = kwargs.pop('bands', [])
+ super(Wifi, self).__init__(*args, **kwargs)
+ self._wpa_control = None
+
+ @property
+ def wpa_supplicant(self):
+ return 'wpa_supplicant' in self.links
+
+ @wpa_supplicant.setter
+ def wpa_supplicant(self, is_up):
+ self._set_link_status('wpa_supplicant', is_up)
+
+ def attached(self):
+ return self._wpa_control and self._wpa_control.attached
+
+ def attach_wpa_control(self, path):
+ if self.attached():
+ return
+
+ socket = os.path.join(path, self.name)
+ if os.path.exists(socket):
+ try:
+ self._wpa_control = self.get_wpa_control(socket)
+ self._wpa_control.attach()
+ except wpactrl.error as e:
+ logging.error('Error attaching to wpa_supplicant: %s', e)
+ return
+
+ self.wpa_supplicant = ('wpa_state=COMPLETED' in
+ self._wpa_control.request('STATUS'))
+
+ def get_wpa_control(self, socket):
+ return wpactrl.WPACtrl(socket)
+
+ def detach_wpa_control(self):
+ if self.attached():
+ try:
+ self._wpa_control.detach()
+ except wpactrl.error:
+ logging.error('Failed to detach from wpa_supplicant interface. This '
+ 'may mean something else killed wpa_supplicant.')
+ self._wpa_control = None
+
+ self.wpa_supplicant = False
+
+ def handle_wpa_events(self):
+ if not self.attached():
+ self.wpa_supplicant = False
+ return
+
+ while self._wpa_control.pending():
+ match = self.WPA_EVENT_RE.match(self._wpa_control.recv())
+ if match:
+ event = match.group('event')
+ if event == 'CONNECTED':
+ self.wpa_supplicant = True
+ elif event in ('DISCONNECTED', 'TERMINATING', 'ASSOC-REJECT',
+ 'AUTH-REJECT'):
+ self.wpa_supplicant = False
+ if event == 'TERMINATING':
+ self.detach_wpa_control()
+ break
+
+ self.update_routes()
diff --git a/conman/interface_test.py b/conman/interface_test.py
new file mode 100755
index 0000000..edc3541
--- /dev/null
+++ b/conman/interface_test.py
@@ -0,0 +1,233 @@
+#!/usr/bin/python
+
+"""Tests for connection_manager.py."""
+
+import logging
+import os
+import shutil
+import tempfile
+
+import wpactrl
+
+import interface
+from wvtest import wvtest
+
+logging.basicConfig(level=logging.DEBUG)
+
+
+class FakeInterfaceMixin(object):
+ """Replace Interface methods which interact with the system."""
+
+ def __init__(self, *args, **kwargs):
+ super(FakeInterfaceMixin, self).__init__(*args, **kwargs)
+ self.set_connection_check_result('succeed')
+ self.routing_table = {}
+
+ def set_connection_check_result(self, result):
+ if result in ['succeed', 'fail', 'restricted']:
+ # pylint: disable=invalid-name
+ self.CONNECTION_CHECK = './test/' + result
+ else:
+ raise ValueError('Invalid fake connection_check script.')
+
+ def _ip_route(self, *args):
+ if not args:
+ return '\n'.join(self.routing_table.values() +
+ ['1.2.3.4/24 dev %s proto kernel scope link' % self.name,
+ 'default via 1.2.3.4 dev fake0',
+ 'random junk'])
+
+ metric = None
+ if 'metric' in args:
+ metric = args[args.index('metric') + 1]
+ key = (self.name, metric)
+ if args[0] == 'add' and key not in self.routing_table:
+ logging.debug('Adding route for %r', key)
+ self.routing_table[key] = ' '.join(args[1:])
+ elif args[0] == 'del':
+ if key in self.routing_table:
+ logging.debug('Deleting route for %r', key)
+ del self.routing_table[key]
+ elif key[1] is None:
+ # pylint: disable=g-builtin-op
+ for k in self.routing_table.keys():
+ if k[0] == key[0]:
+ logging.debug('Deleting route for %r (generalized from %s)', k, key)
+ del self.routing_table[k]
+
+
+class Bridge(FakeInterfaceMixin, interface.Bridge):
+ pass
+
+
+class FakeWPACtrl(object):
+ """Fake wpactrl.WPACtrl."""
+
+ # pylint: disable=unused-argument
+ def __init__(self, socket):
+ self._socket = socket
+ self.events = []
+ self.attached = False
+ self.connected = False
+
+ def pending(self):
+ return bool(self.events)
+
+ def recv(self):
+ return self.events.pop(0)
+
+ def attach(self):
+ if not os.path.exists(self._socket):
+ raise wpactrl.error('wpactrl_attach failed')
+ self.attached = True
+
+ def detach(self):
+ if not os.path.exists(self._socket):
+ raise wpactrl.error('wpactrl_detach failed')
+ self.attached = False
+
+ def request(self, request_type):
+ if request_type == 'STATUS':
+ return 'foo\nwpa_state=COMPLETED\nbar' if self.connected else 'foo'
+ else:
+ raise ValueError('Invalid request_type %s' % request_type)
+
+ # Below methods are not part of WPACtrl.
+
+ def add_event(self, event):
+ self.events.append(event)
+
+
+class Wifi(FakeInterfaceMixin, interface.Wifi):
+ """Fake Wifi for testing."""
+
+ CONNECTED_EVENT = '<2>CTRL-EVENT-CONNECTED'
+ DISCONNECTED_EVENT = '<2>CTRL-EVENT-DISCONNECTED'
+ TERMINATING_EVENT = '<2>CTRL-EVENT-TERMINATING'
+
+ def __init__(self, *args, **kwargs):
+ super(Wifi, self).__init__(*args, **kwargs)
+ self._initially_connected = False
+
+ def attach_wpa_control(self, *args, **kwargs):
+ if self._initially_connected and self._wpa_control:
+ self._wpa_control.connected = True
+ super(Wifi, self).attach_wpa_control(*args, **kwargs)
+
+ def get_wpa_control(self, socket):
+ result = FakeWPACtrl(socket)
+ result.connected = self._initially_connected
+ return result
+
+ def add_connected_event(self):
+ if self.attached():
+ self._wpa_control.add_event(self.CONNECTED_EVENT)
+
+ def add_disconnected_event(self):
+ if self.attached():
+ self._wpa_control.add_event(self.DISCONNECTED_EVENT)
+
+ def add_terminating_event(self):
+ if self.attached():
+ self._wpa_control.add_event(self.TERMINATING_EVENT)
+
+
+@wvtest.wvtest
+def bridge_test():
+ """Test Interface and Bridge."""
+ b = Bridge('br0', '10')
+ b.set_connection_check_result('succeed')
+
+ wvtest.WVFAIL(b.acs())
+ wvtest.WVFAIL(b.internet())
+ wvtest.WVFAIL(b.current_route())
+
+ b.add_moca_station(0)
+ b.set_gateway_ip('192.168.1.1')
+ wvtest.WVPASS(b.acs())
+ wvtest.WVPASS(b.internet())
+ wvtest.WVPASS(b.current_route())
+
+ b.add_moca_station(1)
+ wvtest.WVPASS(b.acs())
+ wvtest.WVPASS(b.internet())
+ wvtest.WVPASS(b.current_route())
+
+ b.remove_moca_station(0)
+ b.remove_moca_station(1)
+ wvtest.WVFAIL(b.acs())
+ wvtest.WVFAIL(b.internet())
+ wvtest.WVFAIL(b.current_route())
+
+ b.add_moca_station(2)
+ wvtest.WVPASS(b.acs())
+ wvtest.WVPASS(b.internet())
+ wvtest.WVPASS(b.current_route())
+
+ b.set_connection_check_result('fail')
+ b.update_routes()
+ wvtest.WVFAIL(b.acs())
+ wvtest.WVFAIL(b.internet())
+ wvtest.WVFAIL(b.current_route())
+
+ b.set_connection_check_result('restricted')
+ b.update_routes()
+ wvtest.WVPASS(b.acs())
+ wvtest.WVFAIL(b.internet())
+ wvtest.WVPASS(b.current_route())
+
+
+@wvtest.wvtest
+def wifi_test():
+ """Test Wifi."""
+ w = Wifi('wcli0', '21')
+ w.set_connection_check_result('succeed')
+
+ try:
+ wpa_path = tempfile.mkdtemp()
+ socket = os.path.join(wpa_path, w.name)
+ open(socket, 'w')
+
+ # Not currently connected.
+ w.attach_wpa_control(wpa_path)
+ wvtest.WVFAIL(w.wpa_supplicant)
+
+ # pylint: disable=protected-access
+ wpa_control = w._wpa_control
+
+ # wpa_supplicant connects.
+ wpa_control.add_event(Wifi.CONNECTED_EVENT)
+ wvtest.WVFAIL(w.wpa_supplicant)
+ w.handle_wpa_events()
+ wvtest.WVPASS(w.wpa_supplicant)
+ w.set_gateway_ip('192.168.1.1')
+
+ # wpa_supplicant disconnects.
+ wpa_control.add_event(Wifi.DISCONNECTED_EVENT)
+ w.handle_wpa_events()
+ wvtest.WVFAIL(w.wpa_supplicant)
+
+ # Now, start over so we can test what happens when wpa_supplicant is already
+ # connected when we attach.
+ w.detach_wpa_control()
+ # pylint: disable=protected-access
+ w._initially_connected = True
+ w.attach_wpa_control(wpa_path)
+ wpa_control = w._wpa_control
+
+ # wpa_supplicant was already connected when we attached.
+ wvtest.WVPASS(w.wpa_supplicant)
+
+ # The wpa_supplicant process disconnects and terminates.
+ wpa_control.add_event(Wifi.DISCONNECTED_EVENT)
+ wpa_control.add_event(Wifi.TERMINATING_EVENT)
+ os.unlink(socket)
+ w.handle_wpa_events()
+ wvtest.WVFAIL(w.wpa_supplicant)
+
+ finally:
+ shutil.rmtree(wpa_path)
+
+
+if __name__ == '__main__':
+ wvtest.wvtest_main()
diff --git a/conman/iw.py b/conman/iw.py
new file mode 100755
index 0000000..f751302
--- /dev/null
+++ b/conman/iw.py
@@ -0,0 +1,111 @@
+#!/usr/bin/python
+
+"""Utility functions for calling iw and parsing its output."""
+
+import re
+import subprocess
+
+
+def _scan(interface, **kwargs):
+ try:
+ return subprocess.check_output(('iw', 'dev', interface, 'scan'), **kwargs)
+ except subprocess.CalledProcessError:
+ return ''
+
+
+_BSSID_RE = r'BSS (?P<BSSID>([0-9a-f]{2}:?){6})\(on .*\)'
+_SSID_RE = r'SSID: (?P<SSID>.*)'
+_VENDOR_IE_RE = (r'Vendor specific: OUI (?P<OUI>([0-9a-f]{2}:?){3}), '
+ 'data:(?P<data>( [0-9a-f]{2})+)')
+
+
+class BssInfo(object):
+ """Contains info about a BSS, parsed from 'iw scan'."""
+
+ def __init__(self, bssid='', ssid='', security=None, vendor_ies=None):
+ self.bssid = bssid
+ self.ssid = ssid
+ self.vendor_ies = vendor_ies or []
+ self.security = security or []
+
+ def __attrs(self):
+ return (self.bssid, self.ssid, tuple(sorted(self.vendor_ies)),
+ tuple(sorted(self.security)))
+
+ def __eq__(self, other):
+ # pylint: disable=protected-access
+ return self.__attrs() == other.__attrs()
+
+ def __hash__(self):
+ return hash(self.__attrs())
+
+ def __repr__(self):
+ return '<BssInfo: SSID=%s BSSID=%s Security=%s Vendor IEs=%s>' % (
+ self.ssid, self.bssid, ','.join(self.security),
+ ','.join('|'.join(ie) for ie in self.vendor_ies))
+
+
+# TODO(rofrankel): waveguide also scans. Can we find a way to avoid two programs
+# scanning in parallel?
+def scan_parsed(interface, **kwargs):
+ """Return the parsed results of 'iw scan'."""
+ result = []
+ bss_info = None
+ for line in _scan(interface, **kwargs).splitlines():
+ line = line.strip()
+ match = re.match(_BSSID_RE, line)
+ if match:
+ if bss_info:
+ result.append(bss_info)
+ bss_info = BssInfo(bssid=match.group('BSSID'))
+ continue
+ match = re.match(_SSID_RE, line)
+ if match:
+ bss_info.ssid = match.group('SSID')
+ continue
+ match = re.match(_VENDOR_IE_RE, line)
+ if match:
+ bss_info.vendor_ies.append((match.group('OUI'),
+ match.group('data').strip()))
+ continue
+ if line.startswith('RSN:'):
+ bss_info.security.append('WPA2')
+ elif line.startswith('WPA:'):
+ bss_info.security.append('WPA')
+ elif line.startswith('Privacy:'):
+ bss_info.security.append('WEP')
+
+ if bss_info:
+ result.append(bss_info)
+
+ return result
+
+
+def find_bssids(interface, vendor_ie_function, include_secure):
+ """Return information about interesting access points.
+
+ Args:
+ interface: The wireless interface with which to scan.
+ vendor_ie_function: A function that takes a vendor IE and returns a bool.
+ include_secure: Whether to exclude secure networks.
+
+ Returns:
+ Two lists of tuples of the form (SSID, BSSID info dict). The first list has
+ BSSIDs which have a vendor IE accepted by vendor_ie_function, and the second
+ list has those which don't.
+ """
+ parsed = scan_parsed(interface)
+ result_with_ie = set()
+ result_without_ie = set()
+
+ for bss_info in parsed:
+ if bss_info.security and not include_secure:
+ continue
+ for oui, data in bss_info.vendor_ies:
+ if vendor_ie_function(oui, data):
+ result_with_ie.add(bss_info)
+ break
+ else:
+ result_without_ie.add(bss_info)
+
+ return result_with_ie, result_without_ie
diff --git a/conman/iw_test.py b/conman/iw_test.py
new file mode 100755
index 0000000..c069c91
--- /dev/null
+++ b/conman/iw_test.py
@@ -0,0 +1,535 @@
+#!/usr/bin/python
+
+"""Tests for iw.py."""
+
+import iw
+from wvtest import wvtest
+
+
+SCAN_OUTPUT = """BSS 00:23:97:57:f4:d8(on wcli0)
+ TSF: 1269828266773 usec (14d, 16:43:48)
+ freq: 2437
+ beacon interval: 100 TUs
+ capability: ESS Privacy ShortSlotTime (0x0411)
+ signal: -60.00 dBm
+ last seen: 2190 ms ago
+ Information elements from Probe Response frame:
+ Vendor specific: OUI 00:11:22, data: 01 23 45 67
+ SSID: short scan result
+ Supported rates: 1.0* 2.0* 5.5* 11.0* 18.0 24.0 36.0 54.0
+ DS Parameter set: channel 6
+ ERP: <no flags>
+ ERP D4.0: <no flags>
+ Privacy: WEP
+ Extended supported rates: 6.0 9.0 12.0 48.0
+BSS 94:b4:0f:f1:02:a0(on wcli0)
+ TSF: 16233722683 usec (0d, 04:30:33)
+ freq: 2412
+ beacon interval: 100 TUs
+ capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
+ signal: -54.00 dBm
+ last seen: 2490 ms ago
+ Information elements from Probe Response frame:
+ SSID: Google
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 1
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ RSN: * Version: 1
+ * Group cipher: CCMP
+ * Pairwise ciphers: CCMP
+ * Authentication suites: IEEE 802.1X
+ * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
+ BSS Load:
+ * station count: 0
+ * channel utilisation: 33/255
+ * available admission capacity: 25625 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 1
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 0
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+BSS 94:b4:0f:f1:35:60(on wcli0)
+ TSF: 1739987968 usec (0d, 00:28:59)
+ freq: 2462
+ beacon interval: 100 TUs
+ capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
+ signal: -39.00 dBm
+ last seen: 1910 ms ago
+ Information elements from Probe Response frame:
+ SSID: Google
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 11
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ RSN: * Version: 1
+ * Group cipher: CCMP
+ * Pairwise ciphers: CCMP
+ * Authentication suites: IEEE 802.1X
+ * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
+ BSS Load:
+ * station count: 0
+ * channel utilisation: 49/255
+ * available admission capacity: 26875 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 11
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 1
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+BSS 94:b4:0f:f1:35:61(on wcli0)
+ TSF: 1739988134 usec (0d, 00:28:59)
+ freq: 2462
+ beacon interval: 100 TUs
+ capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
+ signal: -38.00 dBm
+ last seen: 1910 ms ago
+ Information elements from Probe Response frame:
+ SSID: GoogleGuest
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 11
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ BSS Load:
+ * station count: 1
+ * channel utilisation: 49/255
+ * available admission capacity: 26875 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 11
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 1
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+BSS 94:b4:0f:f1:3a:e0(on wcli0)
+ TSF: 24578560051 usec (0d, 06:49:38)
+ freq: 2437
+ beacon interval: 100 TUs
+ capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
+ signal: -55.00 dBm
+ last seen: 2310 ms ago
+ Information elements from Probe Response frame:
+ SSID: Google
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 6
+ TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ RSN: * Version: 1
+ * Group cipher: CCMP
+ * Pairwise ciphers: CCMP
+ * Authentication suites: IEEE 802.1X
+ * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
+ BSS Load:
+ * station count: 1
+ * channel utilisation: 21/255
+ * available admission capacity: 28125 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 6
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 1
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+BSS 94:b4:0f:f1:3a:e1(on wcli0)
+ TSF: 24578576547 usec (0d, 06:49:38)
+ freq: 2437
+ beacon interval: 100 TUs
+ capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
+ signal: -65.00 dBm
+ last seen: 80 ms ago
+ Information elements from Probe Response frame:
+ SSID: GoogleGuest
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 6
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ BSS Load:
+ * station count: 2
+ * channel utilisation: 21/255
+ * available admission capacity: 28125 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 6
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 1
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+
+BSS 94:b4:0f:f1:36:41(on wcli0)
+ TSF: 12499149351 usec (0d, 03:28:19)
+ freq: 2437
+ beacon interval: 100 TUs
+ capability: ESS ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1521)
+ signal: -67.00 dBm
+ last seen: 80 ms ago
+ Information elements from Probe Response frame:
+ SSID: GoogleGuest
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 6
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ BSS Load:
+ * station count: 1
+ * channel utilisation: 28/255
+ * available admission capacity: 27500 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 6
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 1
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+BSS 94:b4:0f:f1:36:40(on wcli0)
+ TSF: 12499150000 usec (0d, 03:28:19)
+ freq: 2437
+ beacon interval: 100 TUs
+ capability: ESS Privacy ShortPreamble SpectrumMgmt ShortSlotTime RadioMeasure (0x1531)
+ signal: -66.00 dBm
+ last seen: 2350 ms ago
+ Information elements from Probe Response frame:
+ SSID: Google
+ Supported rates: 36.0* 48.0 54.0
+ DS Parameter set: channel 6
+ TIM: DTIM Count 0 DTIM Period 1 Bitmap Control 0x0 Bitmap[0] 0x0
+ Country: US Environment: Indoor/Outdoor
+ Channels [1 - 11] @ 36 dBm
+ Power constraint: 0 dB
+ TPC report: TX power: 3 dBm
+ ERP: <no flags>
+ RSN: * Version: 1
+ * Group cipher: CCMP
+ * Pairwise ciphers: CCMP
+ * Authentication suites: IEEE 802.1X
+ * Capabilities: 4-PTKSA-RC 4-GTKSA-RC (0x0028)
+ BSS Load:
+ * station count: 0
+ * channel utilisation: 28/255
+ * available admission capacity: 27500 [*32us]
+ HT capabilities:
+ Capabilities: 0x19ad
+ RX LDPC
+ HT20
+ SM Power Save disabled
+ RX HT20 SGI
+ TX STBC
+ RX STBC 1-stream
+ Max AMSDU length: 7935 bytes
+ DSSS/CCK HT40
+ Maximum RX AMPDU length 65535 bytes (exponent: 0x003)
+ Minimum RX AMPDU time spacing: 4 usec (0x05)
+ HT RX MCS rate indexes supported: 0-23
+ HT TX MCS rate indexes are undefined
+ HT operation:
+ * primary channel: 6
+ * secondary channel offset: no secondary
+ * STA channel width: 20 MHz
+ * RIFS: 1
+ * HT protection: nonmember
+ * non-GF present: 1
+ * OBSS non-GF present: 1
+ * dual beacon: 0
+ * dual CTS protection: 0
+ * STBC beacon: 0
+ * L-SIG TXOP Prot: 0
+ * PCO active: 0
+ * PCO phase: 0
+ Overlapping BSS scan params:
+ * passive dwell: 20 TUs
+ * active dwell: 10 TUs
+ * channel width trigger scan interval: 300 s
+ * scan passive total per channel: 200 TUs
+ * scan active total per channel: 20 TUs
+ * BSS width channel transition delay factor: 5
+ * OBSS Scan Activity Threshold: 0.25 %
+ Extended capabilities: HT Information Exchange Supported, Extended Channel Switching, BSS Transition, 6
+ WMM: * Parameter version 1
+ * u-APSD
+ * BE: CW 15-1023, AIFSN 3
+ * BK: CW 15-1023, AIFSN 7
+ * VI: CW 7-15, AIFSN 2, TXOP 3008 usec
+ * VO: CW 3-7, AIFSN 2, TXOP 1504 usec
+"""
+
+
+# pylint: disable=unused-argument,protected-access
+def fake_scan(*args, **kwargs):
+ return SCAN_OUTPUT
+iw._scan = fake_scan
+
+
+@wvtest.wvtest
+def find_bssids_test():
+ """Test iw.find_bssids."""
+ short_scan_result = iw.BssInfo(ssid='short scan result',
+ bssid='00:23:97:57:f4:d8',
+ security=['WEP'],
+ vendor_ies=[('00:11:22', '01 23 45 67')])
+
+ with_ie, without_ie = iw.find_bssids('wcli0', lambda o, d: o == '00:11:22',
+ True)
+
+ wvtest.WVPASSEQ(with_ie, set([short_scan_result]))
+
+ wvtest.WVPASSEQ(
+ without_ie,
+ set([iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41'),
+ iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1'),
+ iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61'),
+ iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:36:40',
+ security=['WPA2']),
+ iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:3a:e0',
+ security=['WPA2']),
+ iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:35:60',
+ security=['WPA2']),
+ iw.BssInfo(ssid='Google', bssid='94:b4:0f:f1:02:a0',
+ security=['WPA2'])]))
+
+ with_ie, without_ie = iw.find_bssids('wcli0', lambda o, d: o == '00:11:22',
+ False)
+ wvtest.WVPASSEQ(with_ie, set())
+ wvtest.WVPASSEQ(
+ without_ie,
+ set([iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:36:41'),
+ iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:3a:e1'),
+ iw.BssInfo(ssid='GoogleGuest', bssid='94:b4:0f:f1:35:61')]))
+
+if __name__ == '__main__':
+ wvtest.wvtest_main()
diff --git a/conman/main.py b/conman/main.py
new file mode 100755
index 0000000..3b81bfd
--- /dev/null
+++ b/conman/main.py
@@ -0,0 +1,27 @@
+#!/usr/bin/python
+
+"""Runs a ConnectionManager."""
+
+import logging
+import os
+import sys
+
+import connection_manager
+
+STATUS_DIR = '/tmp/conman'
+
+if __name__ == '__main__':
+ loglevel = logging.INFO
+ if '--debug' in sys.argv:
+ loglevel = logging.DEBUG
+ logging.basicConfig(level=loglevel)
+ logging.debug('Debug logging enabled.')
+
+ sys.stdout = os.fdopen(1, 'w', 1) # force line buffering even if redirected
+ sys.stderr = os.fdopen(2, 'w', 1) # force line buffering even if redirected
+
+ if not os.path.exists(STATUS_DIR):
+ os.makedirs(STATUS_DIR)
+
+ c = connection_manager.ConnectionManager(status_dir=STATUS_DIR)
+ c.run()
diff --git a/conman/test/fail b/conman/test/fail
new file mode 100755
index 0000000..2bb8d86
--- /dev/null
+++ b/conman/test/fail
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 1
diff --git a/conman/test/fake_wpactrl/wpactrl/__init__.py b/conman/test/fake_wpactrl/wpactrl/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/conman/test/fake_wpactrl/wpactrl/__init__.py
diff --git a/conman/test/restricted b/conman/test/restricted
new file mode 100755
index 0000000..9bd8fc3
--- /dev/null
+++ b/conman/test/restricted
@@ -0,0 +1,4 @@
+#!/bin/sh
+
+echo "$@" | grep -q -- "-a"
+
diff --git a/conman/test/succeed b/conman/test/succeed
new file mode 100755
index 0000000..c52d3c2
--- /dev/null
+++ b/conman/test/succeed
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 0
diff --git a/conman/wvtest b/conman/wvtest
new file mode 120000
index 0000000..75927a5
--- /dev/null
+++ b/conman/wvtest
@@ -0,0 +1 @@
+../cmds/wvtest
\ No newline at end of file