Handle NoDeviceError when calling get_network_device
[wifi-radar.git] / wifiradar / connections.py
blobfa4515393fabd1f0356d4527e365f91a9a115b3d
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # connections.py - collection of classes for supporting WiFi connections
6 # Part of WiFi Radar: A utility for managing WiFi profiles on GNU/Linux.
8 # Copyright (C) 2004-2005 Ahmad Baitalmal <ahmad@baitalmal.com>
9 # Copyright (C) 2005 Nicolas Brouard <nicolas.brouard@mandrake.org>
10 # Copyright (C) 2005-2009 Brian Elliott Finley <brian@thefinleys.com>
11 # Copyright (C) 2006 David Decotigny <com.d2@free.fr>
12 # Copyright (C) 2006 Simon Gerber <gesimu@gmail.com>
13 # Copyright (C) 2006-2007 Joey Hurst <jhurst@lucubrate.org>
14 # Copyright (C) 2012 Anari Jalakas <anari.jalakas@gmail.com>
15 # Copyright (C) 2006, 2009 Ante Karamatic <ivoks@ubuntu.com>
16 # Copyright (C) 2009-2010,2014 Sean Robinson <seankrobinson@gmail.com>
17 # Copyright (C) 2010 Prokhor Shuchalov <p@shuchalov.ru>
19 # This program is free software; you can redistribute it and/or modify
20 # it under the terms of the GNU General Public License as published by
21 # the Free Software Foundation; version 2 of the License.
23 # This program is distributed in the hope that it will be useful,
24 # but WITHOUT ANY WARRANTY; without even the implied warranty of
25 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
26 # GNU General Public License in LICENSE.GPL for more details.
28 # You should have received a copy of the GNU General Public License
29 # along with this program; if not, write to the Free Software
30 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
33 try:
34 # Py2
35 import Queue as queue
36 except ImportError:
37 # Py3
38 import queue
40 import logging
41 import os
42 import re
43 import sys
44 from signal import SIGTERM
45 from subprocess import CalledProcessError, Popen, PIPE, STDOUT
46 from threading import Event
47 from time import sleep
49 from wifiradar.config import make_section_name, ConfigManager
50 import wifiradar.misc as misc
51 from wifiradar.pubsub import Message
53 # create a logger
54 logger = logging.getLogger(__name__)
57 def get_enc_mode(use_wpa, key):
58 """ Return the WiFi encryption mode based on the combination of
59 'use_wpa' and 'key'. Possible return values are 'wpa', 'wep',
60 and 'none'.
61 """
62 if use_wpa:
63 return 'wpa'
64 elif key == '':
65 return 'none'
66 else:
67 return 'wep'
69 def scanning_thread(config_manager, apQueue, commandQueue, exit_event):
70 """ Scan for a limited time and return AP names and bssid found.
71 Access points we find will be put on the outgoing Queue, apQueue.
73 Parameters:
75 'config_manager' -- ConfigManager - Configuration Manager
77 'apQueue' -- Queue - Queue on which to put AP profiles
79 'commandQueue' -- Queue - Queue from which to read commands
81 Returns:
83 nothing
84 """
85 logger.info("Begin thread.")
86 # Setup our essid pattern matcher
87 essid_pattern = re.compile("ESSID\s*(:|=)\s*\"([^\"]+)\"", re.I | re.M | re.S)
88 bssid_pattern = re.compile("Address\s*(:|=)\s*([a-zA-Z0-9:]+)", re.I | re.M | re.S)
89 protocol_pattern = re.compile("Protocol\s*(:|=)\s*IEEE 802.11\s*([abgn]+)", re.I | re.M | re.S)
90 mode_pattern = re.compile("Mode\s*(:|=)\s*([^\n]+)", re.I | re.M | re.S)
91 channel_pattern = re.compile("Channel\s*(:|=)*\s*(\d+)", re.I | re.M | re.S)
92 enckey_pattern = re.compile("Encryption key\s*(:|=)\s*(on|off)", re.I | re.M | re.S)
93 signal_pattern = re.compile("Signal level\s*(:|=)\s*(-?[0-9]+)", re.I | re.M | re.S)
95 access_points = {}
96 command = "scan"
97 while True:
98 # check for exit call before trying to process
99 if exit_event.isSet():
100 logger.info("Exiting.")
101 return
102 try:
103 command = commandQueue.get_nowait()
104 logger.info("received command: %s" % (command, ))
105 command_read = True
106 except queue.Empty:
107 command_read = False
108 try:
109 device = config_manager.get_network_device()
110 except misc.NoDeviceError as e:
111 logger.critical('Wifi device not found, ' +
112 'please set this in the preferences.')
113 if ( device and command == "scan" ):
114 logger.debug("Beginning scan pass")
115 # update the signal strengths
116 iwlist_command = [
117 config_manager.get_opt('DEFAULT', 'iwlist_command'),
118 device, 'scan']
119 try:
120 scandata = Popen(iwlist_command, stdout=PIPE,
121 stderr=STDOUT).stdout
122 except OSError as e:
123 if e.errno == 2:
124 logger.critical("iwlist command not found, please set this in the preferences.")
125 scandata = ""
126 # zero out the signal levels for all access points
127 for bssid in access_points:
128 access_points[bssid]['signal'] = 0
129 # split the scan data based on the address line
130 hits = scandata.split(' - ')
131 for hit in hits:
132 # set the defaults for profile template
133 profile = get_new_profile()
134 m = essid_pattern.search(hit)
135 if m:
136 # we found an essid
137 profile['essid'] = m.groups()[1]
138 m = bssid_pattern.search(hit) # get BSSID from scan
139 if m: profile['bssid'] = m.groups()[1]
140 m = protocol_pattern.search(hit) # get protocol from scan
141 if m: profile['protocol'] = m.groups()[1]
142 m = mode_pattern.search(hit) # get mode from scan
143 if m: profile['mode'] = m.groups()[1]
144 m = channel_pattern.search(hit) # get channel from scan
145 if m: profile['channel'] = m.groups()[1]
146 m = enckey_pattern.search(hit) # get encryption key from scan
147 if m: profile['encrypted'] = (m.groups()[1] == 'on')
148 m = signal_pattern.search(hit) # get signal strength from scan
149 if m: profile['signal'] = m.groups()[1]
150 access_points[ profile['bssid'] ] = profile
151 for bssid in access_points:
152 access_points[bssid]['available'] = (access_points[bssid]['signal'] > 0)
153 # Put all, now or previously, sensed access_points into apQueue
154 try:
155 logger.debug("Scanned profile: %s" % (access_points[ bssid ], ))
156 apQueue.put_nowait(access_points[bssid])
157 except queue.Full:
158 pass
159 if command_read:
160 commandQueue.task_done()
161 sleep(1)
164 class ConnectionManager(object):
165 """ Manage a connection; including reporting connection state,
166 connecting/disconnecting from an AP, and returning current IP, ESSID, and BSSID.
168 def __init__(self, msg_pipe):
169 """ Create a new connection manager which communicates with a
170 controlling process or thread through 'msg_pipe' (a Pipe
171 object).
173 self.msg_pipe = msg_pipe
174 self._watching = Event()
175 self._watching.set()
177 def run(self):
178 """ Watch for incoming messages.
180 while self._watching.is_set():
181 try:
182 msg = self.msg_pipe.recv()
183 except (EOFError, IOError) as e:
184 # This is bad, really bad.
185 logger.critical('read on closed ' +
186 'Pipe ({}), failing...'.format(rfd))
187 self._watching.clear()
188 raise misc.PipeError(e)
189 else:
190 self._check_message(msg)
192 def _check_message(self, msg):
193 """ Process incoming messages.
195 if msg.topic == 'EXIT':
196 self.msg_pipe.close()
197 self._watching.clear()
198 elif msg.topic == 'CONFIG-UPDATE':
199 # Replace configuration manager with the one in msg.details.
200 self.set_config(msg.details)
201 elif msg.topic == 'CONNECT':
202 # Try to connect to the profile in msg.details.
203 self.connect(msg.details)
204 elif msg.topic == 'DISCONNECT':
205 # Try to disconnect from the profile in msg.details.
206 self.disconnect(msg.details)
207 elif msg.topic == 'IF-CHANGE':
208 # Try to connect to the profile in msg.details.
209 try:
210 self.if_change(msg.details)
211 except misc.DeviceError as e:
212 self.msg_pipe.send(Message('ERROR', e))
213 except ValueError as e:
214 logger.warning(e)
215 elif msg.topic == 'QUERY-IP':
216 # Send out a message with the current IP address.
217 self.msg_pipe.send(Message('ANN-IP', self.get_current_ip()))
218 elif msg.topic == 'QUERY-ESSID':
219 # Send out a message with the current ESSID.
220 self.msg_pipe.send(Message('ANN-ESSID', self.get_current_essid()))
221 elif msg.topic == 'QUERY-BSSID':
222 # Send out a message with the current BSSID.
223 self.msg_pipe.send(Message('ANN-BSSID', self.get_current_bssid()))
225 def set_config(self, config_manager):
226 """ Set the configuration manager to 'config'. This method
227 must be called before any other public method or a caller
228 will likely receive an AttributeError about a missing
229 'config' attribute.
231 Raises TypeError if 'config_manager' is not a ConfigManager
232 object.
234 if not isinstance(config_manager, ConfigManager):
235 raise TypeError('config must be a ConfigManager object')
236 self.config = config_manager
238 def _run_script(self, script_name, profile, device):
239 """ Run the script (e.g. connection prescript) in 'profile' which
240 is named in 'script_name'.
242 if profile[script_name]:
243 logger.info('running {}'.format(script_name))
244 profile_name = make_section_name(profile['essid'], profile['bssid'])
245 enc_mode = get_enc_mode(profile['use_wpa'], profile['key'])
246 custom_env = {
247 "WIFIRADAR_IP": self.get_current_ip(),
248 "WIFIRADAR_ESSID": self.get_current_essid(),
249 "WIFIRADAR_BSSID": self.get_current_bssid(),
250 "WIFIRADAR_PROFILE": profile_name,
251 "WIFIRADAR_ENCMODE": enc_mode,
252 "WIFIRADAR_SECMODE": profile['security'],
253 "WIFIRADAR_IF": device}
254 try:
255 misc.shellcmd(profile[script_name].split(' '), custom_env)
256 except CalledProcessError as e:
257 logger.error('script "{}" failed: {}'.format(script_name, e))
258 self.msg_pipe.send(Message('ERROR',
259 'script "{}" failed: {}'.format(script_name, e)))
261 def _prepare_nic(self, profile, device):
262 """ Configure the NIC for upcoming connection.
264 # Start building iwconfig command line.
265 iwconfig_command = [
266 self.config.get_opt('DEFAULT', 'iwconfig_command'),
267 device,
268 'essid', "'{}'".format(profile['essid']),
271 # Setting key
272 iwconfig_command.append('key')
273 if (not profile['key']) or (profile['key'] == 's:'):
274 iwconfig_command.append('off')
275 else:
276 # Setting this stops association from working, so remove it for now
277 #if profile['security'] != '':
278 #iwconfig_command.append(profile['security'])
279 iwconfig_command.append("'{}'".format(profile['key']))
280 #iwconfig_commands.append( "key %s %s" % ( profile['security'], profile['key'] ) )
282 # Set mode.
283 profile['mode'] = profile['mode'].lower()
284 if profile['mode'] == 'master' or profile['mode'] == 'auto':
285 profile['mode'] = 'managed'
286 iwconfig_command.extend(['mode', profile['mode']])
288 # Set channel.
289 if 'channel' in profile:
290 iwconfig_command.extend(['channel', profile['channel']])
292 # Set AP address (do this last since iwconfig seems to want it only there).
293 iwconfig_command.extend(['ap', profile['bssid']])
295 # Some cards require a commit
296 if self.config.get_opt_as_bool('DEFAULT', 'commit_required'):
297 iwconfig_command.append('commit')
299 logger.debug('iwconfig_command: {}'.format(iwconfig_command))
301 try:
302 misc.shellcmd(iwconfig_command)
303 except CalledProcessError as e:
304 logger.error('Failed to prepare NIC: {}'.format(e))
305 self.msg_pipe.send(Message('ERROR',
306 'Failed to prepare NIC: {}'.format(e)))
307 raise misc.DeviceError('Could not configure wireless options.')
309 def _stop_dhcp(self, device):
310 """ Stop any DHCP client daemons running with our 'device'.
312 logger.info('Stopping any DHCP clients on "{}"'.format(device))
313 if os.access(self.config.get_opt('DHCP', 'pidfile'), os.R_OK):
314 if self.config.get_opt('DHCP', 'kill_args'):
315 dhcp_command = [self.config.get_opt('DHCP', 'command')]
316 dhcp_command.extend(self.config.get_opt('DHCP', 'kill_args').split(' '))
317 dhcp_command.append(device)
318 logger.info("DHCP command: %s" % (dhcp_command, ))
320 # call DHCP client command and wait for return
321 logger.info("Stopping DHCP with kill_args")
322 try:
323 misc.shellcmd(dhcp_command)
324 except CalledProcessError as e:
325 logger.error('Attempt to stop DHCP failed: {}'.format(e))
326 else:
327 logger.info("Stopping DHCP manually...")
328 os.kill(int(open(self.config.get_opt('DHCP', 'pidfile'), mode='r').readline()), SIGTERM)
330 def _start_dhcp(self, device):
331 """ Start a DHCP client daemon on 'device'.
333 logger.debug('Starting DHCP command on "{}"'.format(device))
334 self.msg_pipe.send(Message('STATUS',
335 'Acquiring IP Address (DHCP)'))
336 dhcp_command = [self.config.get_opt('DHCP', 'command')]
337 dhcp_command.extend(self.config.get_opt('DHCP', 'args').split(' '))
338 dhcp_command.append(device)
339 logger.info("dhcp_command: %s" % (dhcp_command, ))
340 try:
341 dhcp_proc = Popen(dhcp_command, stdout=None, stderr=None)
342 except OSError as e:
343 if e.errno == 2:
344 logger.critical('DHCP client not found, ' +
345 'please set this in the preferences.')
346 else:
347 # The DHCP client daemon should timeout on its own, hence
348 # the +3 seconds on timeout so we don't cut the daemon off
349 # while it is finishing up.
350 timeout = self.config.get_opt_as_int('DHCP', 'timeout') + 3
351 tick = 0.25
352 dhcp_status = dhcp_proc.poll()
353 while dhcp_status is None:
354 if timeout < 0:
355 dhcp_proc.terminate()
356 else:
357 timeout = timeout - tick
358 sleep(tick)
359 dhcp_status = dhcp_proc.poll()
360 if self.get_current_ip() is not None:
361 self.msg_pipe.send(Message('STATUS',
362 'Got IP address. Done.'))
363 else:
364 self.msg_pipe.send(Message('STATUS',
365 'Could not get IP address!'))
367 def _stop_wpa(self):
368 """ Stop all WPA supplicants.
370 logger.info("Kill off any existing WPA supplicants running...")
371 if os.access(self.config.get_opt('WPA', 'pidfile'), os.R_OK):
372 logger.info("Killing existing WPA supplicant...")
373 try:
374 if self.config.get_opt('WPA', 'kill_command'):
375 wpa_command = [self.config.get_opt('WPA', 'kill_command').split(' ')]
376 try:
377 misc.shellcmd(wpa_command)
378 except CalledProcessError as e:
379 logger.error('Attempt to stop WPA supplicant ' + \
380 'failed: {}'.format(e))
381 else:
382 os.kill(int(open(self.config.get_opt('WPA', 'pidfile'), mode='r').readline()), SIGTERM)
383 except OSError:
384 logger.info("Failed to kill WPA supplicant")
386 def _start_wpa(self):
387 """ Start WPA supplicant and let it associate with the AP.
389 self.msg_pipe.send(Message('STATUS',
390 'WPA supplicant starting'))
392 wpa_command = [self.config.get_opt('WPA', 'command')]
393 wpa_command.extend(self.config.get_opt('WPA', 'args').split(' '))
394 try:
395 misc.shellcmd(wpa_command)
396 except CalledProcessError as e:
397 logger.error('WPA supplicant failed to start: {}'.format(e))
399 def _start_manual_network(self, profile, device):
400 """ Manually configure network settings after association.
402 # Bring down the interface before trying to make changes.
403 ifconfig_command = [
404 self.config.get_opt('DEFAULT', 'ifconfig_command'),
405 device, 'down']
406 try:
407 misc.shellcmd(ifconfig_command)
408 except CalledProcessError as e:
409 logger.error('Device "{}" failed to go down: {}'.format(device, e))
410 # Bring the interface up with our manual IP.
411 ifconfig_command = [
412 self.config.get_opt('DEFAULT', 'ifconfig_command'),
413 device, profile['ip'],
414 'netmask', profile['netmask']]
415 try:
416 misc.shellcmd(ifconfig_command)
417 except CalledProcessError as e:
418 logger.error('Device "{}" failed to configure: {}'.format(device, e))
419 # Configure routing information.
420 route_command = [
421 self.config.get_opt('DEFAULT', 'route_command'),
422 'add', 'default', 'gw', profile['gateway']]
423 try:
424 misc.shellcmd(route_command)
425 except CalledProcessError as e:
426 logger.error('Failed to configure routing information: {}'.format(e))
427 # Build the /etc/resolv.conf file, if needed.
428 resolv_contents = ''
429 if profile['domain']:
430 resolv_contents += "domain {}\n".format(profile['domain'])
431 if profile['dns1']:
432 resolv_contents += "nameserver {}\n".format(profile['dns1'])
433 if profile['dns2']:
434 resolv_contents += "nameserver {}\n".format(profile['dns2'])
435 if resolv_contents:
436 with open('/etc/resolv.conf', 'w') as resolv_file:
437 resolv_file.write(resolv_contents)
439 def connect(self, profile):
440 """ Connect to the access point specified by 'profile'.
442 if not profile['bssid']:
443 raise ValueError('missing BSSID')
444 logger.info("Connecting to the {} ({}) network".format(
445 profile['essid'], profile['bssid']))
447 device = self.config.get_network_device()
449 # Ready to dance...
450 self.msg_pipe.send(Message('STATUS', 'starting con_prescript'))
451 self._run_script('con_prescript', profile, device)
452 self.msg_pipe.send(Message('STATUS', 'con_prescript has run'))
454 self._prepare_nic(profile, device)
456 self._stop_dhcp(device)
457 self._stop_wpa()
459 logger.debug("Disable scan while connection attempt in progress...")
460 self.msg_pipe.send(Message('SCAN-STOP', ''))
462 if profile['use_wpa'] :
463 self._start_wpa()
465 if profile['use_dhcp'] :
466 self._start_dhcp(device)
467 else:
468 self._start_manual_network(profile, device)
470 # Begin scanning again
471 self.msg_pipe.send(Message('SCAN-START', ''))
473 # Run the connection postscript.
474 self.msg_pipe.send(Message('STATUS', 'starting con_postscript'))
475 self._run_script('con_postscript', profile, device)
476 self.msg_pipe.send(Message('STATUS', 'con_postscript has run'))
478 self.msg_pipe.send(Message('STATUS', 'connected'))
480 def disconnect(self, profile):
481 """ Disconnect from the AP with which a connection has been
482 established/attempted.
484 logger.info("Disconnecting")
486 # Pause scanning while manipulating card
487 self.msg_pipe.send(Message('SCAN-STOP', ''))
489 device = self.config.get_network_device()
491 self.msg_pipe.send(Message('STATUS', 'starting dis_prescript'))
492 self._run_script('dis_prescript', profile, device)
493 self.msg_pipe.send(Message('STATUS', 'dis_prescript has run'))
495 self._stop_dhcp(device)
497 self._stop_wpa()
499 # Clear out the wireless stuff.
500 iwconfig_command = [
501 self.config.get_opt('DEFAULT.iwconfig_command'),
502 device,
503 'essid', 'any',
504 'key', 'off',
505 'mode', 'managed',
506 'channel', 'auto',
507 'ap', 'off']
508 try:
509 misc.shellcmd(iwconfig_command)
510 except CalledProcessError as e:
511 logger.error('Failed to clean up wireless configuration: {}'.format(e))
513 # Since it may be brought back up by the next scan, unset its IP.
514 ifconfig_command = [
515 self.config.get_opt('DEFAULT.ifconfig_command'),
516 device,
517 '0.0.0.0']
518 try:
519 misc.shellcmd(ifconfig_command)
520 except CalledProcessError as e:
521 logger.error('Failed to unset IP address: {}'.format(e))
523 # Now take the interface down. Taking down the interface too
524 # quickly can crash my system, so pause a moment.
525 sleep(0.25)
526 self.if_change('down')
528 self.msg_pipe.send(Message('STATUS', 'starting dis_postscript'))
529 self._run_script('dis_postscript', profile, device)
530 self.msg_pipe.send(Message('STATUS', 'dis_postscript has run'))
532 logger.info("Disconnect complete.")
534 # Begin scanning again
535 self.msg_pipe.send(Message('SCAN-START', ''))
537 def if_change(self, state):
538 """ Change the interface to 'state', i.e. 'up' or 'down'.
540 'if_change' raises ValueError if 'state' is not recognized,
541 raises OSError if there is a problem running ifconfig, and
542 raises DeviceError if ifconfig reports the change failed.
544 state = state.lower()
545 if ((state == 'up') or (state == 'down')):
546 device = self.config.get_network_device()
547 if device:
548 ifconfig_command = [
549 self.config.get_opt('DEFAULT', 'ifconfig_command'),
550 device, state]
551 try:
552 logger.info('changing interface ' +
553 '{} state to {}'.format(device, state))
554 ifconfig_info = Popen(ifconfig_command, stdout=PIPE,
555 stderr=STDOUT).stdout
556 except OSError as e:
557 if e.errno == 2:
558 logger.critical("ifconfig command not found, " +
559 "please set this in the preferences.")
560 raise e
561 else:
562 for line in ifconfig_info:
563 if len(line) > 0:
564 raise misc.DeviceError('Could not change ' +
565 'device state: {}'.format(line))
566 else:
567 raise ValueError('unrecognized state for device: {}'.format(state))
569 def get_current_ip(self):
570 """ Return the current IP address as a string or None.
572 device = self.config.get_network_device()
573 if device:
574 ifconfig_command = [
575 self.config.get_opt('DEFAULT', 'ifconfig_command'),
576 device]
577 try:
578 ifconfig_info = Popen(ifconfig_command, stdout=PIPE).stdout
579 # Be careful to the language (inet adr: in French for example)
581 # Hi Brian
583 # I'm using wifi-radar on a system with German translations (de_CH-UTF-8).
584 # There the string in ifconfig is inet Adresse for the IP which isn't
585 # found by the current get_current_ip function in wifi-radar. I changed
586 # the according line (#289; gentoo, v1.9.6-r1) to
587 # >ip_re = re.compile(r'inet [Aa]d?dr[^.]*:([^.]*\.[^.]*\.[^.]*\.[0-9]*)')
588 # which works on my system (LC_ALL=de_CH.UTF-8) and still works with LC_ALL=C.
590 # I'd be happy if you could incorporate this small change because as now
591 # I've got to change the file every time it is updated.
593 # Best wishes
595 # Simon
596 ip_re = re.compile(r'inet [Aa]d?dr[^.]*:([^.]*\.[^.]*\.[^.]*\.[0-9]*)')
597 line = ifconfig_info.read()
598 if ip_re.search(line):
599 return ip_re.search(line).group(1)
600 except OSError as e:
601 if e.errno == 2:
602 logger.critical("ifconfig command not found, " +
603 "please set this in the preferences.")
604 return None
606 def get_current_essid(self):
607 """ Return the current ESSID as a string or None.
609 device = self.config.get_network_device()
610 if device:
611 iwconfig_command = [
612 self.config.get_opt('DEFAULT', 'iwconfig_command'),
613 device]
614 try:
615 iwconfig_info = Popen(iwconfig_command, stdout=PIPE, stderr=STDOUT).stdout
616 essid_re = re.compile(r'ESSID\s*(:|=)\s*"([^"]+)"',
617 re.I | re.M | re.S)
618 line = iwconfig_info.read()
619 if essid_re.search(line):
620 return essid_re.search(line).group(2)
621 except OSError as e:
622 if e.errno == 2:
623 logger.critical("iwconfig command not found, " +
624 "please set this in the preferences.")
625 return None
627 def get_current_bssid(self):
628 """ Return the current BSSID as a string or None.
630 device = self.config.get_network_device()
631 if device:
632 iwconfig_command = [
633 self.config.get_opt('DEFAULT', 'iwconfig_command'),
634 device]
635 try:
636 iwconfig_info = Popen(iwconfig_command, stdout=PIPE, stderr=STDOUT).stdout
637 bssid_re = re.compile(r'Access Point\s*(:|=)\s*([a-fA-F0-9:]{17})',
638 re.I | re.M | re.S )
639 line = iwconfig_info.read()
640 if bssid_re.search(line):
641 return bssid_re.search(line).group(2)
642 except OSError as e:
643 if e.errno == 2:
644 logger.critical("iwconfig command not found, " +
645 "please set this in the preferences.")
646 return None
649 # Make so we can be imported
650 if __name__ == "__main__":
651 pass