Allow empty defaults when creating ConfigFileManager
[wifi-radar.git] / wifiradar / config.py
blob72f772921ba23319d703c5f27367d3d886062844
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 logging
37 import os
38 import tempfile
39 from shutil import move
40 from subprocess import Popen, PIPE, STDOUT
41 from types import *
43 from wifiradar.misc import *
45 if PYVERSION < 3:
46 import ConfigParser as configparser
47 str = unicode
48 else:
49 import configparser
51 # create a logger
52 logger = logging.getLogger(__name__)
55 def copy_configuration(original, profiles=False):
56 """ Return a :class:`ConfigManager` copy of :dat:`original`. If
57 :data:`profiles` is False (the default), the copy does not include
58 the known AP profiles.
59 """
60 config_copy = ConfigManager()
61 config_copy._defaults = original._defaults.copy()
62 config_copy._sections = original._sections.copy()
63 # If not needed, remove the profiles from the new copy.
64 if not profiles:
65 for section in config_copy.profiles():
66 config_copy.remove_section(section)
67 return config_copy
69 def make_section_name(essid, bssid):
70 """ Returns the combined 'essid' and 'bssid' to make a config file
71 section name. 'essid' and 'bssid' are strings.
72 """
73 return essid + ':' + bssid
76 class ConfigManager(object, configparser.SafeConfigParser):
77 """ Manage configuration options.
78 """
79 def __init__(self, defaults=None):
80 """ Create a new configuration manger with DEFAULT options and
81 values in the 'defaults' dictionary.
82 """
83 configparser.SafeConfigParser.__init__(self, defaults)
84 self.auto_profile_order = []
86 def get_network_device(self):
87 """ Return the network device name.
89 If a device is specified in the configuration file,
90 'get_network_device' returns that value. If the configuration
91 is set to "auto-detect", this method returns the first
92 WiFi device as returned by iwconfig. 'get_network_device'
93 raises 'wifiradar.misc.NoDeviceError' if no wireless device
94 can be found in auto-detect mode.
95 """
96 device = self.get_opt('DEFAULT', 'interface')
97 if device == 'auto_detect':
98 # auto detect network device
99 iwconfig_command = [
100 self.get_opt('DEFAULT', 'iwconfig_command'),
101 device]
102 try:
103 iwconfig_info = Popen(iwconfig_command, stdout=PIPE,
104 stderr=STDOUT).stdout
105 wireless_devices = list()
106 for line in iwconfig_info:
107 if '802.11' in line:
108 name = line[0:line.find(' ')]
109 wireless_devices.append(name)
110 # return the first device in the list
111 return wireless_devices[0]
112 except OSError as e:
113 logger.critical(_('problem auto-detecting wireless '
114 'device using iwconfig: {EXC}').format(EXC=e))
115 except IndexError:
116 logger.critical(_('No WiFi device found, '
117 'please set this in the preferences.'))
118 raise NoDeviceError('No WiFi device found.')
119 else:
120 # interface has been manually specified in configuration
121 return device
123 def set_section(self, section, dictionary):
124 """ Set the options of 'section' to values from 'dictionary'.
126 'section' will be created if it does not exist. The keys of
127 'dictionary' are the options and its values are the option
128 values.
130 for option, value in dictionary.items():
131 if isinstance(value, BooleanType):
132 self.set_bool_opt(section, option, value)
133 elif isinstance(value, IntType):
134 self.set_int_opt(section, option, value)
135 elif isinstance(value, FloatType):
136 self.set_float_opt(section, option, value)
137 else:
138 self.set_opt(section, option, value)
140 def get_profile(self, section):
141 """ Return the profile values in 'section' as a dictionary.
143 'get_profile' raises NoSectionError if the profile does not
144 exist.
146 str_types = ['bssid', 'channel', 'essid', 'protocol',
147 'con_prescript', 'con_postscript', 'dis_prescript',
148 'dis_postscript', 'key', 'mode', 'security',
149 'wpa_driver', 'ip', 'netmask', 'gateway', 'domain',
150 'dns1', 'dns2']
151 bool_types = ['known', 'available', 'roaming', 'encrypted',
152 'use_wpa', 'use_dhcp']
153 int_types = ['signal']
154 profile = get_new_profile()
155 for option in bool_types:
156 try:
157 profile[option] = self.get_opt_as_bool(section, option)
158 except configparser.NoOptionError:
159 # A missing option means the default will be used.
160 pass
161 for option in int_types:
162 try:
163 profile[option] = self.get_opt_as_int(section, option)
164 except configparser.NoOptionError:
165 # A missing option means the default will be used.
166 pass
167 for option in str_types:
168 try:
169 profile[option] = self.get_opt(section, option)
170 except configparser.NoOptionError:
171 # A missing option means the default will be used.
172 pass
173 return profile
175 def get_opt(self, section, option):
176 """ Return the value of 'option' in 'section', as a string.
178 'section' and 'option' must be strings.
180 'get_opt' raises NoSectionError when 'section' is unknown and
181 NoOptionError when 'option' in unknown.
183 # False means to use interpolation when retrieving the value.
184 return self.get(section, option, False)
186 def get_opt_as_bool(self, section, option):
187 """ Return the value of 'option' in 'section', as a boolean.
189 return self.getboolean(section, option)
191 def get_opt_as_int(self, section, option):
192 """ Return the value of 'option' in 'section', as an integer.
194 return self.getint(section, option)
196 def get_opt_as_float(self, section, option):
197 """ Return the value of 'option' in 'section', as a float.
199 return self.getfloat(section, option)
201 def set_opt(self, section, option, value):
202 """ Set 'option' to 'value' in 'section'.
204 If 'section', does not exist in the configuration, it is
205 created. If 'section' and 'option' are not strings, each
206 will be turned into one before being added. Raises
207 TypeError if 'value' is not a string.
209 section, option = str(section), str(option)
210 try:
211 self.set(section, option, value)
212 except configparser.NoSectionError:
213 self.add_section(section)
214 self.set_opt(section, option, value)
216 def set_bool_opt(self, section, option, value):
217 """ Set 'option' to boolean 'value' in 'section'.
219 'set_bool_opt' calls 'set_opt' after converting 'value' to
220 a string. Raises ValueError if 'value' is not a boolean,
221 where a boolean may be True, 'True', or a number greater
222 than 0; or False, 'False', or 0.
224 if isinstance(value, BooleanType):
225 # use False = 0 and True = 1 to return index into tuple
226 value = ('False', 'True')[value]
227 elif isinstance(value, IntType):
228 if value < 0:
229 raise ValueError(_('boolean value must be >= 0'))
230 # use False = 0 and True = 1 to return index into tuple
231 value = ('False', 'True')[value > 0]
232 elif isinstance(value, StringTypes):
233 # convert to title case (i.e. capital first letter, only)
234 value = value.title()
235 if value not in ('False', 'True'):
236 raise ValueError(_('value must be "True" or "False"'))
237 else:
238 raise ValueError(_('value cannot be converted to string'))
239 self.set_opt(section, option, value)
241 def set_int_opt(self, section, option, value):
242 """ Set 'option' to integer 'value' in 'section'.
244 'set_int_opt' calls 'set_opt' after converting 'value' to
245 a string. Raises TypeError if 'value' is not an integer.
247 if not isinstance(value, IntType):
248 raise TypeError(_('value is not an integer'))
249 self.set_opt(section, option, str(value))
251 def set_float_opt(self, section, option, value):
252 """ Set 'option' to float 'value' in 'section'.
254 'set_float_opt' calls 'set_opt' after converting 'value' to
255 a string. Raises TypeError if 'value' is not a float.
257 if not isinstance(value, (FloatType, IntType)):
258 raise TypeError(_('value is not a float or integer'))
259 self.set_opt(section, option, str(float(value)))
261 def profiles(self):
262 """ Return a list of the section names which denote AP profiles.
264 'profiles' does not return non-AP sections.
266 profile_list = []
267 for section in self.sections():
268 if ':' in section:
269 profile_list.append(section)
270 return profile_list
272 def update(self, config_manager):
273 """ Update internal configuration information using
274 :data:`config_manager`. This works by replacing the DEFAULT
275 and some non-profile sections in the configuration. All profiles,
276 and any non-profile sections not in :data:`config_manager`, are
277 left untouched during the update.
279 self._defaults = config_manager._defaults
280 for section in (set(config_manager.sections()) -
281 set(config_manager.profiles())):
282 self.set_section(section, dict(config_manager.items(section)))
285 class ConfigFileManager(ConfigManager):
286 """ Manage the configuration for the application, including reading
287 from and writing to a file.
289 def __init__(self, filename, defaults=None):
290 """ Create a new configuration file at 'filename' with DEFAULT
291 options and values in the 'defaults' dictionary.
293 ConfigManager.__init__(self, defaults)
294 self.filename = filename
296 def read(self):
297 """ Read configuration file from disk into instance variables.
299 fp = open(self.filename, 'r')
300 self.readfp(fp)
301 # convert the auto_profile_order to a list for ordering
302 self.auto_profile_order = eval(self.get_opt('DEFAULT', 'auto_profile_order'))
303 for ap in self.profiles():
304 self.set_bool_opt(ap, 'known', True)
305 if ap in self.auto_profile_order: continue
306 self.auto_profile_order.append(ap)
307 fp.close()
308 # Remove any auto_profile_order AP without a matching section.
309 auto_profile_order_copy = self.auto_profile_order[:]
310 for ap in auto_profile_order_copy:
311 if ap not in self.profiles():
312 self.auto_profile_order.remove(ap)
314 def write(self):
315 """ Write configuration file to disk from instance variables.
317 Copied from configparser and modified to write options in
318 specific order.
320 self.set_opt('DEFAULT', 'auto_profile_order',
321 str(self.auto_profile_order))
322 self.set_opt('DEFAULT', 'version', WIFI_RADAR_VERSION)
323 (fd, tempfilename) = tempfile.mkstemp(prefix='wifi-radar.conf.')
324 fp = os.fdopen(fd, 'w')
325 # write DEFAULT section first
326 if self._defaults:
327 fp.write('[DEFAULT]\n')
328 for key in sorted(self._defaults.keys()):
329 fp.write('{KEY} = {VALUE}\n'.format(KEY=key,
330 VALUE=str(self._defaults[key]).replace('\n','\n\t')))
331 fp.write('\n')
332 # write other non-profile sections next
333 for section in self._sections:
334 if section not in self.profiles():
335 fp.write('[{SECT}]\n'.format(SECT=section))
336 for key in sorted(self._sections[section].keys()):
337 if key != '__name__':
338 fp.write('{KEY} = {VALUE}\n'.format(KEY=key,
339 VALUE=str(self._sections[section][key]
340 ).replace('\n', '\n\t')))
341 fp.write('\n')
342 # write profile sections
343 for section in self._sections:
344 if section in self.profiles():
345 fp.write('[{SECT}]\n'.format(SECT=section))
346 for key in sorted(self._sections[section].keys()):
347 if key != '__name__':
348 fp.write('{KEY} = {VALUE}\n'.format(KEY=key,
349 VALUE=str(self._sections[section][key]
350 ).replace('\n', '\n\t')))
351 fp.write('\n')
352 fp.close()
353 move(tempfilename, self.filename)
356 # Make so we can be imported
357 if __name__ == '__main__':
358 pass