Merge "gfch100: set power off to 125C"
diff --git a/cache_warming/cache_warming.py b/cache_warming/cache_warming.py
new file mode 100644
index 0000000..7b76fc3
--- /dev/null
+++ b/cache_warming/cache_warming.py
@@ -0,0 +1,169 @@
+#!/usr/bin/python
+"""Updates hit log and periodically fetches top requested hosts.
+
+Loads previousy saved top requested hosts. Reads DNS queries
+through socket and updates hit log dictionary with most recent
+hit time and hit count for each host. Periodically sorts hosts
+in hit log by number of hits, fetches a predetermined number of
+the top requested hosts, and saves fetched hosts.
+"""
+
+import argparse
+from datetime import datetime
+import json
+import os
+import socket
+import dns.resolver
+
+hit_log = {}
+last_fetch = datetime.min
+TOP_N = 50
+FETCH_INTERVAL = 60 # seconds
+UDP_SERVER_PATH = '/tmp/dns_query_log_socket'
+HOSTS_JSON_PATH = '/config/cache_warming_hosts.json'
+
+
+def save_hosts(log):
+ """Saves predetermined number of top requested hosts in json file.
+
+ Stores dictionary with host key and tuple value containing most recent hit
+ time and hit count.
+
+ Args:
+ log: Dictionary of top requested hosts with host key and tuple value
+ containing most recent hit time and hit count.
+ """
+ d = os.path.dirname(HOSTS_JSON_PATH)
+ if not os.path.exists(d):
+ os.makedirs(d)
+ with open(HOSTS_JSON_PATH, 'w') as hosts_json:
+ json.dump(log, hosts_json)
+
+
+def load_hosts():
+ """Loads hosts stored in json file.
+
+ Loads dictionary with host key and tuple value containing most recent hit
+ time and hit count as hit_log if it exists.
+ """
+ if os.path.isfile(HOSTS_JSON_PATH):
+ with open(HOSTS_JSON_PATH, 'r') as hosts_json:
+ global hit_log
+ hit_log = json.load(hosts_json)
+
+
+def process_query(qry):
+ """Processes DNS query and updates hit log.
+
+ Parses DNS query and updates requested host's hit count and
+ most recent hit time in hit log.
+
+ Args:
+ qry: String representing a DNS query of the format
+ '[Unix time] [host name]'.
+ """
+ time, _, host = qry.partition(' ')
+ if host in hit_log:
+ hit_log[host] = (hit_log[host][0] + 1, time)
+ else:
+ hit_log[host] = (1, time)
+
+
+def hit_log_subset(hosts):
+ """Makes a subset of hit log containing selected hosts.
+
+ Args:
+ hosts: List of hosts to be included in the subset of hit log.
+
+ Returns:
+ Dictionary of selected hosts with host key and tuple value
+ containing most recent hit time and hit count.
+ """
+ return {k: hit_log[k] for k in hosts}
+
+
+def fetch(hosts, port, server):
+ """Fetches hosts and saves subset of hit log containing fetched hosts.
+
+ Only fetches predetermined number of top requested hosts. If fetch
+ fails, host is removed from hit log and saved hosts list.
+
+ Args:
+ hosts: List of hosts to be fetched sorted by number of hits
+ in descending order.
+ port: Port to which to send queries (default is 53).
+ server: Alternate nameservers to query (default is None).
+ """
+ my_resolver = dns.resolver.Resolver()
+ my_resolver.port = port
+ if server is not None:
+ my_resolver.nameservers = server
+
+ if len(hosts) > TOP_N:
+ hosts = hosts[:TOP_N]
+ for host in hosts:
+ try:
+ my_resolver.query(host)
+ except:
+ del hit_log[host]
+ hosts.remove(host)
+
+ save_hosts(hit_log_subset(hosts))
+
+
+def sort_hit_log():
+ """Sorts hosts in hit log by number of hits.
+
+ Returns:
+ A list of hosts sorted by number of hits in descending order.
+ """
+ return sorted(hit_log, key=hit_log.get, reverse=True)
+
+
+def warm_cache():
+ """Warms cache with predetermined number of most requested hosts.
+
+ Sorts hosts in hit log by hit count, fetches predetermined
+ number of top requested hosts, updates last fetch time.
+ """
+ sorted_hosts = sort_hit_log()
+ fetch(sorted_hosts, args.port, args.server)
+ global last_fetch
+ last_fetch = datetime.now()
+
+
+def set_args():
+ """Sets arguments for script.
+
+ Returns:
+ List of arguments containing port and server.
+ """
+ parser = argparse.ArgumentParser()
+ parser.add_argument('-p', '--port', nargs='?', default=53, type=int,
+ help='port to which to send queries (default is 53).')
+ parser.add_argument('-s', '--server', nargs='*', type=str,
+ help='alternate nameservers to query (default is None).')
+ return parser.parse_args()
+
+
+if __name__ == '__main__':
+ args = set_args()
+ load_hosts()
+
+ server_address = UDP_SERVER_PATH
+ try:
+ os.remove(server_address)
+ except OSError:
+ if os.path.exists(server_address):
+ raise
+ sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
+ sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ sock.bind(server_address)
+ os.chmod(server_address, 0o777)
+
+ while 1:
+ diff = datetime.now() - last_fetch
+ if diff.total_seconds() > 60:
+ warm_cache()
+ data = sock.recv(128)
+ process_query(data)
diff --git a/cache_warming/cache_warming_test.py b/cache_warming/cache_warming_test.py
new file mode 100644
index 0000000..83ff1e8
--- /dev/null
+++ b/cache_warming/cache_warming_test.py
@@ -0,0 +1,75 @@
+#!/usr/bin/python
+"""Tests for cache_warming.py."""
+
+import cache_warming
+from wvtest import wvtest
+
+
+@wvtest.wvtest
+def testProcessQuery_firstHit():
+ qry = '123456789 www.yahoo.com'
+ expected = {'www.yahoo.com': (1, '123456789')}
+ cache_warming.hit_log = {}
+ cache_warming.process_query(qry)
+ actual = cache_warming.hit_log
+ wvtest.WVPASSEQ(actual, expected)
+
+
+@wvtest.wvtest
+def testProcessQuery_updateHitCount():
+ qry = '123456789 www.yahoo.com'
+ cache_warming.hit_log = {'www.yahoo.com': (1, '123456789')}
+ cache_warming.process_query(qry)
+ expected = 2
+ actual = cache_warming.hit_log['www.yahoo.com'][0]
+ wvtest.WVPASSEQ(actual, expected)
+
+
+@wvtest.wvtest
+def testProcessQuery_updateRecentHitTime():
+ qry = '123456789 www.yahoo.com'
+ cache_warming.hit_log = {'www.yahoo.com': (1, '987654321')}
+ cache_warming.process_query(qry)
+ expected = '123456789'
+ actual = cache_warming.hit_log['www.yahoo.com'][1]
+ wvtest.WVPASSEQ(actual, expected)
+
+
+@wvtest.wvtest
+def testSortHitLog_empty():
+ cache_warming.hit_log = {}
+ expected = []
+ actual = cache_warming.sort_hit_log()
+ wvtest.WVPASSEQ(actual, expected)
+
+
+@wvtest.wvtest
+def testSortHitLog_nonEmpty():
+ cache_warming.hit_log = {
+ 'www.google.com': (2, '123456789'),
+ 'www.yahoo.com': (1, '987654321'),
+ 'www.espn.com': (3, '135792468')
+ }
+ expected = ['www.espn.com', 'www.google.com', 'www.yahoo.com']
+ actual = cache_warming.sort_hit_log()
+ wvtest.WVPASSEQ(actual, expected)
+
+@wvtest.wvtest
+def testHitLogSubset():
+ hosts = ['www.google.com', 'www.yahoo.com']
+ cache_warming.hit_log = cache_warming.hit_log = {
+ 'www.youtube.com': (4, '987654321'),
+ 'www.google.com': (1, '987654321'),
+ 'www.espn.com': (3, '123456789'),
+ 'www.yahoo.com': (2, '135792468')
+ }
+ expected = {
+ 'www.yahoo.com': (2, '135792468'),
+ 'www.google.com': (1, '987654321')
+ }
+ actual = cache_warming.hit_log_subset(hosts)
+ wvtest.WVPASSEQ(actual, expected)
+
+
+if __name__ == '__main__':
+ wvtest.wvtest_main()
diff --git a/cache_warming/fetch_popular.py b/cache_warming/fetch_popular.py
deleted file mode 100644
index fe0f2b4..0000000
--- a/cache_warming/fetch_popular.py
+++ /dev/null
@@ -1,65 +0,0 @@
-#!/usr/bin/python
-"""Pre-fetches top requested hosts.
-
-Sorts dictionary represented in hit_log.json by number of hits
-and sends DNS requests to a predetermined number of the top hosts.
-"""
-
-import argparse
-import json
-import dns.resolver
-
-TOP_N = 50
-HITS_LOG_JSON_PATH = '/tmp/hits_log.json'
-
-
-def sort_hits_log(path):
- """Sorts hosts in hits log by number of hits.
-
- Args:
- path: Path of JSON representation of dictionary mapping host
- to tuple of most recent hit time and hit count.
-
- Returns:
- A list of hosts sorted by number of hits in descending order.
- """
- try:
- log_json = open(path, 'r')
- except IOError:
- print 'unable to open ' + path
- raise
- else:
- log = json.load(log_json)
- return sorted(log, key=log.get, reverse=True)
-
-
-def prefetch(hosts, port, server):
- """Pre-fetches list of hosts.
-
- Args:
- hosts: List of hosts to be fetched sorted by number of hits
- in descending order.
- port: Port to which to send queries (default is 53).
- server: Alternate nameservers to query (default is None).
- """
- my_resolver = dns.resolver.Resolver()
- my_resolver.port = port
- if server is not None:
- my_resolver.nameservers = server
-
- if len(hosts) > TOP_N:
- hosts = hosts[:TOP_N]
- for host in hosts:
- my_resolver.query(host)
-
-
-if __name__ == '__main__':
- parser = argparse.ArgumentParser()
- parser.add_argument('-p', '--port', nargs='?', default=53, type=int,
- help='port to which to send queries (default is 53).')
- parser.add_argument('-s', '--server', nargs='*', type=str,
- help='alternate nameservers to query (default is None).')
- args = parser.parse_args()
-
- sorted_log = sort_hits_log(HITS_LOG_JSON_PATH)
- prefetch(sorted_log, args.port, args.server)
diff --git a/cache_warming/fetch_popular_test.py b/cache_warming/fetch_popular_test.py
deleted file mode 100644
index ea9da8d..0000000
--- a/cache_warming/fetch_popular_test.py
+++ /dev/null
@@ -1,39 +0,0 @@
-#!/usr/bin/python
-"""Tests for fetch_popular.py."""
-
-import os
-import fetch_popular
-from wvtest import wvtest
-
-
-@wvtest.wvtest
-def testSortHitsLog_empty():
- try:
- file_name = 'test_log.json'
- with open(file_name, 'w') as f:
- f.write('{}')
-
- expected = []
- actual = fetch_popular.sort_hits_log(file_name)
- wvtest.WVPASSEQ(actual, expected)
- finally:
- os.remove(file_name)
-
-
-@wvtest.wvtest
-def testSortHitsLog_nonEmpty():
- try:
- file_name = 'test_log.json'
- with open(file_name, 'w') as f:
- f.write('{"www.google.com": [2, "123456789"], "www.yahoo.com":'
- ' [1,"987654321"], "www.espn.com": [3, "135792468"]}')
-
- expected = ['www.espn.com', 'www.google.com', 'www.yahoo.com']
- actual = fetch_popular.sort_hits_log(file_name)
- wvtest.WVPASSEQ(actual, expected)
- finally:
- os.remove(file_name)
-
-
-if __name__ == '__main__':
- wvtest.wvtest_main()
diff --git a/cache_warming/log_hits.py b/cache_warming/log_hits.py
deleted file mode 100644
index 5fbc2e8..0000000
--- a/cache_warming/log_hits.py
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/python
-"""Updates most recent hit time and hit count for hosts in hits log.
-
-Reads queries from dns_query_log.txt and updates hosts in hits log
-dictionary with most recent hit time and hit count for each host.
-Saves hits log dictionary as hits_log.json for future modification.
-"""
-
-import json
-import os.path
-
-DNS_QUERY_LOG_PATH = '/tmp/dns_query_log.txt'
-HITS_LOG_JSON_PATH = '/tmp/hits_log.json'
-
-
-def process_line(log, ln):
- """Processes a line of DNS query log and updates hits log.
-
- Parses line and updates most recent hit time and hit count
- for host in hits log.
-
- Args:
- log: Dictionary mapping host to tuple of hit count and most
- recent hit time.
- ln: String representing a line of DNS query log of the
- format '[Unix time] [host name]'.
-
- Returns:
- An updated dictionary mapping host to tuple of hit count and
- most recent hit time.
- """
- time, _, host = ln[:-1].partition(' ')
- if host in log:
- log[host] = (log[host][0] + 1, time)
- else:
- log[host] = (1, time)
- return log
-
-
-def read_dns_query_log(path):
- """Reads a DNS query log.
-
- Processes each line of file, updating a hits log.
-
- Args:
- path: Path of DNS query log to be read.
-
- Returns:
- An updated dictionary mapping host to tuple of hit count and
- most recent hit time.
- """
- try:
- dns_query_log = open(path, 'r')
- except IOError:
- print 'unable to open ' + path
- else:
- log = {}
- for line in dns_query_log:
- log = process_line(log, line)
- dns_query_log.close()
- return log
-
-
-def clear_dns_query_log(path):
- """Clears a DNS query log.
-
- Opens file for write without writing anything.
-
- Args:
- path: Path of DNS query log to be cleared.
- """
- try:
- open(path, 'w').close()
- return
- except IOError:
- print 'unable to open ' + path
-
-
-def merge_logs(log, hist):
- """Merges two hit logs.
-
- Merges smaller hit log to larger hit log. Uses most recent hit
- time and sums hit count from each log for each host.
-
- Args:
- log: Dictionary mapping host to tuple of hit count and
- most recent hit time.
- hist: Similar dictionary representing previous query history.
-
- Returns:
- An updated dictionary mapping host to tuple of hit count and
- most recent hit time.
- """
- hist_larger = len(hist) > len(log)
- big_log, small_log = (hist, log) if hist_larger else (log, hist)
- for k, v in small_log.iteritems():
- if k in big_log:
- time = log[k][1]
- big_log[k] = (big_log[k][0] + v[0], time)
- else:
- big_log[k] = (v[0], v[1])
- return big_log
-
-
-if __name__ == '__main__':
- hit_log = read_dns_query_log(DNS_QUERY_LOG_PATH)
- clear_dns_query_log(DNS_QUERY_LOG_PATH)
- if os.path.isfile(HITS_LOG_JSON_PATH):
- hist_json = open(HITS_LOG_JSON_PATH, 'r')
- hit_log_hist = json.load(hist_json)
- hist_json.close()
-
- hist_json = open(HITS_LOG_JSON_PATH, 'w')
- json.dump(merge_logs(hit_log, hit_log_hist), hist_json)
- hist_json.close()
- else:
- try:
- hist_json = open(HITS_LOG_JSON_PATH, 'w')
- except IOError:
- print 'unable to open ' + HITS_LOG_JSON_PATH
- raise
- else:
- json.dump(hit_log, hist_json)
- hist_json.close()
diff --git a/cache_warming/log_hits_test.py b/cache_warming/log_hits_test.py
deleted file mode 100644
index 26944ce..0000000
--- a/cache_warming/log_hits_test.py
+++ /dev/null
@@ -1,169 +0,0 @@
-#!/usr/bin/python
-"""Tests for log_hits.py."""
-
-import os
-import log_hits
-from wvtest import wvtest
-
-
-@wvtest.wvtest
-def testProcessLine_firstHit():
- line = '123456789 www.yahoo.com\n'
- expected = {'www.yahoo.com': (1, '123456789')}
- actual = log_hits.process_line({}, line)
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testProcessLine_updateHitCount():
- line = '123456789 www.yahoo.com\n'
- log = {'www.yahoo.com': (1, '123456789')}
- expected = 2
- actual = log_hits.process_line(log, line)['www.yahoo.com'][0]
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testProcessLine_updateRecentHitTime():
- line = '123456789 www.yahoo.com\n'
- log = {'www.yahoo.com': (1, '987654321')}
- expected = '123456789'
- actual = log_hits.process_line(log, line)['www.yahoo.com'][1]
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_emptyLog():
- hist = {'www.yahoo.com': (1, '123456789')}
- expected = hist
- actual = log_hits.merge_logs({}, hist)
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_emptyHist():
- log = {'www.yahoo.com': (1, '123456789')}
- expected = log
- actual = log_hits.merge_logs(log, {})
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_bothEmpty():
- expected = {}
- actual = log_hits.merge_logs({}, {})
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_noOverlap():
- log = {'www.yahoo.com': (1, '123456789')}
- hist = {'www.google.com': (1, '123456789')}
- expected = {
- 'www.yahoo.com': (1, '123456789'),
- 'www.google.com': (1, '123456789')
- }
- actual = log_hits.merge_logs(log, hist)
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_updateHitCount():
- log = {'www.yahoo.com': (1, '987654321')}
- hist = {'www.yahoo.com': (1, '123456789')}
- expected = 2
- actual = log_hits.merge_logs(log, hist)['www.yahoo.com'][0]
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_updateRecentHitTime():
- log = {'www.yahoo.com': (1, '987654321')}
- hist = {'www.yahoo.com': (1, '123456789')}
- expected = '987654321'
- actual = log_hits.merge_logs(log, hist)['www.yahoo.com'][1]
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_histLargerNoOverlap():
- log = {'www.yahoo.com': (1, '123456789')}
- hist = {
- 'www.google.com': (1, '123456789'),
- 'www.espn.com': (1, '123456789')
- }
- expected = {
- 'www.yahoo.com': (1, '123456789'),
- 'www.google.com': (1, '123456789'),
- 'www.espn.com': (1, '123456789')
- }
- actual = log_hits.merge_logs(log, hist)
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_histLargerUpdateHitCount():
- log = {'www.yahoo.com': (1, '987654321')}
- hist = {
- 'www.yahoo.com': (1, '123456789'),
- 'www.google.com': (1, '123456789')
- }
- expected = 2
- actual = log_hits.merge_logs(log, hist)['www.yahoo.com'][0]
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testMergeLogs_histLargerUpdateRecentHitTime():
- log = {'www.yahoo.com': (1, '987654321')}
- hist = {
- 'www.yahoo.com': (1, '123456789'),
- 'www.google.com': (1, '123456789')
- }
- expected = '987654321'
- actual = log_hits.merge_logs(log, hist)['www.yahoo.com'][1]
- wvtest.WVPASSEQ(actual, expected)
-
-
-@wvtest.wvtest
-def testReadDNSQueryLog_empty():
- file_name = 'test_log.txt'
- open(file_name, 'w').close()
- expected = {}
- actual = log_hits.read_dns_query_log(file_name)
- wvtest.WVPASSEQ(actual, expected)
- os.remove(file_name)
-
-
-@wvtest.wvtest
-def testReadDNSQueryLog_nonEmpty():
- file_name = 'test_log.txt'
- f = open(file_name, 'w')
- f.write('123456789 www.yahoo.com\n987654321 www.google.com\n'
- '135792468 www.yahoo.com\n')
- f.close()
- expected = {
- 'www.yahoo.com': (2, '135792468'),
- 'www.google.com': (1, '987654321')
- }
- actual = log_hits.read_dns_query_log(file_name)
- wvtest.WVPASSEQ(actual, expected)
- os.remove(file_name)
-
-
-@wvtest.wvtest
-def testClearDNSQueryLog():
- file_name = 'test_log.txt'
- f = open(file_name, 'w')
- f.write('testing clear_dns_query_log()\n')
- f.close()
-
- log_hits.clear_dns_query_log(file_name)
- expected = 0
- actual = os.stat(file_name).st_size
- wvtest.WVPASSEQ(actual, expected)
- os.remove(file_name)
-
-
-if __name__ == '__main__':
- wvtest.wvtest_main()
diff --git a/cache_warming/warm_cache b/cache_warming/warm_cache
deleted file mode 100644
index fa334d0..0000000
--- a/cache_warming/warm_cache
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/bin/sh
-
-# Periodically processes logged DNS queries and prefetches
-# prefetches the most requested hosts, warming the cache.
-
-while sleep 60; do
- python log_hits.py
- python fetch_popular.py
-done
diff --git a/conman/connection_manager.py b/conman/connection_manager.py
index cbb3b5b..2ab4763 100755
--- a/conman/connection_manager.py
+++ b/conman/connection_manager.py
@@ -798,11 +798,11 @@
return ''
-def _get_quantenna_interface():
+def _get_quantenna_interfaces():
try:
- return subprocess.check_output(['get-quantenna-interface']).strip()
+ return subprocess.check_output(['get-quantenna-interfaces']).split()
except subprocess.CalledProcessError:
- logging.fatal('Failed to call get-quantenna-interface')
+ logging.fatal('Failed to call get-quantenna-interfaces')
raise
@@ -812,6 +812,7 @@
Returns:
A dict mapping wireless client interfaces to their supported bands.
"""
+ # TODO(mikemu): Use wifi_files instead of "wifi show".
current_band = None
result = collections.defaultdict(lambda: collections.defaultdict(set))
@@ -821,10 +822,9 @@
elif line.startswith('Client Interface:'):
result[line.split()[2]]['bands'].add(current_band)
- # TODO(rofrankel): Make 'wifi show' (or wifi_files) include this information
- # so we don't need a subprocess call to check.
- quantenna_interface = _get_quantenna_interface()
- if quantenna_interface in result:
- result[quantenna_interface]['frenzy'] = True
+ for quantenna_interface in _get_quantenna_interfaces():
+ if quantenna_interface.startswith('wcli'):
+ result[quantenna_interface]['bands'].add('5')
+ result[quantenna_interface]['frenzy'] = True
return result
diff --git a/conman/connection_manager_test.py b/conman/connection_manager_test.py
index d96022e..e651cb1 100755
--- a/conman/connection_manager_test.py
+++ b/conman/connection_manager_test.py
@@ -106,22 +106,12 @@
Band: 5
RegDomain: 00
-Interface: wlan0 # 5 GHz ap
-AutoChannel: False
-Station List for band: 5
-
-Client Interface: wlan1 # 5 GHz client
"""
WIFI_SHOW_OUTPUT_FRENZY = """Band: 2.4
RegDomain: 00
Band: 5
RegDomain: 00
-Interface: wlan0 # 5 GHz ap
-AutoChannel: False
-Station List for band: 5
-
-Client Interface: wlan0 # 5 GHz client
"""
IW_SCAN_DEFAULT_OUTPUT = """BSS 00:11:22:33:44:55(on wcli0)
@@ -141,14 +131,22 @@
@wvtest.wvtest
def get_client_interfaces_test():
"""Test get_client_interfaces."""
+ wifi_show = None
+ quantenna_interfaces = None
+
# pylint: disable=protected-access
- original_wifi_show = connection_manager._wifi_show
- original_get_quantenna_interface = connection_manager._get_quantenna_interface
- connection_manager._get_quantenna_interface = lambda: ''
- connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_MARVELL8897
+ old_wifi_show = connection_manager._wifi_show
+ old_get_quantenna_interfaces = connection_manager._get_quantenna_interfaces
+ connection_manager._wifi_show = lambda: wifi_show
+ connection_manager._get_quantenna_interfaces = lambda: quantenna_interfaces
+
+ wifi_show = WIFI_SHOW_OUTPUT_MARVELL8897
+ quantenna_interfaces = []
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
{'wcli0': {'bands': set(['2.4', '5'])}})
- connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_ATH9K_ATH10K
+
+ wifi_show = WIFI_SHOW_OUTPUT_ATH9K_ATH10K
+ quantenna_interfaces = []
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
'wcli0': {'bands': set(['2.4'])},
'wcli1': {'bands': set(['5'])}
@@ -157,21 +155,21 @@
# Test Quantenna devices.
# 2.4 GHz cfg80211 radio + 5 GHz Frenzy (Optimus Prime).
- connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_ATH9K_FRENZY
- connection_manager._get_quantenna_interface = lambda: 'wlan1'
+ wifi_show = WIFI_SHOW_OUTPUT_ATH9K_FRENZY
+ quantenna_interfaces = ['wlan1', 'wlan1_portal', 'wcli1']
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(), {
'wcli0': {'bands': set(['2.4'])},
- 'wlan1': {'frenzy': True, 'bands': set(['5'])}
+ 'wcli1': {'frenzy': True, 'bands': set(['5'])}
})
# Only Frenzy (e.g. Lockdown).
- connection_manager._wifi_show = lambda: WIFI_SHOW_OUTPUT_FRENZY
- connection_manager._get_quantenna_interface = lambda: 'wlan0'
+ wifi_show = WIFI_SHOW_OUTPUT_FRENZY
+ quantenna_interfaces = ['wlan0', 'wlan0_portal', 'wcli0']
wvtest.WVPASSEQ(connection_manager.get_client_interfaces(),
- {'wlan0': {'frenzy': True, 'bands': set(['5'])}})
+ {'wcli0': {'frenzy': True, 'bands': set(['5'])}})
- connection_manager._wifi_show = original_wifi_show
- connection_manager._get_quantenna_interface = original_get_quantenna_interface
+ connection_manager._wifi_show = old_wifi_show
+ connection_manager._get_quantenna_interfaces = old_get_quantenna_interfaces
class WLANConfiguration(connection_manager.WLANConfiguration):
@@ -495,7 +493,7 @@
def connection_manager_test(radio_config, wlan_configs=None,
- quantenna_interface='', **cm_kwargs):
+ quantenna_interfaces=None, **cm_kwargs):
"""Returns a decorator that does ConnectionManager test boilerplate."""
if wlan_configs is None:
wlan_configs = {}
@@ -510,11 +508,12 @@
wifi_scan_period_s = run_duration_s * wifi_scan_period
# pylint: disable=protected-access
- original_wifi_show = connection_manager._wifi_show
+ old_wifi_show = connection_manager._wifi_show
connection_manager._wifi_show = lambda: radio_config
- original_gqi = connection_manager._get_quantenna_interface
- connection_manager._get_quantenna_interface = lambda: quantenna_interface
+ old_gqi = connection_manager._get_quantenna_interfaces
+ connection_manager._get_quantenna_interfaces = (
+ lambda: quantenna_interfaces or [])
try:
# No initial state.
@@ -523,7 +522,6 @@
os.mkdir(os.path.join(tmp_dir, 'interfaces'))
moca_tmp_dir = tempfile.mkdtemp()
wpa_control_interface = tempfile.mkdtemp()
- FrenzyWifi.WPACtrl.WIFIINFO_PATH = tempfile.mkdtemp()
for band, access_point in wlan_configs.iteritems():
write_wlan_config(config_dir, band, 'initial ssid', 'initial psk')
@@ -551,10 +549,9 @@
shutil.rmtree(config_dir)
shutil.rmtree(moca_tmp_dir)
shutil.rmtree(wpa_control_interface)
- shutil.rmtree(FrenzyWifi.WPACtrl.WIFIINFO_PATH)
# pylint: disable=protected-access
- connection_manager._wifi_show = original_wifi_show
- connection_manager._get_quantenna_interface = original_gqi
+ connection_manager._wifi_show = old_wifi_show
+ connection_manager._get_quantenna_interfaces = old_gqi
actual_test.func_name = f.func_name
return actual_test
@@ -853,21 +850,24 @@
@wvtest.wvtest
@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
- quantenna_interface='wlan1')
+ quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
+ )
def connection_manager_test_generic_ath9k_frenzy_2g(c):
connection_manager_test_generic(c, '2.4')
@wvtest.wvtest
@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
- quantenna_interface='wlan1')
+ quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
+ )
def connection_manager_test_generic_ath9k_frenzy_5g(c):
connection_manager_test_generic(c, '5')
@wvtest.wvtest
@connection_manager_test(WIFI_SHOW_OUTPUT_FRENZY,
- quantenna_interface='wlan0')
+ quantenna_interfaces=['wlan0', 'wlan0_portal', 'wcli0']
+ )
def connection_manager_test_generic_frenzy_5g(c):
connection_manager_test_generic(c, '5')
@@ -970,7 +970,8 @@
@wvtest.wvtest
@connection_manager_test(WIFI_SHOW_OUTPUT_ATH9K_FRENZY,
- quantenna_interface='wlan1')
+ quantenna_interfaces=['wlan1', 'wlan1_portal', 'wcli1']
+ )
def connection_manager_test_dual_band_two_radios_ath9k_frenzy(c):
connection_manager_test_dual_band_two_radios(c)
diff --git a/presterastats/prestera_periodic.py b/presterastats/prestera_periodic.py
index 91f146e..82f56e6 100755
--- a/presterastats/prestera_periodic.py
+++ b/presterastats/prestera_periodic.py
@@ -18,6 +18,7 @@
__author__ = 'poist@google.com (Gregory Poist)'
import errno
+import json
import os
import subprocess
import sys
@@ -44,6 +45,38 @@
self.interval = interval
self.ports_output_file = os.path.join(self.OUTPUT_DIR, 'ports.json')
+ def _FlatObject(self, base, obj, out):
+ """Open json object to a list of strings.
+
+ Args:
+ base: is a string that has a base name for a key.
+ obj: is a JSON object that will be flatten.
+ out: is an output where strings will be appended.
+
+ Example:
+ data = {"a": 1, "b": { "c": 2, "d": 3}}
+ out = []
+ _FlatObject('', data, out)
+
+ out will be equal to
+ [u'/a=1', u'/b/c=2', u'/b/d=3']
+ """
+ for k, v in obj.items():
+ name = base + '/' + k
+ if isinstance(v, dict):
+ self._FlatObject(name, v, out)
+ else:
+ val = '%s=' % name
+ out.append(val + str(v))
+
+ def _PrintStats(self, port_stats):
+ json_data = json.loads(port_stats)
+ flat_data = []
+ self._FlatObject('', json_data, flat_data)
+ for s in flat_data:
+ sys.stderr.write('%s\n' % s)
+ sys.stderr.flush()
+
def WriteToStderr(self, msg):
"""Write a message to stderr."""
@@ -62,10 +95,14 @@
ports_stats = ''
try:
ports_stats = self.RunPresteraStats()
+ if ports_stats:
+ self._PrintStats(ports_stats)
except OSError as ex:
self.WriteToStderr('Failed to run presterastats: %s\n' % ex)
except subprocess.CalledProcessError as ex:
self.WriteToStderr('presterastats exited non-zero: %s\n' % ex)
+ except ValueError as ex:
+ self.WriteToStderr('Failed to parse presterastats output: %s\n' % ex)
if not ports_stats:
self.WriteToStderr('Failed to get data from presterastats\n')
diff --git a/presterastats/prestera_periodic_test.py b/presterastats/prestera_periodic_test.py
index 10be8e4..c31f9b5 100644
--- a/presterastats/prestera_periodic_test.py
+++ b/presterastats/prestera_periodic_test.py
@@ -124,6 +124,13 @@
output = ''.join(line for line in f)
self.assertEqual('', output)
+ def testFlatObject(self):
+ obj = {'key1': 1, 'key2': {'key3': 3, 'key4': 4}}
+ got = []
+ self.periodic._FlatObject('base', obj, got)
+ want = ['base/key1=1', 'base/key2/key3=3', 'base/key2/key4=4']
+ self.assertEqual(got.sort(), want.sort())
+
if __name__ == '__main__':
unittest.main()
diff --git a/taxonomy/dhcp.py b/taxonomy/dhcp.py
index 19b7cd6..0354ebd 100644
--- a/taxonomy/dhcp.py
+++ b/taxonomy/dhcp.py
@@ -74,7 +74,7 @@
'1,28,2,3,15,6,12': ['tivo'],
- '1,3,6,12,15,28,42': ['viziotv', 'wemo'],
+ '1,3,6,12,15,28,42': ['viziotv', 'wemo', 'directv'],
'1,3,6,12,15,28,40,41,42': ['viziotv', 'kindle'],
'1,3,6,15,28,33': ['wii'],
diff --git a/taxonomy/ethernet.py b/taxonomy/ethernet.py
index 223d228..dc5a7e1 100644
--- a/taxonomy/ethernet.py
+++ b/taxonomy/ethernet.py
@@ -78,6 +78,7 @@
'64:a7:69': ['htc'],
'7c:61:93': ['htc'],
'80:01:84': ['htc'],
+ '80:7a:bf': ['htc'],
'84:7a:88': ['htc'],
'90:e7:c4': ['htc'],
'a0:f4:50': ['htc'],
@@ -211,6 +212,7 @@
'54:88:0e': ['samsung'],
'5c:0a:5b': ['samsung'],
'5c:f6:dc': ['samsung'],
+ '60:af:6d': ['samsung'],
'6c:2f:2c': ['samsung'],
'6c:83:36': ['samsung'],
'78:40:e4': ['samsung'],
@@ -240,17 +242,20 @@
'b4:79:a7': ['samsung'],
'b8:57:d8': ['samsung'],
'b8:5a:73': ['samsung'],
+ 'b8:5e:7b': ['samsung'],
'bc:20:a4': ['samsung'],
'bc:72:b1': ['samsung'],
'bc:8c:cd': ['samsung'],
'bc:e6:3f': ['samsung'],
'c0:bd:d1': ['samsung'],
'c4:42:02': ['samsung'],
+ 'c4:57:6e': ['samsung'],
'c4:73:1e': ['samsung'],
'cc:07:ab': ['samsung'],
'cc:3a:61': ['samsung'],
'd0:22:be': ['samsung'],
'd0:59:e4': ['samsung'],
+ 'd8:90:e8': ['samsung'],
'e0:99:71': ['samsung'],
'e0:db:10': ['samsung'],
'e4:12:1d': ['samsung'],
@@ -272,6 +277,8 @@
'58:48:22': ['sony'],
'b4:52:7e': ['sony'],
+ 'a4:8d:3b': ['vizio'],
+
'00:24:e4': ['withings'],
'64:cc:2e': ['xiaomi'],
diff --git a/taxonomy/pcaptest.py b/taxonomy/pcaptest.py
index 97873b2..2b864e1 100644
--- a/taxonomy/pcaptest.py
+++ b/taxonomy/pcaptest.py
@@ -68,8 +68,8 @@
('Moto G or Moto X', './testdata/pcaps/Moto X 2.4GHz Specific.pcap'),
('Moto G or Moto X', './testdata/pcaps/Moto X 2.4GHz.pcap'),
('Nest Thermostat v1 or v2', './testdata/pcaps/Nest Thermostat 2.4GHz.pcap'),
- ('Roku 3 or Streaming Stick', './testdata/pcaps/Roku 3 2.4GHz 4230.pcap'),
- ('Roku 3 or Streaming Stick', './testdata/pcaps/Roku 3 5GHz 4230.pcap'),
+ ('Roku 2 or 3 or Streaming Stick', './testdata/pcaps/Roku 3 2.4GHz 4230.pcap'),
+ ('Roku 2 or 3 or Streaming Stick', './testdata/pcaps/Roku 3 5GHz 4230.pcap'),
('Samsung Galaxy Note or S2+', './testdata/pcaps/Samsung Galaxy S2+ 5GHz.pcap'),
('Samsung Galaxy Note or S2+', './testdata/pcaps/Samsung Galaxy Note 5GHz.pcap'),
('Samsung Galaxy S2 or Infuse', './testdata/pcaps/Samsung Galaxy S2 2.4GHz.pcap'),
diff --git a/taxonomy/testdata/dhcp.leases b/taxonomy/testdata/dhcp.leases
index 7bbbc5f..acd0ecc 100644
--- a/taxonomy/testdata/dhcp.leases
+++ b/taxonomy/testdata/dhcp.leases
@@ -51,3 +51,6 @@
1432237016 34:c8:03:89:d3:e8 192.168.42.40 Nokia-Lumia-920
1432237016 14:91:82:07:c7:ed 192.168.42.41 WeMo
1432237016 08:05:81:c5:1f:31 192.168.42.42 Roku3
+1432237016 5c:93:a2:00:00:00 192.168.42.43 Playstation 4
+1432237016 e0:c7:67:00:00:00 192.168.42.44 iPhoone SE
+1432237016 a4:8d:3b:00:00:00 192.168.42.45 VizioSmartTV
diff --git a/taxonomy/testdata/dhcp.signatures b/taxonomy/testdata/dhcp.signatures
index 0849bff..33abcb0 100644
--- a/taxonomy/testdata/dhcp.signatures
+++ b/taxonomy/testdata/dhcp.signatures
@@ -43,3 +43,6 @@
34:c8:03:89:d3:e8 1,15,3,6,44,46,47,31,33,121,249,252,43
14:91:82:07:c7:ed 1,3,6,12,15,28,42
08:05:81:c5:1f:31 1,3,6,15,12
+5c:93:a2:00:00:00 1,3,15,6
+e0:c7:67:00:00:00 1,3,6,15,119,252
+a4:8d:3b:00:00:00 1,3,6,12,15,28,42
diff --git a/taxonomy/testdata/pcaps/Amazon Kindle Keyboard 3 2.4GHz B0006.pcap b/taxonomy/testdata/pcaps/Amazon Kindle Keyboard 3 2.4GHz B0006.pcap
new file mode 100644
index 0000000..2321e03
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Amazon Kindle Keyboard 3 2.4GHz B0006.pcap
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Paperwhite B024.pcap" "b/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Paperwhite B024.pcap"
new file mode 100644
index 0000000..42bd35c
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Paperwhite B024.pcap"
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B013.pcap" "b/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B013.pcap"
new file mode 100644
index 0000000..e91be02
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B013.pcap"
Binary files differ
diff --git "a/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B054.pcap" "b/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B054.pcap"
new file mode 100644
index 0000000..05d88e1
--- /dev/null
+++ "b/taxonomy/testdata/pcaps/Amazon Kindle Voyage or Paperwhite \0502012\051 2.4GHz Voyage B054.pcap"
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Playstation 4 2.4GHz CUH-1115A.pcap b/taxonomy/testdata/pcaps/Playstation 4 2.4GHz CUH-1115A.pcap
new file mode 100644
index 0000000..9f6a313
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Playstation 4 2.4GHz CUH-1115A.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Vizio SmartCast TV 5GHz.pcap b/taxonomy/testdata/pcaps/Vizio SmartCast TV 5GHz.pcap
new file mode 100644
index 0000000..dc2e7f4
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Vizio SmartCast TV 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Vizio Tablet XR6P 2.4GHz.pcap b/taxonomy/testdata/pcaps/Vizio Tablet XR6P 2.4GHz.pcap
new file mode 100644
index 0000000..42c966c
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Vizio Tablet XR6P 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/Vizio Tablet XR6P 5GHz.pcap b/taxonomy/testdata/pcaps/Vizio Tablet XR6P 5GHz.pcap
new file mode 100644
index 0000000..5621e4a
--- /dev/null
+++ b/taxonomy/testdata/pcaps/Vizio Tablet XR6P 5GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone SE 2.4GHz.pcap b/taxonomy/testdata/pcaps/iPhone SE 2.4GHz.pcap
new file mode 100644
index 0000000..5319d03
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone SE 2.4GHz.pcap
Binary files differ
diff --git a/taxonomy/testdata/pcaps/iPhone SE 5GHz.pcap b/taxonomy/testdata/pcaps/iPhone SE 5GHz.pcap
new file mode 100644
index 0000000..37c62a6
--- /dev/null
+++ b/taxonomy/testdata/pcaps/iPhone SE 5GHz.pcap
Binary files differ
diff --git a/taxonomy/wifi.py b/taxonomy/wifi.py
index 58b6ead..f5dfed5 100644
--- a/taxonomy/wifi.py
+++ b/taxonomy/wifi.py
@@ -62,8 +62,12 @@
('Amazon Kindle', '', '5GHz'),
'wifi4|probe:0,1,50|assoc:0,1,50,48,221(0050f2,2)|oui:amazon':
('Amazon Kindle', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50|assoc:0,1,50,221(0050f2,2)|oui:amazon':
+ ('Amazon Kindle', 'Keyboard 3', '2.4GHz'),
'wifi4|probe:0,1,50,45,htcap:01ac,htagg:02,htmcs:0000ffff|assoc:0,1,50,48,221(0050f2,2),45,127,htcap:01ac,htagg:02,htmcs:0000ffff,extcap:01|oui:amazon':
('Amazon Kindle', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,45,htcap:002c,htagg:01,htmcs:000000ff|assoc:0,1,50,45,48,221(0050f2,2),htcap:002c,htagg:01,htmcs:000000ff|oui:amazon':
+ ('Amazon Kindle', 'Voyage or Paperwhite (2012)', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:1130,htagg:18,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),htcap:1130,htagg:18,htmcs:000000ff|oui:amazon':
('Amazon Kindle', 'Fire 7" (2011 edition)', '2.4GHz'),
@@ -105,6 +109,8 @@
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1502,extcap:0000000000000040|name:appletv':
('Apple TV', '4th gen', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff|os:ios':
+ ('Apple Watch', '', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(001018,2),221(00904c,51),221(0050f2,8),htcap:0020,htagg:1a,htmcs:000000ff,extcap:00000804|assoc:0,1,48,50,45,70,221(001018,2),221(00904c,51),221(0050f2,2),htcap:0020,htagg:1a,htmcs:000000ff|os:ios':
('Apple Watch', '', '2.4GHz'),
@@ -160,6 +166,11 @@
'wifi4|probe:0,1,3,45,50,127,191,htcap:0062,htagg:03,htmcs:00000000,vhtcap:33c07030,vhtrxmcs:0124fffc,vhttxmcs:0124fffc,extcap:0000000000000040|assoc:0,1,48,50,127,221(0050f2,2),45,htcap:002c,htagg:03,htmcs:000000ff,extcap:0000000000000140|oui:google':
('Chromecast', 'v2', '2.4GHz'),
+ 'wifi4|probe:0,1,45,221(001018,2),221(00904c,51),htcap:007c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:007c,htagg:1a,htmcs:0000ffff,txpow:1408|os:directv':
+ ('DirecTV', 'HR44 or HD54', '5GHz'),
+ 'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:107c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:107c,htagg:1a,htmcs:0000ffff,txpow:1608|os:directv':
+ ('DirecTV', 'HR44 or HF54', '2.4GHz'),
+
'wifi4|probe:0,1,45,htcap:106e,htagg:01,htmcs:000000ff|assoc:0,1,45,33,36,48,221(0050f2,2),htcap:106e,htagg:01,htmcs:000000ff,txpow:0e00|oui:dropcam':
('Dropcam', '', '5GHz'),
'wifi4|probe:0,1,50,45,htcap:002c,htagg:01,htmcs:000000ff|assoc:0,1,50,45,48,221(0050f2,2),htcap:002c,htagg:01,htmcs:000000ff|oui:dropcam':
@@ -235,6 +246,8 @@
'wifi4|probe:0,1,50,3,45,127,221(506f9a,16),221(0050f2,8),221(001018,2),htcap:1063,htagg:17,htmcs:000000ff,extcap:0000088001400040|assoc:0,1,50,33,36,45,127,107,221(001018,2),221(0050f2,2),htcap:1063,htagg:17,htmcs:000000ff,txpow:1309,extcap:000008800140|oui:htc':
('HTC One', 'M9', '2.4GHz'),
+ 'wifi4|probe:0,1,45,127,191,221(0050f2,4),221(506f9a,10),221(506f9a,9),221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:000008800140,wps:0PJA2|assoc:0,1,33,36,48,45,127,191,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805832,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:e009,extcap:000008800140':
+ ('HTC One', 'M9, Sprint edition', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,221(0050f2,4),221(506f9a,9),221(506f9a,16),221(0050f2,8),221(001018,2),htcap:1063,htagg:17,htmcs:000000ff,extcap:000008800140,wps:0PJA2|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:1063,htagg:17,htmcs:000000ff,txpow:1309,extcap:000008800140':
('HTC One', 'M9, Sprint edition', '2.4GHz'),
@@ -378,10 +391,14 @@
('iPhone 6', '', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(00904c,51),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1302,extcap:0000000000000040|os:ios':
('iPhone 6', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:0000ffff,txpow:1202,extcap:0400000000000040|os:ios':
+ ('iPhone 6', '', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
('iPhone 6+', '', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
('iPhone 6+', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(00904c,51),221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
+ ('iPhone 6+', '', '2.4GHz'),
'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0400088400000040|assoc:0,1,33,36,48,70,45,127,191,221(001018,2),221(0050f2,2),htcap:006f,htagg:17,htmcs:0000ffff,vhtcap:0f815832,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002,extcap:0400000000000040|os:ios':
('iPhone 6s/6s+', '', '5GHz'),
@@ -420,6 +437,11 @@
'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:002d,htagg:17,htmcs:0000ffff,extcap:0400088400000040|assoc:0,1,50,33,36,48,70,45,127,221(001018,2),221(0050f2,2),htcap:002d,htagg:17,htmcs:000000ff,txpow:1202,extcap:0000000000000040|os:ios':
('iPhone 6s/6s+', '', '2.4GHz'),
+ 'wifi4|probe:0,1,45,127,107,191,221(0050f2,8),221(001018,2),htcap:0063,htagg:17,htmcs:000000ff,vhtcap:0f805032,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:0400088400000040|assoc:0,1,33,36,45,127,221(001018,2),221(0050f2,2),htcap:0063,htagg:17,htmcs:000000ff,txpow:e002,extcap:000008|os:ios':
+ ('iPhone SE', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,127,107,221(0050f2,8),221(001018,2),htcap:0021,htagg:17,htmcs:000000ff,extcap:0400088400000040|assoc:0,1,50,33,36,45,127,221(001018,2),221(0050f2,2),htcap:0021,htagg:17,htmcs:000000ff,txpow:1402,extcap:0000000000000040|os:ios':
+ ('iPhone SE', '', '2.4GHz'),
+
'wifi4|probe:0,1,3,50|assoc:0,1,48,50|os:ipodtouch1':
('iPod Touch', '1st or 2nd gen', '2.4GHz'),
@@ -447,6 +469,8 @@
'wifi4|probe:0,1,45,221(0050f2,8),191,127,107,221(506f9a,16),htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31800120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:000000800040|assoc:0,1,33,36,48,45,221(0050f2,2),191,127,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:170d,extcap:00000a8201400040|oui:lg':
('LG G3', '', '5GHz'),
+ 'wifi4|probe:0,1,45,221(0050f2,8),191,127,107,221(506f9a,16),htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,extcap:000000800040|assoc:0,1,33,36,48,45,221(0050f2,2),191,127,htcap:016e,htagg:03,htmcs:000000ff,vhtcap:31805120,vhtrxmcs:0000fffe,vhttxmcs:0000fffe,txpow:170d,extcap:00000a8201400040|oui:lg':
+ ('LG G3', '', '5GHz'),
'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(506f9a,16),htcap:012c,htagg:03,htmcs:000000ff,extcap:000000800040|assoc:0,1,50,48,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a8201400000|oui:lg':
('LG G3', '', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,221(0050f2,8),127,107,221(506f9a,16),htcap:012c,htagg:03,htmcs:000000ff,extcap:000000800040|assoc:0,1,50,33,48,70,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a8201400000|oui:lg':
@@ -464,6 +488,8 @@
'wifi4|probe:0,1,50,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:LGMS323|assoc:0,1,50,48,45,221(0050f2,2),221(004096,3),htcap:012c,htagg:03,htmcs:000000ff':
('LG Optimus', 'L70', '2.4GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:012c,htagg:03,htmcs:000000ff,wps:LG_D415|assoc:0,1,50,45,221(0050f2,2),127,htcap:012c,htagg:03,htmcs:000000ff,extcap:00000a0200000000':
+ ('LG Optimus', 'L90', '2.4GHz'),
'wifi4|probe:0,1,3,45,221(0050f2,8),221(0050f2,4),221(506f9a,9),htcap:016e,htagg:03,htmcs:000000ff,wps:LG_V400|assoc:0,1,33,36,48,70,45,221(0050f2,2),127,htcap:016e,htagg:03,htmcs:000000ff,txpow:170d,extcap:00000a0200000000':
('LG Pad', 'v400', '5GHz'),
@@ -720,12 +746,16 @@
('Playstation', '4', '2.4GHz'),
'wifi4|probe:0,1,3,50|assoc:0,1,33,48,50,221(0050f2,2),45,htcap:010c,htagg:03,htmcs:0000ffff,txpow:0f06|os:playstation':
('Playstation', '4', '2.4GHz'),
+ 'wifi4|probe:0,1,3,50|assoc:0,1,221(0050f2,2),45,htcap:012c,htagg:03,htmcs:0000ffff|os:playstation':
+ ('Playstation', '4', '2.4GHz'),
'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:RCT6303W87DK|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
('RCA Viking Tablet', 'RCA 10 Viking Pro', '2.4GHz'),
'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:RCT6773W42B|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
('RCA Voyager Tablet', 'v1', '2.4GHz'),
+ 'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:RCT6773W42B|assoc:0,1,50,45,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
+ ('RCA Voyager Tablet', 'v1', '2.4GHz'),
# RCA Voyager-II
'wifi4|probe:0,1,50,221(0050f2,4),221(506f9a,9),wps:RCT6773W22B|assoc:0,1,50,45,48,127,221(0050f2,2),htcap:1172,htagg:03,htmcs:000000ff,extcap:01':
@@ -741,7 +771,17 @@
# Roku Streaming Stick model 3400X
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:187c,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:187c,htagg:1a,htmcs:0000ffff,txpow:1208|os:roku':
- ('Roku', 'Streaming Stick', '2.4GHz'),
+ ('Roku', 'Streaming Stick 3400X', '2.4GHz'),
+
+ # Roku Streaming Stick model 3600
+ 'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:19bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,50,45,127,221(001018,2),221(0050f2,2),htcap:19bc,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
+ ('Roku', 'Streaming Stick 3600', '2.4GHz'),
+
+ # Roku TV NP-YW
+ 'wifi4|probe:0,1,45,127,191,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,199,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1109,extcap:0000000000000040|os:roku':
+ ('Roku TV', '', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff|assoc:0,1,50,33,36,48,45,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,txpow:1209|os:roku':
+ ('Roku TV', '', '2.4GHz'),
# Roku 1 models 2000, 2050, 2100, and XD
'wifi4|probe:0,1,50,45,221(001018,2),221(00904c,51),htcap:186e,htagg:1a,htmcs:0000ffff|assoc:0,1,33,36,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:186e,htagg:1a,htmcs:0000ffff,txpow:1308|os:roku':
@@ -759,15 +799,15 @@
'wifi4|probe:0,1,50,45,3,221(001018,2),221(00904c,51),htcap:187c,htagg:1a,htmcs:0000ffff|assoc:0,1,48,50,45,221(001018,2),221(00904c,51),221(0050f2,2),htcap:187c,htagg:1a,htmcs:0000ffff|os:roku':
('Roku', '2', '2.4GHz'),
- # Roku 3 model 4230, 4200, 4200X, 4230X, Roku Streaming Stick model 3500
+ # Roku 3 model 4230, 4200, 4200X, 4230X, Roku Streaming Stick model 3500, Roku 2 model 4210X
'wifi4|probe:0,1,45,127,221(001018,2),221(00904c,51),htcap:09bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:09bc,htagg:16,htmcs:0000ffff,txpow:100a,extcap:0000000000000040|os:roku':
- ('Roku', '3 or Streaming Stick', '5GHz'),
+ ('Roku', '2 or 3 or Streaming Stick', '5GHz'),
'wifi4|probe:0,1,3,45,127,221(001018,2),221(00904c,51),htcap:09bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,221(001018,2),221(0050f2,2),htcap:09bc,htagg:16,htmcs:0000ffff,txpow:100a,extcap:0000000000000040|os:roku':
- ('Roku', '3 or Streaming Stick', '5GHz'),
+ ('Roku', '2 or 3 or Streaming Stick', '5GHz'),
'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:19bc,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:19bc,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
- ('Roku', '3 or Streaming Stick', '2.4GHz'),
+ ('Roku', '2 or 3 or Streaming Stick', '2.4GHz'),
'wifi4|probe:0,1,50,3,45,127,221(001018,2),221(00904c,51),htcap:193c,htagg:16,htmcs:0000ffff,extcap:0000000000000040|assoc:0,1,33,36,48,50,45,127,221(001018,2),221(0050f2,2),htcap:193c,htagg:16,htmcs:0000ffff,txpow:140a,extcap:0000000000000040|os:roku':
- ('Roku', '3 or Streaming Stick', '2.4GHz'),
+ ('Roku', '2 or 3 or Streaming Stick', '2.4GHz'),
# Roku 4 model 4400
'wifi4|probe:0,1,45,127,191,221(001018,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,extcap:0000000000000040|assoc:0,1,33,36,48,45,127,191,199,221(001018,2),221(0050f2,2),htcap:01ad,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:1109,extcap:0000000000000040|os:roku':
@@ -999,17 +1039,26 @@
'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:tivo':
('TiVo', 'Series3 or Series4', '2.4GHz'),
+ 'wifi4|probe:0,1,45,191,221(0050f2,4),221(506f9a,9),221(001018,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,wps:_|assoc:0,1,33,36,48,45,191,221(001018,2),221(0050f2,2),htcap:01ef,htagg:17,htmcs:0000ffff,vhtcap:0f8159b2,vhtrxmcs:0000fffa,vhttxmcs:0000fffa,txpow:e002|oui:vizio':
+ ('Vizio SmartCast TV', '', '2.4GHz'),
'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:12,htmcs:000000ff,extcap:01000000|os:viziotv':
('Vizio Smart TV', '', '2.4GHz'),
'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:13,htmcs:000000ff,extcap:01|os:viziotv':
('Vizio Smart TV', '', '2.4GHz'),
'wifi4|probe:0,1,50,45,127,221(0050f2,4),htcap:106e,htagg:12,htmcs:000000ff,extcap:00,wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:12,htmcs:000000ff,extcap:01000000|os:viziotv':
('Vizio Smart TV', '', '2.4GHz'),
+ 'wifi4|probe:0,1,50,45,127,221(0050f2,4),htcap:106e,htagg:13,htmcs:000000ff,extcap:00,wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,127,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:13,htmcs:000000ff,extcap:01|os:viziotv':
+ ('Vizio Smart TV', '', '2.4GHz'),
'wifi4|probe:0,1,50,221(0050f2,4),wps:Ralink_Wireless_Linux_Client|assoc:0,1,50,45,221(000c43,6),221(0050f2,2),48,htcap:000c,htagg:13,htmcs:000000ff|os:viziotv':
('Vizio Smart TV', '', '2.4GHz'),
'wifi4|probe:0,1,50,48|assoc:0,1,50,221(0050f2,2),45,51,127,48,htcap:012c,htagg:1b,htmcs:000000ff,extcap:01|os:viziotv':
('Vizio Smart TV', '', '2.4GHz'),
+ 'wifi4|probe:0,1,3,45,221(0050f2,8),htcap:016e,htagg:03,htmcs:000000ff|assoc:0,1,33,36,45,221(0050f2,2),htcap:016e,htagg:03,htmcs:000000ff,txpow:110d|oui:vizio':
+ ('Vizio Tablet', 'XR6P', '5GHz'),
+ 'wifi4|probe:0,1,50,3,45,221(0050f2,8),htcap:012c,htagg:03,htmcs:000000ff|assoc:0,1,50,48,45,221(0050f2,2),htcap:012c,htagg:03,htmcs:000000ff|oui:vizio':
+ ('Vizio Tablet', 'XR6P', '2.4GHz'),
+
'wifi4|probe:0,1,50,221(001018,2)|assoc:0,1,48,50,221(001018,2)|os:wii':
('Wii', '', '2.4GHz'),
@@ -1191,9 +1240,10 @@
if __name__ == '__main__':
for k, v in database.iteritems():
- print 'SHA:' + hashlib.sha256(k).hexdigest() + ' ' + v[1]
+ name = v[0] + ' ' + v[1]
+ print 'SHA:' + hashlib.sha256(k).hexdigest() + ' ' + name
# Remove os, oui, etc qualifiers if present.
a = k.split('|')
if len(a) > 3:
sig = '|'.join(a[0:3])
- print 'SHA:' + hashlib.sha256(sig).hexdigest() + ' ' + v[1] + ' (unqualified)'
+ print 'SHA:' + hashlib.sha256(sig).hexdigest() + ' ' + name + ' (unqualified)'
diff --git a/wifi/quantenna.py b/wifi/quantenna.py
index 33378ba..755fb8b 100755
--- a/wifi/quantenna.py
+++ b/wifi/quantenna.py
@@ -2,161 +2,56 @@
"""Wifi commands for Quantenna using QCSAPI."""
-import json
import os
+import re
import subprocess
import time
import utils
-WIFIINFO_PATH = '/tmp/wifi/wifiinfo'
-
-
-ALREADY_MEMBER_FMT = ('device %s is already a member of a bridge; '
- "can't enslave it to bridge %s.")
-NOT_MEMBER_FMT = 'device %s is not a slave of %s'
-
-
-def _get_interface():
- return subprocess.check_output(['get-quantenna-interface']).strip()
-
-
-def _get_mac_address(interface):
- try:
- var = {'wlan0': 'MAC_ADDR_WIFI', 'wlan1': 'MAC_ADDR_WIFI2'}[interface]
- except KeyError:
- raise utils.BinWifiException('no MAC address for %s in hnvram' % interface)
- return subprocess.check_output(['hnvram', '-rq', var]).strip()
+def _get_quantenna_interfaces():
+ return subprocess.check_output(['get-quantenna-interfaces']).split()
def _qcsapi(*args):
return subprocess.check_output(['qcsapi'] + [str(x) for x in args]).strip()
-def _brctl(*args):
- return subprocess.check_output(['brctl'] + list(args),
- stderr=subprocess.STDOUT).strip()
+def _get_external_mac(hif):
+ # The MAC of the LHOST interface is equal to the MAC of the host interface
+ # with the locally administered bit cleared.
+ mac = utils.get_mac_address_for_interface(hif)
+ octets = mac.split(':')
+ octets[0] = '%02x' % (int(octets[0], 16) & ~(1 << 1))
+ return ':'.join(octets)
-def _ifplugd_action(*args):
- return subprocess.check_output(['/etc/ifplugd/ifplugd.action'] + list(args),
- stderr=subprocess.STDOUT).strip()
+def _get_vlan(hif):
+ m = re.search(r'VID: (\d+)', utils.read_or_empty('/proc/net/vlan/%s' % hif))
+ if m:
+ return int(m.group(1))
+ raise utils.BinWifiException('no VLAN ID for interface %s' % hif)
-def info_parsed(interface):
- """Fake version of iw.info_parsed."""
- wifiinfo_filename = os.path.join(WIFIINFO_PATH, interface)
-
- if not os.path.exists(wifiinfo_filename):
- return {}
-
- wifiinfo = json.load(open(wifiinfo_filename))
- return {'addr' if k == 'BSSID' else k.lower(): v
- for k, v in wifiinfo.iteritems()}
+def _get_interface(mode, suffix):
+ # Each host interface (hif) maps to exactly one LHOST interface (lif) based on
+ # the VLAN ID as follows: the lif is wifiX where X is the VLAN ID - 2 (VLAN
+ # IDs start at 2). The client interface must map to wifi0, so it must have
+ # VLAN ID 2.
+ prefix = 'wlan' if mode == 'ap' else 'wcli'
+ suffix = '_' + suffix if suffix else ''
+ for hif in _get_quantenna_interfaces():
+ if re.match(prefix + r'\d*' + suffix, hif):
+ vlan = _get_vlan(hif)
+ lif = 'wifi%d' % (vlan - 2)
+ mac = _get_external_mac(hif)
+ return hif, lif, mac, vlan
+ return None, None, None, None
-def _set_interface_in_bridge(bridge, interface, want_in_bridge):
- """Add/remove Quantenna interface from/to the bridge."""
- if want_in_bridge:
- command = 'addif'
- error_fmt = ALREADY_MEMBER_FMT
- else:
- command = 'delif'
- error_fmt = NOT_MEMBER_FMT
-
- try:
- _brctl(command, bridge, interface)
- except subprocess.CalledProcessError as e:
- if error_fmt % (interface, bridge) not in e.output:
- raise utils.BinWifiException(e.output)
-
-
-def _set(mode, opt):
- """Enable wifi."""
- interface = _get_interface()
- if not interface:
- return False
-
- if opt.encryption == 'WEP':
- raise utils.BinWifiException('WEP not supported')
-
- if mode == 'scan':
- mode = 'sta'
- scan = True
- else:
- scan = False
-
- _qcsapi('rfenable', 0)
- _qcsapi('restore_default_config', 'noreboot')
-
- config = {
- 'bw': opt.width if mode == 'ap' else 80,
- 'channel': 149 if opt.channel == 'auto' else opt.channel,
- 'mode': mode,
- 'pmf': 0,
- 'scs': 0,
- }
- for param, value in config.iteritems():
- _qcsapi('update_config_param', 'wifi0', param, value)
-
- _qcsapi('set_mac_addr', 'wifi0', _get_mac_address(interface))
-
- if int(_qcsapi('is_startprod_done')):
- _qcsapi('reload_in_mode', 'wifi0', mode)
- else:
- _qcsapi('startprod')
- for _ in xrange(30):
- if int(_qcsapi('is_startprod_done')):
- break
- time.sleep(1)
- else:
- raise utils.BinWifiException('startprod timed out')
-
- if mode == 'ap':
- _set_interface_in_bridge(opt.bridge, interface, True)
- _qcsapi('set_ssid', 'wifi0', opt.ssid)
- if opt.encryption == 'NONE':
- _qcsapi('set_beacon_type', 'wifi0', 'Basic')
- else:
- protocol, authentication, encryption = opt.encryption.split('_')
- protocol = {'WPA': 'WPA', 'WPA2': '11i', 'WPA12': 'WPAand11i'}[protocol]
- authentication += 'Authentication'
- encryption += 'Encryption'
- _qcsapi('set_beacon_type', 'wifi0', protocol)
- _qcsapi('set_wpa_authentication_mode', 'wifi0', authentication)
- _qcsapi('set_wpa_encryption_modes', 'wifi0', encryption)
- _qcsapi('set_passphrase', 'wifi0', 0, os.environ['WIFI_PSK'])
- _qcsapi('set_option', 'wifi0', 'ssid_broadcast', int(not opt.hidden_mode))
- _qcsapi('rfenable', 1)
- elif mode == 'sta' and not scan:
- _set_interface_in_bridge(opt.bridge, interface, False)
- _qcsapi('create_ssid', 'wifi0', opt.ssid)
- if opt.bssid:
- _qcsapi('set_ssid_bssid', 'wifi0', opt.ssid, opt.bssid)
- if opt.encryption == 'NONE' or not os.environ.get('WIFI_CLIENT_PSK'):
- _qcsapi('ssid_set_authentication_mode', 'wifi0', opt.ssid, 'NONE')
- else:
- _qcsapi('ssid_set_passphrase', 'wifi0', opt.ssid, 0,
- os.environ['WIFI_CLIENT_PSK'])
- # In STA mode, 'rfenable 1' is already done by 'startprod'/'reload_in_mode'.
- # 'apply_security_config' must be called instead.
- _qcsapi('apply_security_config', 'wifi0')
-
- for _ in xrange(10):
- if _qcsapi('get_ssid', 'wifi0'):
- break
- time.sleep(1)
- else:
- raise utils.BinWifiException('wpa_supplicant failed to connect')
-
- try:
- _ifplugd_action(interface, 'up')
- except subprocess.CalledProcessError:
- utils.log('Failed to call ifplugd.action. %s may not get an IP address.'
- % interface)
-
- return True
+def _set_link_state(hif, state):
+ subprocess.check_output(['ip', 'link', 'set', 'dev', hif, state])
def _parse_scan_result(line):
@@ -179,49 +74,167 @@
# The SSID may contain quotes and spaces. Split on whitespace from the right,
# making at most 10 splits, to preserve spaces in the SSID.
sp = line.strip().rsplit(None, 10)
- return sp[0][1:-1], sp[1], int(sp[2]), float(sp[3]), int(sp[4]), int(sp[5])
+ return sp[0][1:-1], sp[1], int(sp[2]), -float(sp[3]), int(sp[4]), int(sp[5])
+
+
+def _ensure_initialized(mode):
+ """Ensure that the device is in a state suitable for the given mode."""
+ if int(_qcsapi('is_startprod_done')):
+ if (mode == 'scan' or
+ mode == 'ap' and _qcsapi('get_mode', 'wifi0') == 'Access point' or
+ mode == 'sta' and _qcsapi('get_mode', 'wifi0') == 'Station'):
+ return
+ _qcsapi('restore_default_config', 'noreboot', mode)
+ _qcsapi('reload_in_mode', 'wifi0', mode)
+ _qcsapi('rfenable', 1)
+ else:
+ _qcsapi('restore_default_config', 'noreboot',
+ 'sta' if mode == 'scan' else mode)
+
+ _, _, mac, _ = _get_interface('sta', '')
+ if mac:
+ _qcsapi('set_mac_addr', 'wifi0', mac)
+
+ _qcsapi('startprod')
+ for _ in xrange(30):
+ if int(_qcsapi('is_startprod_done')):
+ break
+ time.sleep(1)
+ else:
+ raise utils.BinWifiException('startprod timed out')
+
+ _qcsapi('rfenable', 1)
def set_wifi(opt):
- return _set('ap', opt)
+ """Enable AP."""
+ hif, lif, mac, vlan = _get_interface('ap', opt.interface_suffix)
+ if not hif:
+ return False
+
+ if opt.encryption == 'WEP':
+ raise utils.BinWifiException('WEP not supported')
+
+ stop_ap_wifi(opt)
+
+ try:
+ _ensure_initialized('ap')
+
+ _qcsapi('set_bw', 'wifi0', opt.width)
+ _qcsapi('set_channel', 'wifi0',
+ 149 if opt.channel == 'auto' else opt.channel)
+
+ _qcsapi('wifi_create_bss', lif, mac)
+ _qcsapi('set_ssid', lif, opt.ssid)
+ if opt.encryption == 'NONE':
+ _qcsapi('set_beacon_type', lif, 'Basic')
+ else:
+ protocol, authentication, encryption = opt.encryption.split('_')
+ protocol = {'WPA': 'WPA', 'WPA2': '11i', 'WPA12': 'WPAand11i'}[protocol]
+ authentication += 'Authentication'
+ encryption += 'Encryption'
+ _qcsapi('set_beacon_type', lif, protocol)
+ _qcsapi('set_wpa_authentication_mode', lif, authentication)
+ _qcsapi('set_wpa_encryption_modes', lif, encryption)
+ _qcsapi('set_passphrase', lif, 0, os.environ['WIFI_PSK'])
+ _qcsapi('set_option', lif, 'ssid_broadcast', int(not opt.hidden_mode))
+
+ _qcsapi('vlan_config', lif, 'enable')
+ _qcsapi('vlan_config', lif, 'access', vlan)
+ _qcsapi('vlan_config', 'pcie0', 'enable')
+ _qcsapi('vlan_config', 'pcie0', 'trunk', vlan)
+
+ _qcsapi('block_bss', lif, 0)
+ _set_link_state(hif, 'up')
+ except:
+ stop_ap_wifi(opt)
+ raise
+
+ return True
def set_client_wifi(opt):
- return _set('sta', opt)
+ """Enable client."""
+ hif, lif, _, vlan = _get_interface('sta', opt.interface_suffix)
+ if not hif:
+ return False
+
+ stop_client_wifi(opt)
+
+ try:
+ _ensure_initialized('sta')
+
+ _qcsapi('set_bw', 'wifi0', 80)
+
+ _qcsapi('create_ssid', lif, opt.ssid)
+ if opt.bssid:
+ _qcsapi('set_ssid_bssid', lif, opt.ssid, opt.bssid)
+ if opt.encryption == 'NONE' or not os.environ.get('WIFI_CLIENT_PSK'):
+ _qcsapi('ssid_set_authentication_mode', lif, opt.ssid, 'NONE')
+ else:
+ _qcsapi('ssid_set_passphrase', lif, opt.ssid, 0,
+ os.environ['WIFI_CLIENT_PSK'])
+ _qcsapi('apply_security_config', lif)
+
+ for _ in xrange(10):
+ if _qcsapi('get_ssid', lif):
+ break
+ time.sleep(1)
+ else:
+ raise utils.BinWifiException('wpa_supplicant failed to connect')
+
+ _qcsapi('vlan_config', lif, 'enable')
+ _qcsapi('vlan_config', lif, 'access', vlan)
+ _qcsapi('vlan_config', 'pcie0', 'enable')
+ _qcsapi('vlan_config', 'pcie0', 'trunk', vlan)
+
+ _set_link_state(hif, 'up')
+ except:
+ stop_client_wifi(opt)
+ raise
+
+ return True
-def stop_ap_wifi(_):
+def stop_ap_wifi(opt):
"""Disable AP."""
- if not _get_interface():
+ hif, lif, _, _ = _get_interface('ap', opt.interface_suffix)
+ if not hif:
return False
- if (int(_qcsapi('is_startprod_done')) and
- _qcsapi('get_mode', 'wifi0') == 'Access point'):
- _qcsapi('rfenable', 0)
+ try:
+ _qcsapi('wifi_remove_bss', lif)
+ except subprocess.CalledProcessError:
+ pass
+
+ _set_link_state(hif, 'down')
return True
-def stop_client_wifi(_):
+def stop_client_wifi(opt):
"""Disable client."""
- if not _get_interface():
+ hif, lif, _, _ = _get_interface('sta', opt.interface_suffix)
+ if not hif:
return False
- if (int(_qcsapi('is_startprod_done')) and
- _qcsapi('get_mode', 'wifi0') == 'Station'):
- _qcsapi('rfenable', 0)
+ try:
+ _qcsapi('remove_ssid', lif, _qcsapi('get_ssid_list', lif, 1))
+ except subprocess.CalledProcessError:
+ pass
+
+ _set_link_state(hif, 'down')
return True
-def scan_wifi(opt):
+def scan_wifi(_):
"""Scan for APs."""
- interface = _get_interface()
- if not interface:
+ hif, _, _, _ = _get_interface('ap', '')
+ if not hif:
return False
- if _qcsapi('rfstatus') == 'Off':
- _set('scan', opt)
+ _ensure_initialized('scan')
_qcsapi('start_scan', 'wifi0')
for _ in xrange(30):
@@ -229,14 +242,14 @@
break
time.sleep(1)
else:
- raise utils.BinWifiException('start_scan timed out')
+ raise utils.BinWifiException('scan timed out')
for i in xrange(int(_qcsapi('get_results_ap_scan', 'wifi0'))):
ssid, mac, channel, rssi, flags, protocols = _parse_scan_result(
_qcsapi('get_properties_ap', 'wifi0', i))
- print 'BSS %s(on %s)' % (mac, interface)
+ print 'BSS %s(on %s)' % (mac, hif)
print '\tfreq: %d' % (5000 + 5 * channel)
- print '\tsignal: %.2f' % -rssi
+ print '\tsignal: %.2f' % rssi
print '\tSSID: %s' % ssid
if flags & 0x1:
if protocols & 0x1:
diff --git a/wifi/quantenna_test.py b/wifi/quantenna_test.py
index ebef2a0..177f557 100755
--- a/wifi/quantenna_test.py
+++ b/wifi/quantenna_test.py
@@ -2,243 +2,53 @@
"""Tests for quantenna.py."""
-import json
-import os
-import shutil
-from subprocess import CalledProcessError
-import tempfile
-
-from configs_test import FakeOptDict
import quantenna
+import utils
from wvtest import wvtest
-calls = []
-ifplugd_action_calls = []
-
-
-def fake_qcsapi(*args):
- calls.append([str(x) for x in args])
- if args[0] == 'is_startprod_done':
- return '1' if ['startprod'] in calls else '0'
- if args[0] == 'get_ssid':
- return calls[matching_calls_indices(['set_ssid', 'create_ssid'])[-1]][1]
- if args[0] == 'get_mode':
- i = [c for c in matching_calls_indices(['update_config_param'])
- if calls[c][2] == 'mode']
- return 'Access point' if calls[i[-1]][3] == 'ap' else 'Station'
-
-
-bridge_interfaces = set()
-
-
-def fake_brctl(*args):
- bridge = args[-2]
- wvtest.WVPASS(bridge == 'br0')
- interface = args[-1]
- if 'addif' in args:
- if interface in bridge_interfaces:
- raise CalledProcessError(
- returncode=1, cmd=['brctl'] + list(args),
- output=quantenna.ALREADY_MEMBER_FMT % (interface, bridge))
- bridge_interfaces.add(interface)
- return
-
- if 'delif' in args:
- if interface not in bridge_interfaces:
- raise CalledProcessError(
- returncode=1, cmd=['brctl'] + list(args),
- output=quantenna.NOT_MEMBER_FMT % (interface, bridge))
- bridge_interfaces.remove(interface)
- return
-
-
-def set_fakes(interface='wlan1'):
- del calls[:]
- del ifplugd_action_calls[:]
- bridge_interfaces.clear()
- os.environ['WIFI_PSK'] = 'wifi_psk'
- os.environ['WIFI_CLIENT_PSK'] = 'wifi_client_psk'
- quantenna._get_interface = lambda: interface
- quantenna._get_mac_address = lambda _: '00:11:22:33:44:55'
- quantenna._qcsapi = fake_qcsapi
- quantenna._brctl = fake_brctl
- quantenna._ifplugd_action = lambda *args: ifplugd_action_calls.append(args)
-
-
-def matching_calls_indices(accept):
- return [i for i, c in enumerate(calls) if c[0] in accept]
+@wvtest.wvtest
+def get_external_mac_test():
+ old_get_mac_address_for_interface = utils.get_mac_address_for_interface
+ utils.get_mac_address_for_interface = lambda _: '02:00:00:00:00:00'
+ wvtest.WVPASSEQ(quantenna._get_external_mac('wlan0'), '00:00:00:00:00:00')
+ utils.get_mac_address_for_interface = old_get_mac_address_for_interface
@wvtest.wvtest
-def not_quantenna_test():
- opt = FakeOptDict()
- set_fakes(interface='')
- wvtest.WVFAIL(quantenna.set_wifi(opt))
- wvtest.WVFAIL(quantenna.set_client_wifi(opt))
- wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
- wvtest.WVFAIL(quantenna.stop_client_wifi(opt))
- wvtest.WVPASSEQ(calls, [])
- wvtest.WVFAIL(quantenna.set_wifi(opt))
- wvtest.WVFAIL(quantenna.set_client_wifi(opt))
- wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
- wvtest.WVFAIL(quantenna.stop_client_wifi(opt))
- wvtest.WVPASSEQ(calls, [])
- set_fakes(interface='')
- wvtest.WVFAIL(quantenna.set_wifi(opt))
- wvtest.WVFAIL(quantenna.set_client_wifi(opt))
- wvtest.WVFAIL(quantenna.stop_ap_wifi(opt))
- wvtest.WVFAIL(quantenna.stop_client_wifi(opt))
- wvtest.WVPASSEQ(calls, [])
+def get_vlan_test():
+ old_read_or_empty = utils.read_or_empty
+ utils.read_or_empty = lambda _: 'wlan0 VID: 3 REORDER_HDR: 1'
+ wvtest.WVPASSEQ(quantenna._get_vlan('wlan0'), 3)
+ utils.read_or_empty = lambda _: ''
+ wvtest.WVEXCEPT(utils.BinWifiException, quantenna._get_vlan, 'wlan0')
+ utils.read_or_empty = old_read_or_empty
@wvtest.wvtest
-def set_wifi_test():
- opt = FakeOptDict()
- opt.bridge = 'br0'
- set_fakes()
-
- # Run set_wifi for the first time.
- wvtest.WVPASS(quantenna.set_wifi(opt))
- wvtest.WVPASS('wlan1' in bridge_interfaces)
-
- # 'rfenable 0' must be run first so that a live interface is not being
- # modified.
- wvtest.WVPASSEQ(calls[0], ['rfenable', '0'])
-
- # 'restore_default_config noreboot' must be run before any configuration so
- # that old configuration is cleared.
- wvtest.WVPASSEQ(calls[1], ['restore_default_config', 'noreboot'])
-
- # Check that 'reload_in_mode' is not run.
- wvtest.WVPASS(['reload_in_mode', 'wifi0'] not in calls)
-
- # Check that configs are written.
- wvtest.WVPASS(['update_config_param', 'wifi0', 'bw', '20'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'channel', '149'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'mode', 'ap'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'pmf', '0'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'scs', '0'] in calls)
- wvtest.WVPASS(['set_mac_addr', 'wifi0', '00:11:22:33:44:55'] in calls)
- wvtest.WVPASS(['set_ssid', 'wifi0', 'TEST_SSID'] in calls)
- wvtest.WVPASS(['set_passphrase', 'wifi0', '0', 'wifi_psk'] in calls)
- wvtest.WVPASS(['set_option', 'wifi0', 'ssid_broadcast', '1'] in calls)
-
- # 'update_config_param' and 'set_mac_addr' must be run before 'startprod',
- # since 'startprod' runs scripts that read these configs.
- sp = calls.index(['startprod'])
- i = matching_calls_indices(['update_config_param', 'set_mac_addr'])
- wvtest.WVPASSLT(i[-1], sp)
-
- # 'startprod' must be followed by 'is_startprod_done'.
- wvtest.WVPASSEQ(calls[sp + 1], ['is_startprod_done'])
-
- # Other configs must be written after 'startprod' and before 'rfenable 1'.
- i = matching_calls_indices(['set_ssid', 'set_passphrase', 'set_option'])
- wvtest.WVPASSLT(sp, i[0])
- wvtest.WVPASSLT(i[-1], calls.index(['rfenable', '1']))
-
- # We shouldn't touch ifplugd in AP mode.
- wvtest.WVPASSEQ(len(ifplugd_action_calls), 0)
-
- # Run set_wifi again in client mode with new options.
- opt.channel = '147'
- opt.ssid = 'TEST_SSID2'
- opt.width = '80'
- new_calls_start = len(calls)
- wvtest.WVPASS(quantenna.set_client_wifi(opt))
- wvtest.WVFAIL('wlan1' in bridge_interfaces)
-
- # Clear old calls.
- del calls[:new_calls_start]
-
- # 'rfenable 0' must be run first so that a live interface is not being
- # modified.
- wvtest.WVPASSEQ(calls[0], ['rfenable', '0'])
-
- # 'restore_default_config noreboot' must be run before any configuration so
- # that old configuration is cleared.
- wvtest.WVPASSEQ(calls[1], ['restore_default_config', 'noreboot'])
-
- # Check that 'startprod' is not run.
- wvtest.WVPASS(['startprod'] not in calls)
-
- # Check that configs are written.
- wvtest.WVPASS(['update_config_param', 'wifi0', 'bw', '80'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'channel', '147'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'mode', 'sta'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'pmf', '0'] in calls)
- wvtest.WVPASS(['update_config_param', 'wifi0', 'scs', '0'] in calls)
- wvtest.WVPASS(['set_mac_addr', 'wifi0', '00:11:22:33:44:55'] in calls)
- wvtest.WVPASS(['create_ssid', 'wifi0', 'TEST_SSID2'] in calls)
- wvtest.WVPASS(['ssid_set_passphrase', 'wifi0', 'TEST_SSID2', '0',
- 'wifi_client_psk'] in calls)
-
- # 'update_config_param' and 'set_mac_addr' must be run before
- # 'reload_in_mode', since 'reload_in_mode' runs scripts that read these
- # configs.
- rim = calls.index(['reload_in_mode', 'wifi0', 'sta'])
- i = matching_calls_indices(['update_config_param', 'set_mac_addr'])
- wvtest.WVPASSLT(i[-1], rim)
-
- # Other configs must be written after 'reload_in_mode' and before
- # 'apply_security_config'.
- i = matching_calls_indices(['create_ssid', 'ssid_set_passphrase'])
- wvtest.WVPASSLT(rim, i[0])
- wvtest.WVPASSLT(i[-1], calls.index(['apply_security_config', 'wifi0']))
-
- # We should have called ipflugd.action after setclient.
- wvtest.WVPASSEQ(len(ifplugd_action_calls), 1)
- wvtest.WVPASSEQ(ifplugd_action_calls[0], ('wlan1', 'up'))
-
- # Make sure subsequent equivalent calls don't fail despite the redundant
- # bridge changes.
- wvtest.WVPASS(quantenna.set_client_wifi(opt))
- wvtest.WVPASS(quantenna.set_client_wifi(opt))
- wvtest.WVPASS(quantenna.set_wifi(opt))
- wvtest.WVPASS(quantenna.set_wifi(opt))
-
-
-@wvtest.wvtest
-def stop_wifi_test():
- opt = FakeOptDict()
- opt.bridge = 'br0'
- set_fakes()
- wvtest.WVPASS(quantenna.set_wifi(opt))
- new_calls_start = len(calls)
- wvtest.WVPASS(quantenna.stop_ap_wifi(opt))
- wvtest.WVPASS(['rfenable', '0'] in calls[new_calls_start:])
- new_calls_start = len(calls)
- wvtest.WVPASS(quantenna.stop_client_wifi(opt))
- wvtest.WVPASS(['rfenable', '0'] not in calls[new_calls_start:])
-
-
-@wvtest.wvtest
-def info_parsed_test():
- set_fakes()
-
- try:
- quantenna.WIFIINFO_PATH = tempfile.mkdtemp()
- json.dump({
- 'Channel': '64',
- 'SSID': 'my ssid',
- 'BSSID': '00:00:00:00:00:00',
- }, open(os.path.join(quantenna.WIFIINFO_PATH, 'wlan0'), 'w'))
-
- wvtest.WVPASSEQ(quantenna.info_parsed('wlan0'), {
- 'ssid': 'my ssid',
- 'addr': '00:00:00:00:00:00',
- 'channel': '64',
- })
- finally:
- shutil.rmtree(quantenna.WIFIINFO_PATH)
+def get_interface_test():
+ old_get_quantenna_interfaces = quantenna._get_quantenna_interfaces
+ old_get_external_mac = quantenna._get_external_mac
+ old_get_vlan = quantenna._get_vlan
+ quantenna._get_quantenna_interfaces = lambda: ['wlan0', 'wlan0_portal']
+ quantenna._get_external_mac = lambda _: '00:00:00:00:00:00'
+ quantenna._get_vlan = lambda _: 3
+ wvtest.WVPASSEQ(quantenna._get_interface('ap', ''),
+ ('wlan0', 'wifi1', '00:00:00:00:00:00', 3))
+ wvtest.WVPASSEQ(quantenna._get_interface('ap', 'portal'),
+ ('wlan0_portal', 'wifi1', '00:00:00:00:00:00', 3))
+ wvtest.WVPASSEQ(quantenna._get_interface('sta', ''),
+ (None, None, None, None))
+ quantenna._get_vlan = old_get_vlan
+ quantenna._get_external_mac = old_get_external_mac
+ quantenna._get_quantenna_interfaces = old_get_quantenna_interfaces
@wvtest.wvtest
def parse_scan_result_test():
result = ' " ssid with "quotes" " 00:11:22:33:44:55 40 25 0 0 0 0 0 1 40 '
wvtest.WVPASSEQ(quantenna._parse_scan_result(result),
- (' ssid with "quotes" ', '00:11:22:33:44:55', 40, 25, 0, 0))
+ (' ssid with "quotes" ', '00:11:22:33:44:55', 40, -25, 0, 0))
if __name__ == '__main__':
diff --git a/wifi/wifi.py b/wifi/wifi.py
index 7d0d301..b0ef7f9 100755
--- a/wifi/wifi.py
+++ b/wifi/wifi.py
@@ -459,7 +459,6 @@
True.
"""
for band in opt.band.split():
- frenzy = False
print('Band: %s' % band)
for tokens in utils.subprocess_line_tokens(('iw', 'reg', 'get')):
if len(tokens) >= 2 and tokens[0] == 'country':
@@ -471,20 +470,11 @@
interface = iw.find_interface_from_band(
band, interface_type, opt.interface_suffix)
if interface is None:
- if band == '5':
- interface = _get_quantenna_interface()
- if interface:
- frenzy = True
- if not interface:
- continue
-
+ continue
print('%sInterface: %s # %s GHz %s' %
- (prefix, interface, band, prefix.lower() or 'ap'))
+ (prefix, interface, band, 'client' if 'cli' in interface else 'ap'))
- if frenzy:
- info_parsed = quantenna.info_parsed(interface)
- else:
- info_parsed = iw.info_parsed(interface)
+ info_parsed = iw.info_parsed(interface)
for k, label in (('channel', 'Channel'),
('ssid', 'SSID'),
('addr', 'BSSID')):
@@ -511,13 +501,6 @@
return True
-def _get_quantenna_interface():
- try:
- return subprocess.check_output(['get-quantenna-interface']).strip()
- except subprocess.CalledProcessError as e:
- utils.log('Failed to call get-quantenna-interface: %s', e)
-
-
@iw.requires_iw
def scan_wifi(opt):
"""Prints 'iw scan' results.