/usr/share/arm/util/conf.py is in tor-arm 1.4.5.0-1.1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | """
This provides handlers for specially formatted configuration files. Entries are
expected to consist of simple key/value pairs, and anything after "#" is
stripped as a comment. Excess whitespace is trimmed and empty lines are
ignored. For instance:
# This is my sample config
user.name Galen
user.password yabba1234 # here's an inline comment
user.notes takes a fancy to pepperjack chese
blankEntry.example
would be loaded as four entries (the last one's value being an empty string).
If a key's defined multiple times then the last instance of it is used.
"""
import threading
from util import log
CONFS = {} # mapping of identifier to singleton instances of configs
CONFIG = {"log.configEntryNotFound": None,
"log.configEntryTypeError": log.NOTICE}
def loadConfig(config):
config.update(CONFIG)
def getConfig(handle):
"""
Singleton constructor for configuration file instances. If a configuration
already exists for the handle then it's returned. Otherwise a fresh instance
is constructed.
Arguments:
handle - unique identifier used to access this config instance
"""
if not handle in CONFS: CONFS[handle] = Config()
return CONFS[handle]
class Config():
"""
Handler for easily working with custom configurations, providing persistence
to and from files. All operations are thread safe.
Parameters:
path - location from which configurations are saved and loaded
contents - mapping of current key/value pairs
rawContents - last read/written config (initialized to an empty string)
"""
def __init__(self):
"""
Creates a new configuration instance.
"""
self.path = None # location last loaded from
self.contents = {} # configuration key/value pairs
self.contentsLock = threading.RLock()
self.requestedKeys = set()
self.rawContents = [] # raw contents read from configuration file
def getValue(self, key, default=None, multiple=False):
"""
This provides the currently value associated with a given key. If no such
key exists then this provides the default.
Arguments:
key - config setting to be fetched
default - value provided if no such key exists
multiple - provides back a list of all values if true, otherwise this
returns the last loaded configuration value
"""
self.contentsLock.acquire()
if key in self.contents:
val = self.contents[key]
if not multiple: val = val[-1]
self.requestedKeys.add(key)
else:
msg = "config entry '%s' not found, defaulting to '%s'" % (key, str(default))
log.log(CONFIG["log.configEntryNotFound"], msg)
val = default
self.contentsLock.release()
return val
def get(self, key, default=None):
"""
Fetches the given configuration, using the key and default value to hint
the type it should be. Recognized types are:
- logging runlevel if key starts with "log."
- boolean if default is a boolean (valid values are 'true' and 'false',
anything else provides the default)
- integer or float if default is a number (provides default if fails to
cast)
- list of all defined values default is a list
- mapping of all defined values (key/value split via "=>") if the default
is a dict
Arguments:
key - config setting to be fetched
default - value provided if no such key exists
"""
isMultivalue = isinstance(default, list) or isinstance(default, dict)
val = self.getValue(key, default, isMultivalue)
if val == default: return val
if key.startswith("log."):
if val.upper() == "NONE": val = None
elif val.upper() in log.Runlevel.values(): val = val.upper()
else:
msg = "Config entry '%s' is expected to be a runlevel" % key
if default != None: msg += ", defaulting to '%s'" % default
log.log(CONFIG["log.configEntryTypeError"], msg)
val = default
elif isinstance(default, bool):
if val.lower() == "true": val = True
elif val.lower() == "false": val = False
else:
msg = "Config entry '%s' is expected to be a boolean, defaulting to '%s'" % (key, str(default))
log.log(CONFIG["log.configEntryTypeError"], msg)
val = default
elif isinstance(default, int):
try: val = int(val)
except ValueError:
msg = "Config entry '%s' is expected to be an integer, defaulting to '%i'" % (key, default)
log.log(CONFIG["log.configEntryTypeError"], msg)
val = default
elif isinstance(default, float):
try: val = float(val)
except ValueError:
msg = "Config entry '%s' is expected to be a float, defaulting to '%f'" % (key, default)
log.log(CONFIG["log.configEntryTypeError"], msg)
val = default
elif isinstance(default, list):
pass # nothing special to do (already a list)
elif isinstance(default, dict):
valMap = {}
for entry in val:
if "=>" in entry:
entryKey, entryVal = entry.split("=>", 1)
valMap[entryKey.strip()] = entryVal.strip()
else:
msg = "Ignoring invalid %s config entry (expected a mapping, but \"%s\" was missing \"=>\")" % (key, entry)
log.log(CONFIG["log.configEntryTypeError"], msg)
val = valMap
return val
def getStrCSV(self, key, default = None, count = None):
"""
Fetches the given key as a comma separated value. This provides back a list
with the stripped values.
Arguments:
key - config setting to be fetched
default - value provided if no such key exists or doesn't match the count
count - if set, then a TypeError is logged (and default returned) if
the number of elements doesn't match the count
"""
confValue = self.getValue(key)
if confValue == None: return default
else:
confComp = [entry.strip() for entry in confValue.split(",")]
# check if the count doesn't match
if count != None and len(confComp) != count:
msg = "Config entry '%s' is expected to be %i comma separated values" % (key, count)
if default != None and (isinstance(default, list) or isinstance(default, tuple)):
defaultStr = ", ".join([str(i) for i in default])
msg += ", defaulting to '%s'" % defaultStr
log.log(CONFIG["log.configEntryTypeError"], msg)
return default
return confComp
def getIntCSV(self, key, default = None, count = None, minValue = None, maxValue = None):
"""
Fetches the given comma separated value, logging a TypeError (and returning
the default) if the values arne't ints or aren't constrained to the given
bounds.
Arguments:
key - config setting to be fetched
default - value provided if no such key exists, doesn't match the count,
values aren't all integers, or doesn't match the bounds
count - checks that the number of values matches this if set
minValue - checks that all values are over this if set
maxValue - checks that all values are less than this if set
"""
confComp = self.getStrCSV(key, default, count)
if confComp == default: return default
# validates the input, setting the errorMsg if there's a problem
errorMsg = None
baseErrorMsg = "Config entry '%s' is expected to %%s" % key
if default != None and (isinstance(default, list) or isinstance(default, tuple)):
defaultStr = ", ".join([str(i) for i in default])
baseErrorMsg += ", defaulting to '%s'" % defaultStr
for val in confComp:
if not val.isdigit():
errorMsg = baseErrorMsg % "only have integer values"
break
else:
if minValue != None and int(val) < minValue:
errorMsg = baseErrorMsg % "only have values over %i" % minValue
break
elif maxValue != None and int(val) > maxValue:
errorMsg = baseErrorMsg % "only have values less than %i" % maxValue
break
if errorMsg:
log.log(CONFIG["log.configEntryTypeError"], errorMsg)
return default
else: return [int(val) for val in confComp]
def update(self, confMappings, limits = {}):
"""
Revises a set of key/value mappings to reflect the current configuration.
Undefined values are left with their current values.
Arguments:
confMappings - configuration key/value mappings to be revised
limits - mappings of limits on numeric values, expected to be of
the form "configKey -> min" or "configKey -> (min, max)"
"""
for entry in confMappings.keys():
val = self.get(entry, confMappings[entry])
if entry in limits and (isinstance(val, int) or isinstance(val, float)):
if isinstance(limits[entry], tuple):
val = max(val, limits[entry][0])
val = min(val, limits[entry][1])
else: val = max(val, limits[entry])
confMappings[entry] = val
def getKeys(self):
"""
Provides all keys in the currently loaded configuration.
"""
return self.contents.keys()
def getUnusedKeys(self):
"""
Provides the set of keys that have never been requested.
"""
return set(self.getKeys()).difference(self.requestedKeys)
def set(self, key, value):
"""
Stores the given configuration value.
Arguments:
key - config key to be set
value - config value to be set
"""
self.contentsLock.acquire()
self.contents[key] = [value]
self.contentsLock.release()
def clear(self):
"""
Drops all current key/value mappings.
"""
self.contentsLock.acquire()
self.contents.clear()
self.contentsLock.release()
def load(self, path):
"""
Reads in the contents of the given path, adding its configuration values
and overwriting any that already exist. If the file's empty then this
doesn't do anything. Other issues (like having insufficient permissions or
if the file doesn't exist) result in an IOError.
Arguments:
path - file path to be loaded
"""
configFile = open(path, "r")
self.rawContents = configFile.readlines()
configFile.close()
self.contentsLock.acquire()
for line in self.rawContents:
# strips any commenting or excess whitespace
commentStart = line.find("#")
if commentStart != -1: line = line[:commentStart]
line = line.strip()
# parse the key/value pair
if line and " " in line:
key, value = line.split(" ", 1)
value = value.strip()
if key in self.contents: self.contents[key].append(value)
else: self.contents[key] = [value]
self.path = path
self.contentsLock.release()
def save(self, saveBackup=True):
"""
Writes the contents of the current configuration. If a configuration file
already exists then merges as follows:
- comments and file contents not in this config are left unchanged
- lines with duplicate keys are stripped (first instance is kept)
- existing entries are overwritten with their new values, preserving the
positioning of in-line comments if able
- config entries not in the file are appended to the end in alphabetical
order
If problems arise in writing (such as an unset path or insufficient
permissions) result in an IOError.
Arguments:
saveBackup - if true and a file already exists then it's saved (with
'.backup' appended to its filename)
"""
pass # TODO: implement when persistence is needed
|