blob: 0ecd12b01b3fecd5bf3d1e04a8d631cd183d51c2 [file] [log] [blame]
#!/usr/bin/env python
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# $Id: $
"""
postgresql.conf configuration file reader
Module contents:
readfile() - Read postgresql.conf file
class gucdict - Container for postgresql.conf settings
class setting - Holds one setting
class ConfigurationError - a subclass of EnvironmentError
Example:
import lib.pgconf as pgconf
d = pgconf.readfile()
port = d.int('port', 5432)
pe = d.bool('password_encryption', False)
sb = d.kB('shared_buffers')
at = d.time('authentication_timeout', 'ms', 2500)
"""
import os
import os.path
import re
# Max recursion level for postgresql.conf include directives.
# The max value is 10 in the postgres code, so it's the same here.
MAX_RECURSION_LEVEL=10
def readfile(filename='postgresql.conf', defaultpath=None):
"""
Read postgresql.conf file and put the settings into a dictionary.
Returns the dictionary: a newly created pgconf.gucdict object.
If filename does not specify an absolute path, it is treated as relative
to defaultpath, or to the current working directory.
"""
if not os.path.isabs(filename):
if defaultpath is None:
defaultpath = os.getcwd()
filename = os.path.normpath(os.path.join(defaultpath, filename))
fp = open(filename)
try:
dictionary = gucdict()
dictionary.populate(fp, filename)
return dictionary
except Exception:
raise
finally:
fp.close()
class gucdict(dict):
"""
A container for settings from a postgresql.conf file.
Behaves as an ordinary dictionary, with a few added methods.
The keys of the dictionary are GUC names in lower case, and the
values are instances of the pgconf.setting class.
The populate() method loads the dictionary with settings from a file.
The str(), bool(), int(), float(), kB(), and time() methods return a
value from the dictionary, converted to internal form.
"""
def populate(self, lines, filename='', recurLevel=0):
'''
Given a postgresql.conf input file (or a list of strings, or some
iterable object yielding lines), look for lines of the form
name[=][value][#comment]
For each one found, construct a pgconf.setting object and put it
into our dictionary.
'''
if recurLevel == MAX_RECURSION_LEVEL:
raise Exception('could not open configuration file "%s": maximum nesting depth exceeded' % filename)
linenumber = 0
for line in lines:
linenumber += 1
m = _setpat.match(line)
if m:
name, value, pos = m.group(1), m.group(3), m.start(3)
if name == 'include':
try:
# Remove the ' from the filename and then convert to abspath if needed.
incfilename = value.strip("'")
if not incfilename.startswith('/') and filename != '':
incfilename = '%s/%s' % (filename[0:filename.rfind('/')], incfilename)
fp = open(incfilename)
self.populate(fp, incfilename, recurLevel+1)
fp.close()
except IOError:
raise Exception('File %s included from %s:%d does not exist' % (incfilename, filename, linenumber))
else:
self[name.lower()] = setting(name, value, filename, linenumber, pos)
def str(self, name, default=None):
"""
Return string setting, or default if absent.
"""
v = self.get(name)
if v:
return v.str()
else:
return default
def bool(self, name, default=None):
"""
Return Boolean setting, or default if absent.
"""
v = self.get(name)
if v:
return v.bool()
else:
return default
def int(self, name, default=None):
"""
Return integer setting, or default if absent.
"""
v = self.get(name)
if v:
return v.int()
else:
return default
def float(self, name, default=None):
"""
Return floating-point setting, or default if absent.
"""
v = self.get(name)
if v:
return v.float()
else:
return default
def kB(self, name, default=None):
"""
Return memory setting in units of 1024 bytes, or default if absent.
"""
v = self.get(name)
if v:
return v.kB()
else:
return default
def time(self, name, unit='s', default=None):
"""
Return time setting, or default if absent.
Specify desired unit as 'ms', 's', or 'min'.
"""
v = self.get(name)
if v:
return v.time(unit)
else:
return default
class setting(object):
"""
Holds a GUC setting from a postgresql.conf file.
The str(), bool(), int(), float(), kB(), and time() methods return the
value converted to the requested internal form. pgconf.ConfigurationError
is raised if the conversion fails, i.e. the value does not conform to the
expected syntax.
"""
def __init__(self, name, value, filename='', linenumber=0, pos=0):
self.name = name
self.value = value
self.filename = filename
self.linenumber = linenumber
self.pos = pos # starting offset of value within the input line
def __repr__(self):
return repr(self.value)
def str(self):
"""
Return the value as a string.
"""
v = self.value
if v and v.endswith("'"):
# Single-quoted string. Remove the opening and closing quotes.
# Replace each escape sequence with the character it stands for.
i = v.index("'") + 1
v = _escapepat.sub(_escapefun, v[i:-1])
return v
def bool(self):
"""
Interpret the value as a Boolean. Returns True or False.
"""
s = self.value
if s:
s = s.lower()
n = len(s)
if (s == '1' or
s == 'on' or
s == 'true'[:n] or
s == 'yes'[:n]):
return True
if (s == '0' or
s == 'off'[:n] or
s == 'false'[:n] or
s == 'no'[:n]):
return False
raise self.ConfigurationError('Boolean value should be one of: 1, 0, '
'on, off, true, false, yes, no.')
def int(self):
"""
Interpret the value as an integer. Returns an int or long.
"""
try:
return int(self.value, 0)
except ValueError:
raise self.ConfigurationError('Value should be integer.')
def float(self):
"""
Interpret the value as floating point. Returns a float.
"""
try:
return float(self.value)
except ValueError:
raise self.ConfigurationError('Value should be floating point.')
def kB(self):
"""
Interpret the value as an amount of memory. Returns an int or long,
in units of 1024 bytes.
"""
try:
m = 1
t = re.split('(kB|MB|GB)', self.value)
if len(t) > 1:
i = ['kB', 'MB', 'GB'].index(t[1])
m = (1, 1024, 1024*1024)[i]
try:
return int(t[0], 0) * m
except ValueError:
pass
return int(float(t[0]) * m)
except (ValueError, IndexError):
raise self.ConfigurationError('Value should be integer or float '
'with optional suffix kB, MB, or GB '
'(kB is default).')
def time(self, unit='s'):
"""
Interpret the value as a time. Returns an int or long.
Specify desired unit as 'ms', 's', or 'min'.
"""
u = ['ms', 's', 'min'].index(unit)
u = (1, 1000, 60*1000)[u]
try:
m = u
t = re.split('(ms|s|min|h|d)', self.value)
if len(t) > 1:
i = ['ms', 's', 'min', 'h', 'd'].index(t[1])
m = (1, 1000, 60*1000, 3600*1000, 24*3600*1000)[i]
return int(t[0], 0) * m / u
except (ValueError, IndexError):
raise self.ConfigurationError('Value should be integer with '
'optional suffix ms, s, min, h, or d '
'(%s is default).' % unit)
def ConfigurationError(self, msg):
msg = '(%s = %s) %s' % (self.name, self.value, msg)
return ConfigurationError(msg, self.filename, self.linenumber)
class ConfigurationError(EnvironmentError):
def __init__(self, msg, filename='', linenumber=0):
self.msg = msg
self.filename = filename
self.linenumber = linenumber
if linenumber:
msg = '%s line %d: %s' % (filename, linenumber, msg)
elif filename:
msg = '%s: %s' % (filename, msg)
EnvironmentError.__init__(self, msg)
def __str__(self):
return self.message
#-------------------------------- private --------------------------------
_setpat = re.compile(r"\s*(\w+)\s*(=\s*)?" # name [=]
'('
r"[eE]?('((\\.)?[^\\']*)*')+|" # single-quoted string or
r"[^\s#']*" # token ending at whitespace or comment
')')
_escapepat = re.compile(r"''|" # pair of single quotes, or
r"\\(" # backslash followed by
r"[0-7][0-7]?[0-7]?|" # nnn (1 to 3 octal digits) or
r"x[0-9A-Fa-f][0-9A-Fa-f]?|" # xHH (1 or 2 hex digits) or
r".)") # one char
def _escapefun(matchobj):
"""Callback to interpret an escape sequence"""
s = matchobj.group()
c = s[1]
i = "bfnrt".find(c)
if i >= 0:
c = "\b\f\n\r\t"[i]
elif c == 'x':
c = chr(int(s[2:], 16))
elif c in '01234567':
c = chr(int(s[1:], 8))
return c