migrate to Python3 (from alejeune)

This commit is contained in:
htouvet
2022-04-25 10:02:43 +02:00
parent 1655977e64
commit bc4b9811ed
65 changed files with 574 additions and 3884 deletions
+87 -65
View File
@@ -1,20 +1,30 @@
#============================================================================
# This library is free software; you can redistribute it and/or
# modify it under the terms of version 2.1 of the GNU Lesser General Public
# License as published by the Free Software Foundation.
# Copyright (c) Citrix Systems, Inc.
# All rights reserved.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#============================================================================
# Copyright (C) 2006-2007 XenSource Inc.
#============================================================================
# 1) Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# 2) Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
# --------------------------------------------------------------------
# Parts of this file are based upon xmlrpclib.py, the XML-RPC client
# interface included in the Python distribution.
#
@@ -45,27 +55,16 @@
# --------------------------------------------------------------------
import gettext
import xmlrpclib
import httplib
import six.moves.xmlrpc_client as xmlrpclib
import six.moves.http_client as httplib
import socket
import sys
translation = gettext.translation('xen-xm', fallback = True)
API_VERSION_1_1 = '1.1'
API_VERSION_1_2 = '1.2'
#
# Methods that have different parameters between API versions 1.1 and 1.2, and
# the number of parameters in 1.1.
#
COMPATIBILITY_METHODS_1_1 = [
('SR.create' , 8),
('SR.introduce' , 6),
('SR.make' , 7),
('VDI.snapshot' , 1),
('VDI.clone' , 1),
]
class Failure(Exception):
def __init__(self, details):
self.details = details
@@ -73,17 +72,18 @@ class Failure(Exception):
def __str__(self):
try:
return str(self.details)
except Exception, exn:
import sys
print >>sys.stderr, exn
return "Xen-API failure: %s" % str(self.details)
except Exception as exn:
msg = "Xen-API failure: %s" % exn
sys.stderr.write(msg)
return msg
def _details_map(self):
return dict([(str(i), self.details[i])
for i in range(len(self.details))])
_RECONNECT_AND_RETRY = (lambda _ : ())
# Just a "constant" that we use to decide whether to retry the RPC
_RECONNECT_AND_RETRY = object()
class UDSHTTPConnection(httplib.HTTPConnection):
"""HTTPConnection subclass to allow HTTP over Unix domain sockets. """
@@ -92,12 +92,26 @@ class UDSHTTPConnection(httplib.HTTPConnection):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(path)
class UDSHTTP(httplib.HTTP):
class UDSHTTP(httplib.HTTPConnection):
_connection_class = UDSHTTPConnection
class UDSTransport(xmlrpclib.Transport):
def __init__(self, use_datetime=0):
self._use_datetime = use_datetime
self._extra_headers=[]
self._connection = (None, None)
def add_extra_header(self, key, value):
self._extra_headers += [ (key,value) ]
def make_connection(self, host):
return UDSHTTP(host)
# Python 2.4 compatibility
if sys.version_info[0] <= 2 and sys.version_info[1] < 7:
return UDSHTTP(host)
else:
return UDSHTTPConnection(host)
def send_request(self, connection, handler, request_body):
connection.putrequest("POST", handler)
for key, value in self._extra_headers:
connection.putheader(key, value)
class Session(xmlrpclib.ServerProxy):
"""A server proxy and session manager for communicating with xapi using
@@ -106,15 +120,26 @@ class Session(xmlrpclib.ServerProxy):
Example:
session = Session('http://localhost/')
session.login_with_password('me', 'mypassword')
session.login_with_password('me', 'mypassword', '1.0', 'xen-api-scripts-xenapi.py')
session.xenapi.VM.start(vm_uuid)
session.xenapi.session.logout()
"""
def __init__(self, uri, transport=None, encoding=None, verbose=0,
allow_none=1):
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding,
verbose, allow_none)
allow_none=1, ignore_ssl=False):
# Fix for CA-172901 (+ Python 2.4 compatibility)
# Fix for context=ctx ( < Python 2.7.9 compatibility)
if not (sys.version_info[0] <= 2 and sys.version_info[1] <= 7 and sys.version_info[2] <= 9 ) \
and ignore_ssl:
import ssl
ctx = ssl._create_unverified_context()
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding,
verbose, allow_none, context=ctx)
else:
xmlrpclib.ServerProxy.__init__(self, uri, transport, encoding,
verbose, allow_none)
self.transport = transport
self._session = None
self.last_login_method = None
self.last_login_params = None
@@ -125,7 +150,7 @@ class Session(xmlrpclib.ServerProxy):
if methodname.startswith('login'):
self._login(methodname, params)
return None
elif methodname == 'logout':
elif methodname == 'logout' or methodname == 'session.logout':
self._logout()
return None
else:
@@ -133,7 +158,7 @@ class Session(xmlrpclib.ServerProxy):
while retry_count < 3:
full_params = (self._session,) + params
result = _parse_result(getattr(self, methodname)(*full_params))
if result == _RECONNECT_AND_RETRY:
if result is _RECONNECT_AND_RETRY:
retry_count += 1
if self.last_login_method:
self._login(self.last_login_method,
@@ -145,21 +170,24 @@ class Session(xmlrpclib.ServerProxy):
raise xmlrpclib.Fault(
500, 'Tried 3 times to get a valid session, but failed')
def _login(self, method, params):
result = _parse_result(getattr(self, 'session.%s' % method)(*params))
if result == _RECONNECT_AND_RETRY:
raise xmlrpclib.Fault(
500, 'Received SESSION_INVALID when logging in')
self._session = result
self.last_login_method = method
self.last_login_params = params
if method.startswith("slave_local"):
self.API_version = API_VERSION_1_2
else:
try:
result = _parse_result(
getattr(self, 'session.%s' % method)(*params))
if result is _RECONNECT_AND_RETRY:
raise xmlrpclib.Fault(
500, 'Received SESSION_INVALID when logging in')
self._session = result
self.last_login_method = method
self.last_login_params = params
self.API_version = self._get_api_version()
except socket.error as e:
if e.errno == socket.errno.ETIMEDOUT:
raise xmlrpclib.Fault(504, 'The connection timed out')
else:
raise e
def logout(self):
def _logout(self):
try:
if self.last_login_method.startswith("slave_local"):
return _parse_result(self.session.local_logout(self._session))
@@ -174,11 +202,9 @@ class Session(xmlrpclib.ServerProxy):
def _get_api_version(self):
pool = self.xenapi.pool.get_all()[0]
host = self.xenapi.pool.get_master(pool)
if (self.xenapi.host.get_API_version_major(host) == "1" and
self.xenapi.host.get_API_version_minor(host) == "2"):
return API_VERSION_1_2
else:
return API_VERSION_1_1
major = self.xenapi.host.get_API_version_major(host)
minor = self.xenapi.host.get_API_version_minor(host)
return "%s.%s"%(major,minor)
def __getattr__(self, name):
if name == 'handle':
@@ -187,11 +213,13 @@ class Session(xmlrpclib.ServerProxy):
return _Dispatcher(self.API_version, self.xenapi_request, None)
elif name.startswith('login') or name.startswith('slave_local'):
return lambda *params: self._login(name, params)
elif name == 'logout':
return _Dispatcher(self.API_version, self.xenapi_request, "logout")
else:
return xmlrpclib.ServerProxy.__getattr__(self, name)
def xapi_local():
return Session("http://_var_xapi_xapi/", transport=UDSTransport())
return Session("http://_var_lib_xcp_xapi/", transport=UDSTransport())
def _parse_result(result):
if type(result) != dict or 'Status' not in result:
@@ -233,10 +261,4 @@ class _Dispatcher:
return _Dispatcher(self.__API_version, self.__send, "%s.%s" % (self.__name, name))
def __call__(self, *args):
if self.__API_version == API_VERSION_1_1:
for m in COMPATIBILITY_METHODS_1_1:
if self.__name == m[0]:
return self.__send(self.__name, args[0:m[1]])
return self.__send(self.__name, args)
+7 -7
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -24,13 +24,13 @@ import sys
try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
import paramiko
except ImportError,e:
print "Error : can not load paramiko library %s" % e
except ImportError as e:
print(("Error : can not load paramiko library %s" % e))
raise
sys.stderr = sys.__stderr__
from common import *
from libtisbackup.common import *
class backup_mysql(backup_generic):
"""Backup a mysql database as gzipped sql file through ssh"""
@@ -52,7 +52,7 @@ class backup_mysql(backup_generic):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print 'mkdir "%s"' % self.dest_dir
print(('mkdir "%s"' % self.dest_dir))
else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir)
@@ -101,7 +101,7 @@ class backup_mysql(backup_generic):
self.logger.debug('[%s] Dump DB : %s',self.backup_name,cmd)
if not self.dry_run:
(error_code,output) = ssh_exec(cmd,ssh=self.ssh)
print output
print(output)
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
@@ -176,4 +176,4 @@ class backup_mysql(backup_generic):
self.logger.info('Skipping %s, already registered',dir_name)
register_driver(backup_mysql)
register_driver(backup_mysql)
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -20,7 +20,7 @@
import os
import datetime
from common import *
from .common import *
class backup_null(backup_generic):
+7 -7
View File
@@ -21,8 +21,8 @@ import sys
try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
import paramiko
except ImportError,e:
print "Error : can not load paramiko library %s" % e
except ImportError as e:
print(("Error : can not load paramiko library %s" % e))
raise
sys.stderr = sys.__stderr__
@@ -30,7 +30,7 @@ sys.stderr = sys.__stderr__
import datetime
import base64
import os
from common import *
from libtisbackup.common import *
import re
class backup_oracle(backup_generic):
@@ -49,8 +49,8 @@ class backup_oracle(backup_generic):
try:
mykey = paramiko.RSAKey.from_private_key_file(self.private_key)
except paramiko.SSHException:
#mykey = paramiko.DSSKey.from_private_key_file(self.private_key)
mykey = paramiko.Ed25519Key.from_private_key_file(self.private_key)
#mykey = paramiko.DSSKey.from_private_key_file(self.private_key)
mykey = paramiko.Ed25519Key.from_private_key_file(self.private_key)
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
@@ -66,7 +66,7 @@ class backup_oracle(backup_generic):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print 'mkdir "%s"' % self.dest_dir
print(('mkdir "%s"' % self.dest_dir))
else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir)
# dump db
@@ -171,4 +171,4 @@ class backup_oracle(backup_generic):
if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
register_driver(backup_oracle)
register_driver(backup_oracle)
+5 -5
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -21,13 +21,13 @@ import sys
try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
import paramiko
except ImportError,e:
print "Error : can not load paramiko library %s" % e
except ImportError as e:
print(("Error : can not load paramiko library %s" % e))
raise
sys.stderr = sys.__stderr__
from common import *
from .common import *
class backup_pgsql(backup_generic):
"""Backup a postgresql database as gzipped sql file through ssh"""
@@ -46,7 +46,7 @@ class backup_pgsql(backup_generic):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print 'mkdir "%s"' % self.dest_dir
print(('mkdir "%s"' % self.dest_dir))
else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir)
-127
View File
@@ -1,127 +0,0 @@
# -----------------------------------------------------------------------
# This file is part of TISBackup
#
# TISBackup is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# TISBackup is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
#
# -----------------------------------------------------------------------
import os
import datetime
from common import *
import time
class backup_rdiff:
backup_dir=''
backup_start_date=None
backup_name=''
server_name=''
exclude_list=''
ssh_port='22'
remote_user='root'
remote_dir=''
dest_dir=''
verbose = False
dry_run=False
def __init__(self, backup_name, backup_base_dir):
self.backup_dir = backup_base_dir + '/' + backup_name
if os.path.isdir(self.backup_dir )==False:
os.makedirs(self.backup_dir)
self.backup_name = backup_name
t = datetime.datetime.now()
self.backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
def get_latest_backup(self):
filelist = os.listdir(self.backup_dir)
if len(filelist) == 0:
return ''
filelist.sort()
return filelist[-1]
def cleanup_backup(self):
filelist = os.listdir(self.backup_dir)
if len(filelist) == 0:
return ''
filelist.sort()
for backup_date in filelist:
today = time.time()
print backup_date
datestring = backup_date[0:8]
c = time.strptime(datestring,"%Y%m%d")
# TODO: improve
if today - c < 60 * 60 * 24* 30:
print time.strftime("%Y%m%d",c) + " is to be deleted"
def copy_latest_to_new(self):
# TODO check that latest exist
# TODO check that new does not exist
last_backup = self.get_latest_backup()
if last_backup=='':
print "*********************************"
print "*first backup for " + self.backup_name
else:
latest_backup_path = self.backup_dir + '/' + last_backup
new_backup_path = self.backup_dir + '/' + self.backup_start_date
print "#cp -al starting"
cmd = 'cp -al ' + latest_backup_path + ' ' + new_backup_path
print cmd
if self.dry_run==False:
call_external_process(cmd)
print "#cp -al finished"
def rsync_to_new(self):
self.dest_dir = self.backup_dir + '/' + self.backup_start_date + '/'
src_server = self.remote_user + '@' + self.server_name + ':"' + self.remote_dir.strip() + '/"'
print "#starting rsync"
verbose_arg=""
if self.verbose==True:
verbose_arg = "-P "
cmd = "rdiff-backup " + verbose_arg + ' --compress-level=9 --numeric-ids -az --partial -e "ssh -o StrictHostKeyChecking=no -p ' + self.ssh_port + ' -i ' + self.private_key + '" --stats --delete-after ' + self.exclude_list + ' ' + src_server + ' ' + self.dest_dir
print cmd
## deal with exit code 24 (file vanished)
if self.dry_run==False:
p = subprocess.call(cmd, shell=True)
if (p ==24):
print "Note: some files vanished before transfer"
if (p != 0 and p != 24 ):
raise Exception('shell program exited with error code ' + str(p), cmd)
print "#finished rsync"
def process_backup(self):
print ""
print "#========Starting backup item ========="
self.copy_latest_to_new()
self.rsync_to_new()
print "#========Backup item finished=========="
+13 -13
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -20,7 +20,7 @@
import os
import datetime
from common import *
from libtisbackup.common import *
import time
import logging
import re
@@ -69,7 +69,7 @@ class backup_rsync(backup_generic):
if not self.dry_run:
os.makedirs(dest_dir)
else:
print 'mkdir "%s"' % dest_dir
print(('mkdir "%s"' % dest_dir))
else:
raise Exception('backup destination directory already exists : %s' % dest_dir)
@@ -80,7 +80,7 @@ class backup_rsync(backup_generic):
if self.dry_run:
options.append('-d')
if self.overload_args <> None:
if self.overload_args != None:
options.append(self.overload_args)
elif not "cygdrive" in self.remote_dir:
# we don't preserve owner, group, links, hardlinks, perms for windows/cygwin as it is not reliable nor useful
@@ -118,7 +118,7 @@ class backup_rsync(backup_generic):
try:
# newsettings with exclude_list='too','titi', parsed as a str python list content
excludes = eval('[%s]' % self.exclude_list)
except Exception,e:
except Exception as e:
raise Exception('Error reading exclude list : value %s, eval error %s (don\'t forget quotes and comma...)' % (self.exclude_list,e))
options.extend(['--exclude="%s"' % x for x in excludes])
@@ -146,13 +146,13 @@ class backup_rsync(backup_generic):
ssh_params.append('-i %s' % self.private_key)
if self.cipher_spec:
ssh_params.append('-c %s' % self.cipher_spec)
if self.ssh_port <> 22:
if self.ssh_port != 22:
ssh_params.append('-p %i' % self.ssh_port)
options.append('-e "/usr/bin/ssh %s"' % (" ".join(ssh_params)))
backup_source = '%s@%s:%s' % (self.remote_user,self.server_name,self.remote_dir)
# ensure there is a slash at end
if backup_source[-1] <> '/':
if backup_source[-1] != '/':
backup_source += '/'
options_params = " ".join(options)
@@ -165,7 +165,7 @@ class backup_rsync(backup_generic):
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
def ondata(data,context):
if context.verbose:
print data
print(data)
context.logger.debug(data)
log = monitor_stdout(process,ondata,self)
@@ -195,7 +195,7 @@ class backup_rsync(backup_generic):
self.logger.error("[" + self.backup_name + "] shell program exited with error code " + str(returncode))
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:])
else:
print cmd
print(cmd)
#we suppress the .rsync suffix if everything went well
finaldest = os.path.join(self.backup_dir,self.backup_start_date)
@@ -203,14 +203,14 @@ class backup_rsync(backup_generic):
if not self.dry_run:
os.rename(dest_dir, finaldest)
self.logger.debug("[%s] touching datetime of target directory %s" ,self.backup_name,finaldest)
print os.popen('touch "%s"' % finaldest).read()
print((os.popen('touch "%s"' % finaldest).read()))
else:
print "mv" ,dest_dir,finaldest
print(("mv" ,dest_dir,finaldest))
stats['backup_location'] = finaldest
stats['status']='OK'
stats['log']='ssh+rsync backup from %s OK, %d bytes written for %d changed files' % (backup_source,stats['written_bytes'],stats['written_files_count'])
except BaseException , e:
except BaseException as e:
stats['status']='ERROR'
stats['log']=str(e)
raise
@@ -340,5 +340,5 @@ if __name__=='__main__':
b = backup_rsync('htouvet','/backup/data/htouvet',dbstat)
b.read_config(cp)
b.process_backup()
print b.checknagios()
print((b.checknagios()))
+14 -14
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -20,13 +20,13 @@
import os
import datetime
from common import *
from .common import *
import time
import logging
import re
import os.path
import datetime
from common import *
from .common import *
class backup_rsync_btrfs(backup_generic):
@@ -78,7 +78,7 @@ class backup_rsync_btrfs(backup_generic):
else:
self.logger.info("[" + self.backup_name + "] create btrs volume: %s"%dest_dir)
else:
print 'btrfs subvolume create "%s"' %dest_dir
print(('btrfs subvolume create "%s"' %dest_dir))
@@ -89,7 +89,7 @@ class backup_rsync_btrfs(backup_generic):
if self.dry_run:
options.append('-d')
if self.overload_args <> None:
if self.overload_args != None:
options.append(self.overload_args)
elif not "cygdrive" in self.remote_dir:
# we don't preserve owner, group, links, hardlinks, perms for windows/cygwin as it is not reliable nor useful
@@ -128,7 +128,7 @@ class backup_rsync_btrfs(backup_generic):
try:
# newsettings with exclude_list='too','titi', parsed as a str python list content
excludes = eval('[%s]' % self.exclude_list)
except Exception,e:
except Exception as e:
raise Exception('Error reading exclude list : value %s, eval error %s (don\'t forget quotes and comma...)' % (self.exclude_list,e))
options.extend(['--exclude="%s"' % x for x in excludes])
@@ -154,13 +154,13 @@ class backup_rsync_btrfs(backup_generic):
ssh_params.append('-i %s' % self.private_key)
if self.cipher_spec:
ssh_params.append('-c %s' % self.cipher_spec)
if self.ssh_port <> 22:
if self.ssh_port != 22:
ssh_params.append('-p %i' % self.ssh_port)
options.append('-e "/usr/bin/ssh %s"' % (" ".join(ssh_params)))
backup_source = '%s@%s:%s' % (self.remote_user,self.server_name,self.remote_dir)
# ensure there is a slash at end
if backup_source[-1] <> '/':
if backup_source[-1] != '/':
backup_source += '/'
options_params = " ".join(options)
@@ -173,7 +173,7 @@ class backup_rsync_btrfs(backup_generic):
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
def ondata(data,context):
if context.verbose:
print data
print(data)
context.logger.debug(data)
log = monitor_stdout(process,ondata,self)
@@ -203,7 +203,7 @@ class backup_rsync_btrfs(backup_generic):
self.logger.error("[" + self.backup_name + "] shell program exited with error code ", str(returncode))
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd, log[-512:])
else:
print cmd
print(cmd)
#we take a snapshot of last_backup if everything went well
finaldest = os.path.join(self.backup_dir,self.backup_start_date)
@@ -220,16 +220,16 @@ class backup_rsync_btrfs(backup_generic):
else:
self.logger.info("[" + self.backup_name + "] snapshot directory created %s"%finaldest)
else:
print "btrfs snapshot of %s to %s"%(dest_dir,finaldest)
print(("btrfs snapshot of %s to %s"%(dest_dir,finaldest)))
else:
raise Exception('snapshot directory already exists : %s' %finaldest)
self.logger.debug("[%s] touching datetime of target directory %s" ,self.backup_name,finaldest)
print os.popen('touch "%s"' % finaldest).read()
print((os.popen('touch "%s"' % finaldest).read()))
stats['backup_location'] = finaldest
stats['status']='OK'
stats['log']='ssh+rsync+btrfs backup from %s OK, %d bytes written for %d changed files' % (backup_source,stats['written_bytes'],stats['written_files_count'])
except BaseException , e:
except BaseException as e:
stats['status']='ERROR'
stats['log']=str(e)
raise
@@ -358,5 +358,5 @@ if __name__=='__main__':
b = backup_rsync('htouvet','/backup/data/htouvet',dbstat)
b.read_config(cp)
b.process_backup()
print b.checknagios()
print((b.checknagios()))
+6 -6
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -24,13 +24,13 @@ import sys
try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
import paramiko
except ImportError,e:
print "Error : can not load paramiko library %s" % e
except ImportError as e:
print("Error : can not load paramiko library %s" % e)
raise
sys.stderr = sys.__stderr__
from common import *
from .common import *
class backup_samba4(backup_generic):
"""Backup a samba4 databases as gzipped tdbs file through ssh"""
@@ -47,7 +47,7 @@ class backup_samba4(backup_generic):
if not self.dry_run:
os.makedirs(self.dest_dir)
else:
print 'mkdir "%s"' % self.dest_dir
print('mkdir "%s"' % self.dest_dir)
else:
raise Exception('backup destination directory already exists : %s' % self.dest_dir)
@@ -88,7 +88,7 @@ class backup_samba4(backup_generic):
self.logger.debug('[%s] Dump DB : %s',self.backup_name,cmd)
if not self.dry_run:
(error_code,output) = ssh_exec(cmd,ssh=self.ssh)
print output
print(output)
self.logger.debug("[%s] Output of %s :\n%s",self.backup_name,cmd,output)
if error_code:
raise Exception('Aborting, Not null exit code (%i) for "%s"' % (error_code,cmd))
+9 -10
View File
@@ -24,8 +24,8 @@ import sys
try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
import paramiko
except ImportError,e:
print "Error : can not load paramiko library %s" % e
except ImportError as e:
print("Error : can not load paramiko library %s" % e)
raise
sys.stderr = sys.__stderr__
@@ -33,7 +33,7 @@ sys.stderr = sys.__stderr__
import datetime
import base64
import os
from common import *
from .common import *
class backup_sqlserver(backup_generic):
"""Backup a SQLSERVER database as gzipped sql file through ssh"""
@@ -67,18 +67,17 @@ class backup_sqlserver(backup_generic):
backup_start_date = t.strftime('%Y%m%d-%Hh%Mm%S')
backup_file = self.remote_backup_dir + '/' + self.db_name + '-' + backup_start_date + '.bak'
if not self.db_user == '':
self.userdb = '-U %s -P %s' % ( self.db_user, self.db_password )
if not self.db_user == '':
self.userdb = '-U %s -P %s' % ( self.db_user, self.db_password )
# dump db
stats['status']='Dumping'
if self.sqlserver_before_2005:
cmd = """osql -E -Q "BACKUP DATABASE [%s]
if self.sqlserver_before_2005:
cmd = """osql -E -Q "BACKUP DATABASE [%s]
TO DISK='%s'
WITH FORMAT" """ % ( self.db_name, backup_file )
else:
cmd = """sqlcmd %s -S "%s" -d master -Q "BACKUP DATABASE [%s]
else:
cmd = """sqlcmd %s -S "%s" -d master -Q "BACKUP DATABASE [%s]
TO DISK = N'%s'
WITH INIT, NOUNLOAD ,
NAME = N'Backup %s', NOSKIP ,STATS = 10, NOFORMAT" """ % (self.userdb, self.db_server_name, self.db_name, backup_file ,self.db_name )
+7 -6
View File
@@ -20,15 +20,15 @@
import os
import datetime
from common import *
import XenAPI
from .common import *
from . import XenAPI
import time
import logging
import re
import os.path
import datetime
import select
import urllib2, urllib
import urllib.request, urllib.error, urllib.parse, urllib.request, urllib.parse, urllib.error
import base64
import socket
import requests
@@ -149,8 +149,9 @@ class backup_switch(backup_generic):
else:
child.sendline(self.switch_user)
child.expect(".*#")
child.sendline( "terminal datadump")
child.expect("#")
child.sendline( "terminal datadump")
child.expect("#")
child.sendline( "show startup-config")
child.expect("#")
lines = child.before
@@ -237,7 +238,7 @@ class backup_switch(backup_generic):
stats['log']='Switch backup from %s OK, %d bytes written' % (self.server_name,stats['written_bytes'])
except BaseException , e:
except BaseException as e:
stats['status']='ERROR'
stats['log']=str(e)
raise
+5 -5
View File
@@ -17,8 +17,8 @@
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
#
# -----------------------------------------------------------------------
from __future__ import with_statement
from common import *
from .common import *
import pyVmomi
from pyVmomi import vim
from pyVmomi import vmodl
@@ -101,7 +101,7 @@ class backup_vmdk(backup_generic):
ovfDescParams = vim.OvfManager.CreateDescriptorParams()
ovf = si.content.ovfManager.CreateDescriptor(vm, ovfDescParams)
root = ET.fromstring(ovf.ovfDescriptor)
new_id = root[0][1].attrib.values()[0][1:3]
new_id = list(root[0][1].attrib.values())[0][1:3]
ovfFiles = []
for vmdk in vmdks:
old_id = vmdk['id'][1:3]
@@ -211,7 +211,7 @@ class backup_vmdk(backup_generic):
if not self.dry_run:
os.makedirs(dest_dir)
else:
print 'mkdir "%s"' % dest_dir
print('mkdir "%s"' % dest_dir)
else:
raise Exception('backup destination directory already exists : %s' % dest_dir)
os.chdir(dest_dir)
@@ -271,7 +271,7 @@ class backup_vmdk(backup_generic):
stats['status']='OK'
except BaseException , e:
except BaseException as e:
stats['status']='ERROR'
stats['log']=str(e)
raise
+2 -2
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -20,7 +20,7 @@
from common import *
from .common import *
import paramiko
class backup_xcp_metadata(backup_generic):
+20 -14
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -17,20 +17,21 @@
# along with TISBackup. If not, see <http://www.gnu.org/licenses/>.
#
# -----------------------------------------------------------------------
from __future__ import with_statement
import logging
import re
import os
import datetime
import urllib
import urllib.request, urllib.parse, urllib.error
import socket
import tarfile
import hashlib
from stat import *
import ssl
import requests
from common import *
import XenAPI
from .common import *
from . import XenAPI
if hasattr(ssl, '_create_unverified_context'):
ssl._create_default_https_context = ssl._create_unverified_context
@@ -72,7 +73,7 @@ class backup_xva(backup_generic):
session = XenAPI.Session('https://'+self.xcphost)
try:
session.login_with_password(user_xen,password_xen)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
msg,ip = error.details
if msg == 'HOST_IS_SLAVE':
@@ -117,7 +118,7 @@ class backup_xva(backup_generic):
if not 'NULL' in vdi:
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(old_snapshot)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
return("error when destroy snapshot %s"%(error))
now = datetime.datetime.now()
@@ -125,7 +126,7 @@ class backup_xva(backup_generic):
try:
snapshot = session.xenapi.VM.snapshot(vm,"tisbackup-%s"%(vdi_name))
self.logger.debug("[%s] got snapshot %s", vdi_name, snapshot)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
return("error when snapshot %s"%(error))
#get snapshot opaqueRef
vm = session.xenapi.VM.get_by_name_label("tisbackup-%s"%(vdi_name))[0]
@@ -135,7 +136,7 @@ class backup_xva(backup_generic):
if status_vm == "Running":
self.logger.debug("[%s] Shudown in progress",self.backup_name)
if dry_run:
print "session.xenapi.VM.clean_shutdown(vm)"
print("session.xenapi.VM.clean_shutdown(vm)")
else:
session.xenapi.VM.clean_shutdown(vm)
try:
@@ -150,8 +151,13 @@ class backup_xva(backup_generic):
scheme = "https://"
url = scheme+user_xen+":"+password_xen+"@"+self.xcphost+"/export?use_compression="+self.use_compression+"&uuid="+session.xenapi.VM.get_uuid(vm)
urllib.urlretrieve(url, filename_temp)
urllib.urlcleanup()
top_level_url = scheme+self.xcphost+"/export?use_compression="+self.use_compression+"&uuid="+session.xenapi.VM.get_uuid(vm)
r = requests.get(top_level_url, auth=(user_xen, password_xen))
open(filename_temp, 'wb').write(r.content)
except Exception as e:
self.logger.error("[%s] error when fetching snap: %s", "tisbackup-%s"%(vdi_name), e)
@@ -171,13 +177,13 @@ class backup_xva(backup_generic):
if not 'NULL' in vdi:
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(snapshot)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
return("error when destroy snapshot %s"%(error))
elif status_vm == "Running":
self.logger.debug("[%s] Starting in progress",self.backup_name)
if dry_run:
print "session.xenapi.Async.VM.start(vm,False,True)"
print("session.xenapi.Async.VM.start(vm,False,True)")
else:
session.xenapi.Async.VM.start(vm,False,True)
@@ -219,7 +225,7 @@ class backup_xva(backup_generic):
else:
raise Exception(cmd)
except BaseException , e:
except BaseException as e:
stats['status']='ERROR'
stats['log']=str(e)
raise
+26 -18
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -18,6 +18,7 @@
#
# -----------------------------------------------------------------------
from abc import ABC, abstractmethod
import os
import subprocess
import re
@@ -34,8 +35,8 @@ import sys
try:
sys.stderr = open('/dev/null') # Silence silly warnings from paramiko
import paramiko
except ImportError,e:
print "Error : can not load paramiko library %s" % e
except ImportError as e:
print(("Error : can not load paramiko library %s" % e))
raise
sys.stderr = sys.__stderr__
@@ -121,7 +122,7 @@ def check_string(test_string):
pattern = r'[^\.A-Za-z0-9\-_]'
if re.search(pattern, test_string):
#Character other then . a-z 0-9 was found
print 'Invalid : %r' % (test_string,)
print(('Invalid : %r' % (test_string,)))
def convert_bytes(bytes):
if bytes is None:
@@ -207,7 +208,7 @@ def html_table(cur,callback=None):
yield dict((cur.description[idx][0], value)
for idx, value in enumerate(row))
head=u"<tr>"+"".join(["<th>"+c[0]+"</th>" for c in cur.description])+"</tr>"
head="<tr>"+"".join(["<th>"+c[0]+"</th>" for c in cur.description])+"</tr>"
lines=""
if callback:
for r in itermap(cur):
@@ -237,7 +238,7 @@ def monitor_stdout(aprocess, onoutputdata,context):
while read_set:
try:
rlist, wlist, xlist = select.select(read_set, [], [])
except select.error, e:
except select.error as e:
if e.args[0] == errno.EINTR:
continue
raise
@@ -245,12 +246,14 @@ def monitor_stdout(aprocess, onoutputdata,context):
# Reads one line from stdout
if aprocess.stdout in rlist:
data = os.read(aprocess.stdout.fileno(), 1)
data = data.decode('utf-8')
if data == "":
aprocess.stdout.close()
read_set.remove(aprocess.stdout)
while data and not data in ('\n','\r'):
line += data
data = os.read(aprocess.stdout.fileno(), 1)
data = data.decode('utf-8')
if line or data in ('\n','\r'):
stdout.append(line)
if onoutputdata:
@@ -260,12 +263,14 @@ def monitor_stdout(aprocess, onoutputdata,context):
# Reads one line from stderr
if aprocess.stderr in rlist:
data = os.read(aprocess.stderr.fileno(), 1)
data = data.decode('utf-8')
if data == "":
aprocess.stderr.close()
read_set.remove(aprocess.stderr)
while data and not data in ('\n','\r'):
line += data
data = os.read(aprocess.stderr.fileno(), 1)
data = data.decode('utf-8')
if line or data in ('\n','\r'):
stdout.append(line)
if onoutputdata:
@@ -442,7 +447,7 @@ CREATE INDEX idx_stats_backup_name_start on stats(backup_name,backup_start);""")
return value
#for r in self.query('select * from stats where backup_name=? order by backup_end desc limit ?',(backup_name,count)):
print pp(cur,None,1,fcb)
print((pp(cur,None,1,fcb)))
def fcb(self,fields,fieldname,value):
@@ -492,12 +497,13 @@ def ssh_exec(command,ssh=None,server_name='',remote_user='',private_key='',ssh_p
chan.exec_command(command)
stdout.flush()
output = stdout.read()
output_base = stdout.read()
output = output_base.decode(encoding='UTF-8').replace("'","")
exit_code = chan.recv_exit_status()
return (exit_code,output)
class backup_generic:
class backup_generic(ABC):
"""Generic ancestor class for backups, not registered"""
type = 'generic'
required_params = ['type','backup_name','backup_dir','server_name','backup_retention_time','maximum_backup_age']
@@ -696,7 +702,7 @@ class backup_generic:
self.logger.info('[%s] ######### Backup finished : %s',self.backup_name,stats['log'])
return stats
except BaseException, e:
except BaseException as e:
stats['status']='ERROR'
stats['log']=str(e)
endtime = time.time()
@@ -798,7 +804,7 @@ class backup_generic:
if not self.dry_run:
self.dbstat.db.execute('update stats set TYPE="CLEAN" where backup_name=? and backup_location=?',(self.backup_name,oldbackup_location))
self.dbstat.db.commit()
except BaseException,e:
except BaseException as e:
self.logger.error('cleanup_backup : Unable to remove directory/file "%s". Error %s', oldbackup_location,e)
removed.append((self.backup_name,oldbackup_location))
else:
@@ -809,10 +815,12 @@ class backup_generic:
self.logger.info('[%s] Cleanup finished : removed : %s' , self.backup_name,','.join([('[%s]-"%s"') % r for r in removed]) or 'Nothing')
return removed
@abstractmethod
def register_existingbackups(self):
"""scan existing backups and insert stats in database"""
registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',[self.backup_name])]
raise Exception('Abstract method')
pass
# """scan existing backups and insert stats in database"""
# registered = [b['backup_location'] for b in self.dbstat.query('select distinct backup_location from stats where backup_name=?',[self.backup_name])]
# raise Exception('Abstract method')
def export_latestbackup(self,destdir):
"""Copy (rsync) latest OK backup to external storage located at locally mounted "destdir"
@@ -846,9 +854,9 @@ class backup_generic:
raise Exception('Backup source %s doesn\'t exists' % backup_source)
# ensure there is a slash at end
if os.path.isdir(backup_source) and backup_source[-1] <> '/':
if os.path.isdir(backup_source) and backup_source[-1] != '/':
backup_source += '/'
if backup_dest[-1] <> '/':
if backup_dest[-1] != '/':
backup_dest += '/'
if not os.path.isdir(backup_dest):
@@ -874,7 +882,7 @@ class backup_generic:
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, close_fds=True)
def ondata(data,context):
if context.verbose:
print data
print(data)
context.logger.debug(data)
log = monitor_stdout(process,ondata,self)
@@ -898,7 +906,7 @@ class backup_generic:
self.logger.error("[" + self.backup_name + "] shell program exited with error code ")
raise Exception("[" + self.backup_name + "] shell program exited with error code " + str(returncode), cmd)
else:
print cmd
print(cmd)
stats['status']='OK'
self.logger.info('export backup from %s to %s OK, %d bytes written for %d changed files' % (backup_source,backup_dest,stats['written_bytes'],stats['written_files_count']))
+24 -19
View File
@@ -1,4 +1,4 @@
#!/usr/bin/python
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# -----------------------------------------------------------------------
# This file is part of TISBackup
@@ -20,8 +20,8 @@
import os
import datetime
from common import *
import XenAPI
from .common import *
from . import XenAPI
import time
import logging
import re
@@ -29,7 +29,7 @@ import os.path
import os
import datetime
import select
import urllib2
import urllib.request, urllib.error, urllib.parse
import base64
import socket
from stat import *
@@ -66,7 +66,7 @@ class copy_vm_xcp(backup_generic):
session = XenAPI.Session('https://'+self.server_name)
try:
session.login_with_password(user_xen,password_xen)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
msg,ip = error.details
if msg == 'HOST_IS_SLAVE':
@@ -81,7 +81,7 @@ class copy_vm_xcp(backup_generic):
#get storage opaqueRef
try:
storage = session.xenapi.SR.get_by_name_label(storage_name)[0]
except IndexError,error:
except IndexError as error:
result = (1,"error get SR opaqueref %s"%(error))
return result
@@ -89,14 +89,14 @@ class copy_vm_xcp(backup_generic):
#get vm to copy opaqueRef
try:
vm = session.xenapi.VM.get_by_name_label(vm_name)[0]
except IndexError,error:
except IndexError as error:
result = (1,"error get VM opaqueref %s"%(error))
return result
# get vm backup network opaqueRef
try:
networkRef = session.xenapi.network.get_by_name_label(self.network_name)[0]
except IndexError, error:
except IndexError as error:
result = (1, "error get VM network opaqueref %s" % (error))
return result
@@ -104,9 +104,9 @@ class copy_vm_xcp(backup_generic):
status_vm = session.xenapi.VM.get_power_state(vm)
self.logger.debug("[%s] Status of VM: %s",self.backup_name,status_vm)
if status_vm == "Running":
self.logger.debug("[%s] Shudown in progress",self.backup_name)
self.logger.debug("[%s] Shutdown in progress",self.backup_name)
if dry_run:
print "session.xenapi.VM.clean_shutdown(vm)"
print("session.xenapi.VM.clean_shutdown(vm)")
else:
session.xenapi.VM.clean_shutdown(vm)
snapshot = vm
@@ -115,7 +115,7 @@ class copy_vm_xcp(backup_generic):
self.logger.debug("[%s] Snapshot in progress",self.backup_name)
try:
snapshot = session.xenapi.VM.snapshot(vm,"tisbackup-%s"%(vm_name))
except XenAPI.Failure, error:
except XenAPI.Failure as error:
result = (1,"error when snapshot %s"%(error))
return result
@@ -165,7 +165,7 @@ class copy_vm_xcp(backup_generic):
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(oldest_backup_vm)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
result = (1,"error when destroy old backup vm %s"%(error))
return result
@@ -173,7 +173,7 @@ class copy_vm_xcp(backup_generic):
self.logger.debug("[%s] Copy %s in progress on %s",self.backup_name,vm_name,storage_name)
try:
backup_vm = session.xenapi.VM.copy(snapshot,vm_backup_name+now.strftime("%Y-%m-%d %H:%M"),storage)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
result = (1,"error when copy %s"%(error))
return result
@@ -184,7 +184,7 @@ class copy_vm_xcp(backup_generic):
#change the network of the new VM
try:
vifDestroy = session.xenapi.VM.get_VIFs(backup_vm)
except IndexError,error:
except IndexError as error:
result = (1,"error get VIF opaqueref %s"%(error))
return result
@@ -213,7 +213,7 @@ class copy_vm_xcp(backup_generic):
}
try:
session.xenapi.VIF.create(data)
except Exception, error:
except Exception as error:
result = (1,error)
return result
@@ -237,7 +237,7 @@ class copy_vm_xcp(backup_generic):
return result
#Disable automatic boot
if session.xenapi.VM.get_other_config(backup_vm).has_key('auto_poweron'):
if 'auto_poweron' in session.xenapi.VM.get_other_config(backup_vm):
session.xenapi.VM.remove_from_other_config(backup_vm, "auto_poweron")
if not str2bool(self.halt_vm):
@@ -251,14 +251,14 @@ class copy_vm_xcp(backup_generic):
if not 'NULL' in vdi:
session.xenapi.VDI.destroy(vdi)
session.xenapi.VM.destroy(snapshot)
except XenAPI.Failure, error:
except XenAPI.Failure as error:
result = (1,"error when destroy snapshot %s"%(error))
return result
else:
if status_vm == "Running":
self.logger.debug("[%s] Starting in progress",self.backup_name)
if dry_run:
print "session.xenapi.VM.start(vm,False,True)"
print("session.xenapi.VM.start(vm,False,True)")
else:
session.xenapi.VM.start(vm,False,True)
@@ -282,9 +282,14 @@ class copy_vm_xcp(backup_generic):
stats['status']='ERROR'
stats['log']=cmd[1]
except BaseException,e:
except BaseException as e:
stats['status']='ERROR'
stats['log']=str(e)
raise
def register_existingbackups(self):
"""scan backup dir and insert stats in database"""
#This backup is on target server, no data available on this server
pass
register_driver(copy_vm_xcp)
+25
View File
@@ -0,0 +1,25 @@
# Copyright (c) 2001, 2002, 2003 Python Software Foundation
# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu>
# Copyright (c) 2007 Tim Lauridsen <tla@rasmil.dk>
# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
from .ini import INIConfig, change_comment_syntax
from .config import BasicConfig, ConfigNamespace
from .compat import RawConfigParser, ConfigParser, SafeConfigParser
from .utils import tidy
from .configparser import DuplicateSectionError, \
NoSectionError, NoOptionError, \
InterpolationMissingOptionError, \
InterpolationDepthError, \
InterpolationSyntaxError, \
DEFAULTSECT, MAX_INTERPOLATION_DEPTH
__all__ = [
'BasicConfig', 'ConfigNamespace',
'INIConfig', 'tidy', 'change_comment_syntax',
'RawConfigParser', 'ConfigParser', 'SafeConfigParser',
'DuplicateSectionError', 'NoSectionError', 'NoOptionError',
'InterpolationMissingOptionError', 'InterpolationDepthError',
'InterpolationSyntaxError', 'DEFAULTSECT', 'MAX_INTERPOLATION_DEPTH',
]
+343
View File
@@ -0,0 +1,343 @@
# Copyright (c) 2001, 2002, 2003 Python Software Foundation
# Copyright (c) 2004-2008 Paramjit Oberoi <param.cs.wisc.edu>
# All Rights Reserved. See LICENSE-PSF & LICENSE for details.
"""Compatibility interfaces for ConfigParser
Interfaces of ConfigParser, RawConfigParser and SafeConfigParser
should be completely identical to the Python standard library
versions. Tested with the unit tests included with Python-2.3.4
The underlying INIConfig object can be accessed as cfg.data
"""
import re
from .configparser import DuplicateSectionError, \
NoSectionError, NoOptionError, \
InterpolationMissingOptionError, \
InterpolationDepthError, \
InterpolationSyntaxError, \
DEFAULTSECT, MAX_INTERPOLATION_DEPTH
# These are imported only for compatiability.
# The code below does not reference them directly.
from .configparser import Error, InterpolationError, \
MissingSectionHeaderError, ParsingError
import six
from . import ini
class RawConfigParser(object):
def __init__(self, defaults=None, dict_type=dict):
if dict_type != dict:
raise ValueError('Custom dict types not supported')
self.data = ini.INIConfig(defaults=defaults, optionxformsource=self)
def optionxform(self, optionstr):
return optionstr.lower()
def defaults(self):
d = {}
secobj = self.data._defaults
for name in secobj._options:
d[name] = secobj._compat_get(name)
return d
def sections(self):
"""Return a list of section names, excluding [DEFAULT]"""
return list(self.data)
def add_section(self, section):
"""Create a new section in the configuration.
Raise DuplicateSectionError if a section by the specified name
already exists. Raise ValueError if name is DEFAULT or any of
its case-insensitive variants.
"""
# The default section is the only one that gets the case-insensitive
# treatment - so it is special-cased here.
if section.lower() == "default":
raise ValueError('Invalid section name: %s' % section)
if self.has_section(section):
raise DuplicateSectionError(section)
else:
self.data._new_namespace(section)
def has_section(self, section):
"""Indicate whether the named section is present in the configuration.
The DEFAULT section is not acknowledged.
"""
return section in self.data
def options(self, section):
"""Return a list of option names for the given section name."""
if section in self.data:
return list(self.data[section])
else:
raise NoSectionError(section)
def read(self, filenames):
"""Read and parse a filename or a list of filenames.
Files that cannot be opened are silently ignored; this is
designed so that you can specify a list of potential
configuration file locations (e.g. current directory, user's
home directory, systemwide directory), and all existing
configuration files in the list will be read. A single
filename may also be given.
"""
files_read = []
if isinstance(filenames, six.string_types):
filenames = [filenames]
for filename in filenames:
try:
fp = open(filename)
except IOError:
continue
files_read.append(filename)
self.data._readfp(fp)
fp.close()
return files_read
def readfp(self, fp, filename=None):
"""Like read() but the argument must be a file-like object.
The `fp' argument must have a `readline' method. Optional
second argument is the `filename', which if not given, is
taken from fp.name. If fp has no `name' attribute, `<???>' is
used.
"""
self.data._readfp(fp)
def get(self, section, option, vars=None):
if not self.has_section(section):
raise NoSectionError(section)
sec = self.data[section]
if option in sec:
return sec._compat_get(option)
else:
raise NoOptionError(option, section)
def items(self, section):
if section in self.data:
ans = []
for opt in self.data[section]:
ans.append((opt, self.get(section, opt)))
return ans
else:
raise NoSectionError(section)
def getint(self, section, option):
return int(self.get(section, option))
def getfloat(self, section, option):
return float(self.get(section, option))
_boolean_states = {'1': True, 'yes': True, 'true': True, 'on': True,
'0': False, 'no': False, 'false': False, 'off': False}
def getboolean(self, section, option):
v = self.get(section, option)
if v.lower() not in self._boolean_states:
raise ValueError('Not a boolean: %s' % v)
return self._boolean_states[v.lower()]
def has_option(self, section, option):
"""Check for the existence of a given option in a given section."""
if section in self.data:
sec = self.data[section]
else:
raise NoSectionError(section)
return (option in sec)
def set(self, section, option, value):
"""Set an option."""
if section in self.data:
self.data[section][option] = value
else:
raise NoSectionError(section)
def write(self, fp):
"""Write an .ini-format representation of the configuration state."""
fp.write(str(self.data))
def remove_option(self, section, option):
"""Remove an option."""
if section in self.data:
sec = self.data[section]
else:
raise NoSectionError(section)
if option in sec:
del sec[option]
return 1
else:
return 0
def remove_section(self, section):
"""Remove a file section."""
if not self.has_section(section):
return False
del self.data[section]
return True
class ConfigDict(object):
"""Present a dict interface to a ini section."""
def __init__(self, cfg, section, vars):
self.cfg = cfg
self.section = section
self.vars = vars
def __getitem__(self, key):
try:
return RawConfigParser.get(self.cfg, self.section, key, self.vars)
except (NoOptionError, NoSectionError):
raise KeyError(key)
class ConfigParser(RawConfigParser):
def get(self, section, option, raw=False, vars=None):
"""Get an option value for a given section.
All % interpolations are expanded in the return values, based on the
defaults passed into the constructor, unless the optional argument
`raw' is true. Additional substitutions may be provided using the
`vars' argument, which must be a dictionary whose contents overrides
any pre-existing defaults.
The section DEFAULT is special.
"""
if section != DEFAULTSECT and not self.has_section(section):
raise NoSectionError(section)
option = self.optionxform(option)
value = RawConfigParser.get(self, section, option, vars)
if raw:
return value
else:
d = ConfigDict(self, section, vars)
return self._interpolate(section, option, value, d)
def _interpolate(self, section, option, rawval, vars):
# do the string interpolation
value = rawval
depth = MAX_INTERPOLATION_DEPTH
while depth: # Loop through this until it's done
depth -= 1
if "%(" in value:
try:
value = value % vars
except KeyError as e:
raise InterpolationMissingOptionError(
option, section, rawval, e.args[0])
else:
break
if value.find("%(") != -1:
raise InterpolationDepthError(option, section, rawval)
return value
def items(self, section, raw=False, vars=None):
"""Return a list of tuples with (name, value) for each option
in the section.
All % interpolations are expanded in the return values, based on the
defaults passed into the constructor, unless the optional argument
`raw' is true. Additional substitutions may be provided using the
`vars' argument, which must be a dictionary whose contents overrides
any pre-existing defaults.
The section DEFAULT is special.
"""
if section != DEFAULTSECT and not self.has_section(section):
raise NoSectionError(section)
if vars is None:
options = list(self.data[section])
else:
options = []
for x in self.data[section]:
if x not in vars:
options.append(x)
options.extend(vars.keys())
if "__name__" in options:
options.remove("__name__")
d = ConfigDict(self, section, vars)
if raw:
return [(option, d[option])
for option in options]
else:
return [(option, self._interpolate(section, option, d[option], d))
for option in options]
class SafeConfigParser(ConfigParser):
_interpvar_re = re.compile(r"%\(([^)]+)\)s")
_badpercent_re = re.compile(r"%[^%]|%$")
def set(self, section, option, value):
if not isinstance(value, six.string_types):
raise TypeError("option values must be strings")
# check for bad percent signs:
# first, replace all "good" interpolations
tmp_value = self._interpvar_re.sub('', value)
# then, check if there's a lone percent sign left
m = self._badpercent_re.search(tmp_value)
if m:
raise ValueError("invalid interpolation syntax in %r at "
"position %d" % (value, m.start()))
ConfigParser.set(self, section, option, value)
def _interpolate(self, section, option, rawval, vars):
# do the string interpolation
L = []
self._interpolate_some(option, L, rawval, section, vars, 1)
return ''.join(L)
_interpvar_match = re.compile(r"%\(([^)]+)\)s").match
def _interpolate_some(self, option, accum, rest, section, map, depth):
if depth > MAX_INTERPOLATION_DEPTH:
raise InterpolationDepthError(option, section, rest)
while rest:
p = rest.find("%")
if p < 0:
accum.append(rest)
return
if p > 0:
accum.append(rest[:p])
rest = rest[p:]
# p is no longer used
c = rest[1:2]
if c == "%":
accum.append("%")
rest = rest[2:]
elif c == "(":
m = self._interpvar_match(rest)
if m is None:
raise InterpolationSyntaxError(option, section, "bad interpolation variable reference %r" % rest)
var = m.group(1)
rest = rest[m.end():]
try:
v = map[var]
except KeyError:
raise InterpolationMissingOptionError(
option, section, rest, var)
if "%" in v:
self._interpolate_some(option, accum, v,
section, map, depth + 1)
else:
accum.append(v)
else:
raise InterpolationSyntaxError(
option, section,
"'%' must be followed by '%' or '(', found: " + repr(rest))
+292
View File
@@ -0,0 +1,292 @@
class ConfigNamespace(object):
"""Abstract class representing the interface of Config objects.
A ConfigNamespace is a collection of names mapped to values, where
the values may be nested namespaces. Values can be accessed via
container notation - obj[key] - or via dotted notation - obj.key.
Both these access methods are equivalent.
To minimize name conflicts between namespace keys and class members,
the number of class members should be minimized, and the names of
all class members should start with an underscore.
Subclasses must implement the methods for container-like access,
and this class will automatically provide dotted access.
"""
# Methods that must be implemented by subclasses
def _getitem(self, key):
return NotImplementedError(key)
def __setitem__(self, key, value):
raise NotImplementedError(key, value)
def __delitem__(self, key):
raise NotImplementedError(key)
def __iter__(self):
return NotImplementedError()
def _new_namespace(self, name):
raise NotImplementedError(name)
def __contains__(self, key):
try:
self._getitem(key)
except KeyError:
return False
return True
# Machinery for converting dotted access into container access,
# and automatically creating new sections/namespaces.
#
# To distinguish between accesses of class members and namespace
# keys, we first call object.__getattribute__(). If that succeeds,
# the name is assumed to be a class member. Otherwise it is
# treated as a namespace key.
#
# Therefore, member variables should be defined in the class,
# not just in the __init__() function. See BasicNamespace for
# an example.
def __getitem__(self, key):
try:
return self._getitem(key)
except KeyError:
return Undefined(key, self)
def __getattr__(self, name):
try:
return self._getitem(name)
except KeyError:
if name.startswith('__') and name.endswith('__'):
raise AttributeError
return Undefined(name, self)
def __setattr__(self, name, value):
try:
object.__getattribute__(self, name)
object.__setattr__(self, name, value)
except AttributeError:
self.__setitem__(name, value)
def __delattr__(self, name):
try:
object.__getattribute__(self, name)
object.__delattr__(self, name)
except AttributeError:
self.__delitem__(name)
# During unpickling, Python checks if the class has a __setstate__
# method. But, the data dicts have not been initialised yet, which
# leads to _getitem and hence __getattr__ raising an exception. So
# we explicitly impement default __setstate__ behavior.
def __setstate__(self, state):
self.__dict__.update(state)
class Undefined(object):
"""Helper class used to hold undefined names until assignment.
This class helps create any undefined subsections when an
assignment is made to a nested value. For example, if the
statement is "cfg.a.b.c = 42", but "cfg.a.b" does not exist yet.
"""
def __init__(self, name, namespace):
object.__setattr__(self, 'name', name)
object.__setattr__(self, 'namespace', namespace)
def __setattr__(self, name, value):
obj = self.namespace._new_namespace(self.name)
obj[name] = value
def __setitem__(self, name, value):
obj = self.namespace._new_namespace(self.name)
obj[name] = value
# ---- Basic implementation of a ConfigNamespace
class BasicConfig(ConfigNamespace):
"""Represents a hierarchical collection of named values.
Values are added using dotted notation:
>>> n = BasicConfig()
>>> n.x = 7
>>> n.name.first = 'paramjit'
>>> n.name.last = 'oberoi'
...and accessed the same way, or with [...]:
>>> n.x
7
>>> n.name.first
'paramjit'
>>> n.name.last
'oberoi'
>>> n['x']
7
>>> n['name']['first']
'paramjit'
Iterating over the namespace object returns the keys:
>>> l = list(n)
>>> l.sort()
>>> l
['name', 'x']
Values can be deleted using 'del' and printed using 'print'.
>>> n.aaa = 42
>>> del n.x
>>> print(n)
aaa = 42
name.first = paramjit
name.last = oberoi
Nested namespaces are also namespaces:
>>> isinstance(n.name, ConfigNamespace)
True
>>> print(n.name)
first = paramjit
last = oberoi
>>> sorted(list(n.name))
['first', 'last']
Finally, values can be read from a file as follows:
>>> from six import StringIO
>>> sio = StringIO('''
... # comment
... ui.height = 100
... ui.width = 150
... complexity = medium
... have_python
... data.secret.password = goodness=gracious me
... ''')
>>> n = BasicConfig()
>>> n._readfp(sio)
>>> print(n)
complexity = medium
data.secret.password = goodness=gracious me
have_python
ui.height = 100
ui.width = 150
"""
# this makes sure that __setattr__ knows this is not a namespace key
_data = None
def __init__(self):
self._data = {}
def _getitem(self, key):
return self._data[key]
def __setitem__(self, key, value):
self._data[key] = value
def __delitem__(self, key):
del self._data[key]
def __iter__(self):
return iter(self._data)
def __str__(self, prefix=''):
lines = []
keys = list(self._data.keys())
keys.sort()
for name in keys:
value = self._data[name]
if isinstance(value, ConfigNamespace):
lines.append(value.__str__(prefix='%s%s.' % (prefix,name)))
else:
if value is None:
lines.append('%s%s' % (prefix, name))
else:
lines.append('%s%s = %s' % (prefix, name, value))
return '\n'.join(lines)
def _new_namespace(self, name):
obj = BasicConfig()
self._data[name] = obj
return obj
def _readfp(self, fp):
while True:
line = fp.readline()
if not line:
break
line = line.strip()
if not line: continue
if line[0] == '#': continue
data = line.split('=', 1)
if len(data) == 1:
name = line
value = None
else:
name = data[0].strip()
value = data[1].strip()
name_components = name.split('.')
ns = self
for n in name_components[:-1]:
if n in ns:
ns = ns[n]
if not isinstance(ns, ConfigNamespace):
raise TypeError('value-namespace conflict', n)
else:
ns = ns._new_namespace(n)
ns[name_components[-1]] = value
# ---- Utility functions
def update_config(target, source):
"""Imports values from source into target.
Recursively walks the <source> ConfigNamespace and inserts values
into the <target> ConfigNamespace. For example:
>>> n = BasicConfig()
>>> n.playlist.expand_playlist = True
>>> n.ui.display_clock = True
>>> n.ui.display_qlength = True
>>> n.ui.width = 150
>>> print(n)
playlist.expand_playlist = True
ui.display_clock = True
ui.display_qlength = True
ui.width = 150
>>> from iniparse import ini
>>> i = ini.INIConfig()
>>> update_config(i, n)
>>> print(i)
[playlist]
expand_playlist = True
<BLANKLINE>
[ui]
display_clock = True
display_qlength = True
width = 150
"""
for name in sorted(source):
value = source[name]
if isinstance(value, ConfigNamespace):
if name in target:
myns = target[name]
if not isinstance(myns, ConfigNamespace):
raise TypeError('value-namespace conflict')
else:
myns = target._new_namespace(name)
update_config(myns, value)
else:
target[name] = value
+7
View File
@@ -0,0 +1,7 @@
try:
from ConfigParser import *
# not all objects get imported with __all__
from ConfigParser import Error, InterpolationMissingOptionError
except ImportError:
from configparser import *
from configparser import Error, InterpolationMissingOptionError
+652
View File
@@ -0,0 +1,652 @@
"""Access and/or modify INI files
* Compatiable with ConfigParser
* Preserves order of sections & options
* Preserves comments/blank lines/etc
* More conveninet access to data
Example:
>>> from six import StringIO
>>> sio = StringIO('''# configure foo-application
... [foo]
... bar1 = qualia
... bar2 = 1977
... [foo-ext]
... special = 1''')
>>> cfg = INIConfig(sio)
>>> print(cfg.foo.bar1)
qualia
>>> print(cfg['foo-ext'].special)
1
>>> cfg.foo.newopt = 'hi!'
>>> cfg.baz.enabled = 0
>>> print(cfg)
# configure foo-application
[foo]
bar1 = qualia
bar2 = 1977
newopt = hi!
[foo-ext]
special = 1
<BLANKLINE>
[baz]
enabled = 0
"""
# An ini parser that supports ordered sections/options
# Also supports updates, while preserving structure
# Backward-compatiable with ConfigParser
import re
from .configparser import DEFAULTSECT, ParsingError, MissingSectionHeaderError
import six
from . import config
class LineType(object):
line = None
def __init__(self, line=None):
if line is not None:
self.line = line.strip('\n')
# Return the original line for unmodified objects
# Otherwise construct using the current attribute values
def __str__(self):
if self.line is not None:
return self.line
else:
return self.to_string()
# If an attribute is modified after initialization
# set line to None since it is no longer accurate.
def __setattr__(self, name, value):
if hasattr(self,name):
self.__dict__['line'] = None
self.__dict__[name] = value
def to_string(self):
raise Exception('This method must be overridden in derived classes')
class SectionLine(LineType):
regex = re.compile(r'^\['
r'(?P<name>[^]]+)'
r'\]\s*'
r'((?P<csep>;|#)(?P<comment>.*))?$')
def __init__(self, name, comment=None, comment_separator=None,
comment_offset=-1, line=None):
super(SectionLine, self).__init__(line)
self.name = name
self.comment = comment
self.comment_separator = comment_separator
self.comment_offset = comment_offset
def to_string(self):
out = '[' + self.name + ']'
if self.comment is not None:
# try to preserve indentation of comments
out = (out+' ').ljust(self.comment_offset)
out = out + self.comment_separator + self.comment
return out
def parse(cls, line):
m = cls.regex.match(line.rstrip())
if m is None:
return None
return cls(m.group('name'), m.group('comment'),
m.group('csep'), m.start('csep'),
line)
parse = classmethod(parse)
class OptionLine(LineType):
def __init__(self, name, value, separator=' = ', comment=None,
comment_separator=None, comment_offset=-1, line=None):
super(OptionLine, self).__init__(line)
self.name = name
self.value = value
self.separator = separator
self.comment = comment
self.comment_separator = comment_separator
self.comment_offset = comment_offset
def to_string(self):
out = '%s%s%s' % (self.name, self.separator, self.value)
if self.comment is not None:
# try to preserve indentation of comments
out = (out+' ').ljust(self.comment_offset)
out = out + self.comment_separator + self.comment
return out
regex = re.compile(r'^(?P<name>[^:=\s[][^:=]*)'
r'(?P<sep>[:=]\s*)'
r'(?P<value>.*)$')
def parse(cls, line):
m = cls.regex.match(line.rstrip())
if m is None:
return None
name = m.group('name').rstrip()
value = m.group('value')
sep = m.group('name')[len(name):] + m.group('sep')
# comments are not detected in the regex because
# ensuring total compatibility with ConfigParser
# requires that:
# option = value ;comment // value=='value'
# option = value;1 ;comment // value=='value;1 ;comment'
#
# Doing this in a regex would be complicated. I
# think this is a bug. The whole issue of how to
# include ';' in the value needs to be addressed.
# Also, '#' doesn't mark comments in options...
coff = value.find(';')
if coff != -1 and value[coff-1].isspace():
comment = value[coff+1:]
csep = value[coff]
value = value[:coff].rstrip()
coff = m.start('value') + coff
else:
comment = None
csep = None
coff = -1
return cls(name, value, sep, comment, csep, coff, line)
parse = classmethod(parse)
def change_comment_syntax(comment_chars='%;#', allow_rem=False):
comment_chars = re.sub(r'([\]\-\^])', r'\\\1', comment_chars)
regex = r'^(?P<csep>[%s]' % comment_chars
if allow_rem:
regex += '|[rR][eE][mM]'
regex += r')(?P<comment>.*)$'
CommentLine.regex = re.compile(regex)
class CommentLine(LineType):
regex = re.compile(r'^(?P<csep>[;#])'
r'(?P<comment>.*)$')
def __init__(self, comment='', separator='#', line=None):
super(CommentLine, self).__init__(line)
self.comment = comment
self.separator = separator
def to_string(self):
return self.separator + self.comment
def parse(cls, line):
m = cls.regex.match(line.rstrip())
if m is None:
return None
return cls(m.group('comment'), m.group('csep'), line)
parse = classmethod(parse)
class EmptyLine(LineType):
# could make this a singleton
def to_string(self):
return ''
value = property(lambda self: '')
def parse(cls, line):
if line.strip():
return None
return cls(line)
parse = classmethod(parse)
class ContinuationLine(LineType):
regex = re.compile(r'^\s+(?P<value>.*)$')
def __init__(self, value, value_offset=None, line=None):
super(ContinuationLine, self).__init__(line)
self.value = value
if value_offset is None:
value_offset = 8
self.value_offset = value_offset
def to_string(self):
return ' '*self.value_offset + self.value
def parse(cls, line):
m = cls.regex.match(line.rstrip())
if m is None:
return None
return cls(m.group('value'), m.start('value'), line)
parse = classmethod(parse)
class LineContainer(object):
def __init__(self, d=None):
self.contents = []
self.orgvalue = None
if d:
if isinstance(d, list): self.extend(d)
else: self.add(d)
def add(self, x):
self.contents.append(x)
def extend(self, x):
for i in x: self.add(i)
def get_name(self):
return self.contents[0].name
def set_name(self, data):
self.contents[0].name = data
def get_value(self):
if self.orgvalue is not None:
return self.orgvalue
elif len(self.contents) == 1:
return self.contents[0].value
else:
return '\n'.join([('%s' % x.value) for x in self.contents
if not isinstance(x, CommentLine)])
def set_value(self, data):
self.orgvalue = data
lines = ('%s' % data).split('\n')
# If there is an existing ContinuationLine, use its offset
value_offset = None
for v in self.contents:
if isinstance(v, ContinuationLine):
value_offset = v.value_offset
break
# Rebuild contents list, preserving initial OptionLine
self.contents = self.contents[0:1]
self.contents[0].value = lines[0]
del lines[0]
for line in lines:
if line.strip():
self.add(ContinuationLine(line, value_offset))
else:
self.add(EmptyLine())
name = property(get_name, set_name)
value = property(get_value, set_value)
def __str__(self):
s = [x.__str__() for x in self.contents]
return '\n'.join(s)
def finditer(self, key):
for x in self.contents[::-1]:
if hasattr(x, 'name') and x.name==key:
yield x
def find(self, key):
for x in self.finditer(key):
return x
raise KeyError(key)
def _make_xform_property(myattrname, srcattrname=None):
private_attrname = myattrname + 'value'
private_srcname = myattrname + 'source'
if srcattrname is None:
srcattrname = myattrname
def getfn(self):
srcobj = getattr(self, private_srcname)
if srcobj is not None:
return getattr(srcobj, srcattrname)
else:
return getattr(self, private_attrname)
def setfn(self, value):
srcobj = getattr(self, private_srcname)
if srcobj is not None:
setattr(srcobj, srcattrname, value)
else:
setattr(self, private_attrname, value)
return property(getfn, setfn)
class INISection(config.ConfigNamespace):
_lines = None
_options = None
_defaults = None
_optionxformvalue = None
_optionxformsource = None
_compat_skip_empty_lines = set()
def __init__(self, lineobj, defaults=None, optionxformvalue=None, optionxformsource=None):
self._lines = [lineobj]
self._defaults = defaults
self._optionxformvalue = optionxformvalue
self._optionxformsource = optionxformsource
self._options = {}
_optionxform = _make_xform_property('_optionxform')
def _compat_get(self, key):
# identical to __getitem__ except that _compat_XXX
# is checked for backward-compatible handling
if key == '__name__':
return self._lines[-1].name
if self._optionxform: key = self._optionxform(key)
try:
value = self._options[key].value
del_empty = key in self._compat_skip_empty_lines
except KeyError:
if self._defaults and key in self._defaults._options:
value = self._defaults._options[key].value
del_empty = key in self._defaults._compat_skip_empty_lines
else:
raise
if del_empty:
value = re.sub('\n+', '\n', value)
return value
def _getitem(self, key):
if key == '__name__':
return self._lines[-1].name
if self._optionxform: key = self._optionxform(key)
try:
return self._options[key].value
except KeyError:
if self._defaults and key in self._defaults._options:
return self._defaults._options[key].value
else:
raise
def __setitem__(self, key, value):
if self._optionxform: xkey = self._optionxform(key)
else: xkey = key
if xkey in self._compat_skip_empty_lines:
self._compat_skip_empty_lines.remove(xkey)
if xkey not in self._options:
# create a dummy object - value may have multiple lines
obj = LineContainer(OptionLine(key, ''))
self._lines[-1].add(obj)
self._options[xkey] = obj
# the set_value() function in LineContainer
# automatically handles multi-line values
self._options[xkey].value = value
def __delitem__(self, key):
if self._optionxform: key = self._optionxform(key)
if key in self._compat_skip_empty_lines:
self._compat_skip_empty_lines.remove(key)
for l in self._lines:
remaining = []
for o in l.contents:
if isinstance(o, LineContainer):
n = o.name
if self._optionxform: n = self._optionxform(n)
if key != n: remaining.append(o)
else:
remaining.append(o)
l.contents = remaining
del self._options[key]
def __iter__(self):
d = set()
for l in self._lines:
for x in l.contents:
if isinstance(x, LineContainer):
if self._optionxform:
ans = self._optionxform(x.name)
else:
ans = x.name
if ans not in d:
yield ans
d.add(ans)
if self._defaults:
for x in self._defaults:
if x not in d:
yield x
d.add(x)
def _new_namespace(self, name):
raise Exception('No sub-sections allowed', name)
def make_comment(line):
return CommentLine(line.rstrip('\n'))
def readline_iterator(f):
"""iterate over a file by only using the file object's readline method"""
have_newline = False
while True:
line = f.readline()
if not line:
if have_newline:
yield ""
return
if line.endswith('\n'):
have_newline = True
else:
have_newline = False
yield line
def lower(x):
return x.lower()
class INIConfig(config.ConfigNamespace):
_data = None
_sections = None
_defaults = None
_optionxformvalue = None
_optionxformsource = None
_sectionxformvalue = None
_sectionxformsource = None
_parse_exc = None
_bom = False
def __init__(self, fp=None, defaults=None, parse_exc=True,
optionxformvalue=lower, optionxformsource=None,
sectionxformvalue=None, sectionxformsource=None):
self._data = LineContainer()
self._parse_exc = parse_exc
self._optionxformvalue = optionxformvalue
self._optionxformsource = optionxformsource
self._sectionxformvalue = sectionxformvalue
self._sectionxformsource = sectionxformsource
self._sections = {}
if defaults is None: defaults = {}
self._defaults = INISection(LineContainer(), optionxformsource=self)
for name, value in defaults.items():
self._defaults[name] = value
if fp is not None:
self._readfp(fp)
_optionxform = _make_xform_property('_optionxform', 'optionxform')
_sectionxform = _make_xform_property('_sectionxform', 'optionxform')
def _getitem(self, key):
if key == DEFAULTSECT:
return self._defaults
if self._sectionxform: key = self._sectionxform(key)
return self._sections[key]
def __setitem__(self, key, value):
raise Exception('Values must be inside sections', key, value)
def __delitem__(self, key):
if self._sectionxform: key = self._sectionxform(key)
for line in self._sections[key]._lines:
self._data.contents.remove(line)
del self._sections[key]
def __iter__(self):
d = set()
d.add(DEFAULTSECT)
for x in self._data.contents:
if isinstance(x, LineContainer):
if x.name not in d:
yield x.name
d.add(x.name)
def _new_namespace(self, name):
if self._data.contents:
self._data.add(EmptyLine())
obj = LineContainer(SectionLine(name))
self._data.add(obj)
if self._sectionxform: name = self._sectionxform(name)
if name in self._sections:
ns = self._sections[name]
ns._lines.append(obj)
else:
ns = INISection(obj, defaults=self._defaults,
optionxformsource=self)
self._sections[name] = ns
return ns
def __str__(self):
if self._bom:
fmt = u'\ufeff%s'
else:
fmt = '%s'
return fmt % self._data.__str__()
__unicode__ = __str__
_line_types = [EmptyLine, CommentLine,
SectionLine, OptionLine,
ContinuationLine]
def _parse(self, line):
for linetype in self._line_types:
lineobj = linetype.parse(line)
if lineobj:
return lineobj
else:
# can't parse line
return None
def _readfp(self, fp):
cur_section = None
cur_option = None
cur_section_name = None
cur_option_name = None
pending_lines = []
pending_empty_lines = False
try:
fname = fp.name
except AttributeError:
fname = '<???>'
line_count = 0
exc = None
line = None
for line in readline_iterator(fp):
# Check for BOM on first line
if line_count == 0 and isinstance(line, six.text_type):
if line[0] == u'\ufeff':
line = line[1:]
self._bom = True
line_obj = self._parse(line)
line_count += 1
if not cur_section and not isinstance(line_obj, (CommentLine, EmptyLine, SectionLine)):
if self._parse_exc:
raise MissingSectionHeaderError(fname, line_count, line)
else:
line_obj = make_comment(line)
if line_obj is None:
if self._parse_exc:
if exc is None:
exc = ParsingError(fname)
exc.append(line_count, line)
line_obj = make_comment(line)
if isinstance(line_obj, ContinuationLine):
if cur_option:
if pending_lines:
cur_option.extend(pending_lines)
pending_lines = []
if pending_empty_lines:
optobj._compat_skip_empty_lines.add(cur_option_name)
pending_empty_lines = False
cur_option.add(line_obj)
else:
# illegal continuation line - convert to comment
if self._parse_exc:
if exc is None:
exc = ParsingError(fname)
exc.append(line_count, line)
line_obj = make_comment(line)
if isinstance(line_obj, OptionLine):
if pending_lines:
cur_section.extend(pending_lines)
pending_lines = []
pending_empty_lines = False
cur_option = LineContainer(line_obj)
cur_section.add(cur_option)
if self._optionxform:
cur_option_name = self._optionxform(cur_option.name)
else:
cur_option_name = cur_option.name
if cur_section_name == DEFAULTSECT:
optobj = self._defaults
else:
optobj = self._sections[cur_section_name]
optobj._options[cur_option_name] = cur_option
if isinstance(line_obj, SectionLine):
self._data.extend(pending_lines)
pending_lines = []
pending_empty_lines = False
cur_section = LineContainer(line_obj)
self._data.add(cur_section)
cur_option = None
cur_option_name = None
if cur_section.name == DEFAULTSECT:
self._defaults._lines.append(cur_section)
cur_section_name = DEFAULTSECT
else:
if self._sectionxform:
cur_section_name = self._sectionxform(cur_section.name)
else:
cur_section_name = cur_section.name
if cur_section_name not in self._sections:
self._sections[cur_section_name] = \
INISection(cur_section, defaults=self._defaults,
optionxformsource=self)
else:
self._sections[cur_section_name]._lines.append(cur_section)
if isinstance(line_obj, (CommentLine, EmptyLine)):
pending_lines.append(line_obj)
if isinstance(line_obj, EmptyLine):
pending_empty_lines = True
self._data.extend(pending_lines)
if line and line[-1] == '\n':
self._data.add(EmptyLine())
if exc:
raise exc
+48
View File
@@ -0,0 +1,48 @@
from . import compat
from .ini import LineContainer, EmptyLine
def tidy(cfg):
"""Clean up blank lines.
This functions makes the configuration look clean and
handwritten - consecutive empty lines and empty lines at
the start of the file are removed, and one is guaranteed
to be at the end of the file.
"""
if isinstance(cfg, compat.RawConfigParser):
cfg = cfg.data
cont = cfg._data.contents
i = 1
while i < len(cont):
if isinstance(cont[i], LineContainer):
tidy_section(cont[i])
i += 1
elif (isinstance(cont[i-1], EmptyLine) and
isinstance(cont[i], EmptyLine)):
del cont[i]
else:
i += 1
# Remove empty first line
if cont and isinstance(cont[0], EmptyLine):
del cont[0]
# Ensure a last line
if cont and not isinstance(cont[-1], EmptyLine):
cont.append(EmptyLine())
def tidy_section(lc):
cont = lc.contents
i = 1
while i < len(cont):
if isinstance(cont[i-1], EmptyLine) and isinstance(cont[i], EmptyLine):
del cont[i]
else:
i += 1
# Remove empty first line
if len(cont) > 1 and isinstance(cont[1], EmptyLine):
del cont[1]