Change parent class of ConfigFileManager
[wifi-radar.git] / wifiradar / config.py
blob3491f5c4b4abbbab01bd08db0783c82c8307e354
1 #!/usr/bin/python
2 # -*- coding: utf-8 -*-
4 # config.py - support for WiFi Radar configuration
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) 2006, 2009 Ante Karamatic <ivoks@ubuntu.com>
15 # Copyright (C) 2009-2010,2014 Sean Robinson <robinson@tuxfamily.org>
16 # Copyright (C) 2010 Prokhor Shuchalov <p@shuchalov.ru>
18 # This program is free software; you can redistribute it and/or modify
19 # it under the terms of the GNU General Public License as published by
20 # the Free Software Foundation; version 2 of the License.
22 # This program is distributed in the hope that it will be useful,
23 # but WITHOUT ANY WARRANTY; without even the implied warranty of
24 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 # GNU General Public License in LICENSE.GPL for more details.
27 # You should have received a copy of the GNU General Public License
28 # along with this program; if not, write to:
29 # Free Software Foundation, Inc.
30 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
34 from __future__ import unicode_literals
36 import codecs
37 import configparser
38 import logging
39 import os
40 import tempfile
41 from shutil import move
42 from subprocess import Popen, PIPE, STDOUT
43 from types import *
45 from wifiradar.misc import *
47 if PYVERSION < 3:
48 str = unicode
50 # create a logger
51 logger = logging.getLogger(__name__)
54 def copy_configuration(original, profiles=False):
55 """ Return a :class:`ConfigManager` copy of :dat:`original`. If
56 :data:`profiles` is False (the default), the copy does not include
57 the known AP profiles.
58 """
59 config_copy = ConfigManager()
60 config_copy._defaults = original._defaults.copy()
61 config_copy._sections = original._sections.copy()
62 # If not needed, remove the profiles from the new copy.
63 if not profiles:
64 for section in config_copy.profiles():
65 config_copy.remove_section(section)
66 return config_copy
68 def make_section_name(essid, bssid):
69 """ Returns the combined :data:`essid` and :data:`bssid` to make a
70 config file section name. :data:`essid` and :data:`bssid` are
71 strings.
72 """
73 return essid + ':' + bssid
76 class ConfigManager(object, configparser.SafeConfigParser):
77 """ Manage configuration options, grouped into sections.
78 :class:`ConfigManager` is based on the standard library's
79 configparser API and is meant as a less capable replacement.
80 """
81 def __init__(self, defaults=None):
82 """ Create a new configuration manager.
83 """
84 configparser.SafeConfigParser.__init__(self, defaults)
85 self.auto_profile_order = []
87 def get_network_device(self):
88 """ Return the network device name.
90 If a device is specified in the configuration file,
91 :meth:`get_network_device` returns that value. If the
92 configuration is set to "auto-detect", this method returns the
93 first WiFi device as returned by iwconfig.
94 :meth:`get_network_device` raises
95 :exc:`wifiradar.misc.NoDeviceError` if no wireless device can
96 be found in auto-detect mode.
97 """
98 device = self.get_opt('GENERAL', 'interface')
99 if device == 'auto_detect':
100 # auto detect network device
101 iwconfig_command = [
102 self.get_opt('GENERAL', 'iwconfig_command'),
103 device]
104 try:
105 iwconfig_info = Popen(iwconfig_command, stdout=PIPE,
106 stderr=STDOUT).stdout
107 wireless_devices = list()
108 for line in iwconfig_info:
109 if '802.11' in line:
110 name = line[0:line.find(' ')]
111 wireless_devices.append(name)
112 # return the first device in the list
113 return wireless_devices[0]
114 except OSError as e:
115 logger.critical(_('problem auto-detecting wireless '
116 'device using iwconfig: {EXC}').format(EXC=e))
117 except IndexError:
118 logger.critical(_('No WiFi device found, '
119 'please set this in the preferences.'))
120 raise NoDeviceError('No WiFi device found.')
121 else:
122 # interface has been manually specified in configuration
123 return device
125 def set_section(self, section, dictionary):
126 """ Set the options of :data:`section` to values from
127 :data:`dictionary`.
129 :data:`section` will be created if it does not exist. The keys
130 of :data:`dictionary` are the options and its values are the
131 option values.
133 for option, value in dictionary.items():
134 if isinstance(value, BooleanType):
135 self.set_bool_opt(section, option, value)
136 elif isinstance(value, IntType):
137 self.set_int_opt(section, option, value)
138 elif isinstance(value, FloatType):
139 self.set_float_opt(section, option, value)
140 else:
141 self.set_opt(section, option, value)
143 def get_profile(self, section):
144 """ Return the profile values in :data:`section` as a dictionary.
146 :meth:`get_profile` raises :exc:`NoSectionError` if the profile
147 does not exist.
149 str_types = ['bssid', 'channel', 'essid', 'protocol',
150 'con_prescript', 'con_postscript', 'dis_prescript',
151 'dis_postscript', 'key', 'mode', 'security',
152 'wpa_driver', 'ip', 'netmask', 'gateway', 'domain',
153 'dns1', 'dns2']
154 bool_types = ['known', 'available', 'roaming', 'encrypted',
155 'use_wpa', 'use_dhcp']
156 int_types = ['signal']
157 profile = get_new_profile()
158 for option in bool_types:
159 try:
160 profile[option] = self.get_opt_as_bool(section, option)
161 except configparser.NoOptionError:
162 # A missing option means the default will be used.
163 pass
164 for option in int_types:
165 try:
166 profile[option] = self.get_opt_as_int(section, option)
167 except configparser.NoOptionError:
168 # A missing option means the default will be used.
169 pass
170 for option in str_types:
171 try:
172 profile[option] = self.get_opt(section, option)
173 except configparser.NoOptionError:
174 # A missing option means the default will be used.
175 pass
176 return profile
178 def get_opt(self, section, option):
179 """ Return the value of 'option' in 'section', as a string.
181 'section' and 'option' must be strings.
183 'get_opt' raises NoSectionError when 'section' is unknown and
184 NoOptionError when 'option' in unknown.
186 # False means to use interpolation when retrieving the value.
187 return self.get(section, option, False)
189 def get_opt_as_bool(self, section, option):
190 """ Return the value of :data:`option` in :data:`section`, as a
191 boolean.
193 :meth:`get_opt_as_bool` calls :meth:`get_opt` before converting
194 the string to a boolean and raises :exc:`ValueError` if the
195 conversion is not possible.
197 value = self.get_opt(section, option)
198 if value == 'True':
199 return True
200 elif value == 'False':
201 return False
202 raise ValueError('value was not True or False')
204 def get_opt_as_int(self, section, option):
205 """ Return the value of :data:`option` in :data:`section`, as an
206 integer.
208 :meth:`get_opt_as_int` calls :meth:`get_opt` before converting
209 the string to an integer and raises :exc:`ValueError` if the
210 conversion is not possible.
212 value = self.get_opt(section, option)
213 return int(value)
215 def get_opt_as_float(self, section, option):
216 """ Return the value of :data:`option` in :data:`section`, as a
217 float.
219 :meth:`get_opt_as_float` calls :meth:`get_opt` before converting
220 the string to a float and raises :exc:`ValueError` if the
221 conversion is not possible.
223 value = self.get_opt(section, option)
224 return float(value)
226 def set_opt(self, section, option, value):
227 """ Set :data:`option` to :data:`value` in :data:`section`.
229 If :data:`section`, does not exist in the configuration, it is
230 created. If :data:`section` and :data:`option` are not strings,
231 each will be turned into one before being added. Raises
232 :exc:`TypeError` if :data:`value` is not a string.
234 section, option = str(section), str(option)
235 try:
236 self.set(section, option, value)
237 except configparser.NoSectionError:
238 self.add_section(section)
239 self.set_opt(section, option, value)
241 def set_bool_opt(self, section, option, value):
242 """ Set :data:`option` to boolean :data:`value` in :data:`section`.
244 :meth:`set_bool_opt` calls :meth:`set_opt` after converting
245 :data:`value` to a string. Raises :exc:`ValueError` if
246 :data:`value` is not a boolean, where a boolean may be True,
247 'True', or a number greater than 0; or False, 'False', or 0.
249 if isinstance(value, BooleanType):
250 # use False = 0 and True = 1 to return index into tuple
251 value = ('False', 'True')[value]
252 elif isinstance(value, IntType):
253 if value < 0:
254 raise ValueError(_('boolean value must be >= 0'))
255 # use False = 0 and True = 1 to return index into tuple
256 value = ('False', 'True')[value > 0]
257 elif isinstance(value, StringTypes):
258 # convert to title case (i.e. capital first letter, only)
259 value = value.title()
260 if value not in ('False', 'True'):
261 raise ValueError(_('value must be "True" or "False"'))
262 else:
263 raise ValueError(_('value cannot be converted to string'))
264 self.set_opt(section, option, value)
266 def set_int_opt(self, section, option, value):
267 """ Set :data:`option` to integer :data:`value` in :data:`section`.
269 :meth:`set_int_opt` calls :meth:`set_opt` after converting
270 :data:`value` to a string. Raises :exc:`TypeError` if
271 :data:`value` is not an integer.
273 if not isinstance(value, IntType):
274 raise TypeError(_('value is not an integer'))
275 self.set_opt(section, option, str(value))
277 def set_float_opt(self, section, option, value):
278 """ Set :data:`option` to float :data:`value` in :data:`section`.
280 :meth:`set_float_opt` calls :meth:`set_opt` after converting
281 :data:`value` to a string. Raises :exc:`TypeError` if
282 :data:`value` is not a float.
284 if not isinstance(value, (FloatType, IntType)):
285 raise TypeError(_('value is not a float or integer'))
286 self.set_opt(section, option, str(float(value)))
288 def profiles(self):
289 """ Return a list of the section names which denote AP profiles.
291 :meth:`profiles` does not return non-AP sections.
293 profile_list = []
294 for section in self.sections():
295 if ':' in section:
296 profile_list.append(section)
297 return profile_list
299 def update(self, config_manager):
300 """ Update internal configuration information using
301 :data:`config_manager`. This works by replacing the DEFAULT
302 and some non-profile sections in the configuration. All profiles,
303 and any non-profile sections not in :data:`config_manager`, are
304 left untouched during the update.
306 self._defaults = config_manager._defaults
307 for section in (set(config_manager.sections()) -
308 set(config_manager.profiles())):
309 self.set_section(section,
310 config_manager._sections[section].copy())
313 class ConfigFileManager(configparser.ConfigParser):
314 """ Manage the configuration for the application, including reading
315 from and writing to a file.
317 def __init__(self, filename, defaults=None):
318 """ Create a new configuration file at :data:`filename` with DEFAULT
319 options and values in the 'defaults' dictionary.
321 super(ConfigFileManager, self).__init__(defaults)
322 self.filename = filename
324 def read(self):
325 """ Read configuration file from disk into instance variables.
327 with codecs.open(self.filename, 'r', encoding='utf8') as f:
328 self.read_file(f, self.filename)
329 # convert the auto_profile_order to a list for ordering
330 self.auto_profile_order = eval(self.get_opt('GENERAL', 'auto_profile_order'))
331 for ap in self.profiles():
332 self.set_bool_opt(ap, 'known', True)
333 if ap in self.auto_profile_order: continue
334 self.auto_profile_order.append(ap)
335 # Remove any auto_profile_order AP without a matching section.
336 auto_profile_order_copy = self.auto_profile_order[:]
337 for ap in auto_profile_order_copy:
338 if ap not in self.profiles():
339 self.auto_profile_order.remove(ap)
341 def write(self):
342 """ Write configuration file to disk from instance variables.
344 Copied from configparser and modified to write options in
345 specific order.
347 self.set_opt('GENERAL', 'auto_profile_order',
348 str(self.auto_profile_order))
349 self.set_opt('GENERAL', 'version', WIFI_RADAR_VERSION)
350 # Safely create a temporary file, ...
351 (fd, tempfilename) = tempfile.mkstemp(prefix='wifi-radar.conf.')
352 # ....close the file descriptor to the temporary file, ...
353 os.close(fd)
354 # ....and re-open the temporary file with a codec filter.
355 with codecs.open(tempfilename, 'w', encoding='utf8') as fp:
356 # Write a byte-encoding note on the first line.
357 fp.write('# -*- coding: utf-8 -*-\n')
358 # write DEFAULT section
359 if self._defaults:
360 fp.write('[DEFAULT]\n')
361 for key in sorted(self._defaults.keys()):
362 fp.write('{KEY} = {VALUE}\n'.format(KEY=key,
363 VALUE=str(self._defaults[key]).replace('\n','\n\t')))
364 fp.write('\n')
365 # write other non-profile sections next
366 for section in self._sections:
367 if section not in self.profiles():
368 fp.write('[{SECT}]\n'.format(SECT=section))
369 for key in sorted(self._sections[section].keys()):
370 if key != '__name__':
371 fp.write('{KEY} = {VALUE}\n'.format(KEY=key,
372 VALUE=str(self._sections[section][key]
373 ).replace('\n', '\n\t')))
374 fp.write('\n')
375 # write profile sections
376 for section in self._sections:
377 if section in self.profiles():
378 fp.write('[{SECT}]\n'.format(SECT=section))
379 for key in sorted(self._sections[section].keys()):
380 if key != '__name__':
381 fp.write('{KEY} = {VALUE}\n'.format(KEY=key,
382 VALUE=str(self._sections[section][key]
383 ).replace('\n', '\n\t')))
384 fp.write('\n')
385 move(tempfilename, self.filename)
388 # Make so we can be imported
389 if __name__ == '__main__':
390 pass