Nécessite python2.4 (pour logging.TimedRotatingFileHandler et subprocess)
Le code source est également disponible par bazaar-ng:
bzr get http://bzr.flibuste.net/libre/flibuste/snapy
#!/usr/bin/env python
#
# Copyright (c) 2005,2006 William Dode <www.flibuste.net>
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
#
__version__ = "0.3.7"
import sys
import os
import time
import re
from types import *
from ConfigParser import RawConfigParser
from subprocess import Popen, call, PIPE
import logging
import logging.config
import logging.handlers
#
# cron
#
def cron2list(c):
"""
1-3 == 1,2,3
1 == 1,
* == *,
"""
r = c.split("-")
if len(r) == 2:
return range(int(r[0]),int(r[1])+1)
lst = c.split(",")
if len(lst) > 1:
return [int(x) for x in lst]
if c != "*":
return [int(c)]
return c
def cron_match(c, t):
if "*" in c or t in c:
return True
else:
return False
class Snapy:
def __init__(self, config):
log_file_keep = config.get("DEFAULT", "log-file-keep")
log_file = config.get("DEFAULT", "log-file")
if log_file_keep:
log_rotate = logging.handlers.TimedRotatingFileHandler(log_file,"d",1,log_file_keep)
log_rotate.setFormatter(logging.Formatter("%(asctime)s %(levelname)s:%(message)s","%Y-%m-%d %H:%M"))
log_rotate.setLevel(logging.DEBUG)
log.addHandler(log_rotate)
log.info("Start Snapy %s" % __version__ )
try:
sections = config.sections()
sections.sort()
cron_enabled = True
first_time = True
while cron_enabled:
cron_enabled = False
year, month, day, hour, minute, second, day_week = time.localtime()[0:7]
for section in sections:
self.cron = config.get(section, "cron")
if self.cron:
cron_enabled = True
c_minute, c_hour, c_day, c_month, c_day_week = self.cron.split(" ")
c_minute = cron2list(c_minute)
c_hour = cron2list(c_hour)
c_month = cron2list(c_month)
c_day_week = cron2list(c_day_week)
if not cron_match(c_minute, minute) or \
not cron_match(c_hour, hour) or \
not cron_match(c_month, month) or \
not cron_match(c_day_week, day_week):
continue
elif not first_time:
continue # if no cron, do only one time
log.info("-" * len(section))
log.info(section)
log.info("-" * len(section))
self.days = config.getint(section, "days")
self.months = config.getint(section, "months")
self.years = config.getint(section, "years")
self.pruneb = config.getboolean(section, "prune")
self.sources = args(config.get(section, "sources"))
self.target = config.get(section, "target")
self.target_host = config.get(section, "target-host")
self.rsync_exe = config.get(section, "rsync-exe")
self.ssh_exe = config.get(section, "ssh-exe")
self.pre_snapy = args(config.get(section, "pre-snapy"))
self.post_snapy = args(config.get(section, "post-snapy"))
self.pre_prune = args(config.get(section, "pre-prune"))
self.post_prune = args(config.get(section, "post-prune"))
self.rsync_args = []
self.ssh_args = []
for c, v in config.items(section):
if c.startswith("rsync-") and c != "rsync-exe":
op = c[5:]
if op != "-":
self.rsync_args.extend([op])
self.rsync_args.extend(args(v))
if c.startswith("ssh-") and c != "ssh-exe":
op = c[3:]
if op != "-":
self.ssh_args.extend([op])
self.ssh_args.extend(args(v))
if self.target and self.sources:
if self.pre_snapy:
res = self.sh(self.pre_snapy)
if res != 0:
log.warning("Snapy canceled %s" % res)
continue
self.snapy()
if self.post_snapy:
print repr (self.post_snapy)
self.sh(self.post_snapy)
if self.pruneb:
if self.pre_prune:
res = self.sh(self.pre_prune)
if res != 0:
log.warning("Prune canceled")
continue
self.prune()
if self.post_prune:
self.sh(self.post_prune)
first_time = False
if cron_enabled:
sys.stdout.write(".")
sys.stdout.flush()
time.sleep(60)
except:
log.exception("Exception")
log.info("End")
def sh(self,cmd):
log.debug("S cmd: " + " ".join(cmd))
p = Popen(cmd, stdout = PIPE, stderr = PIPE)
stdout, stderr = p.communicate()
if stdout:
log.debug("S ret: %s" % stdout)
if stderr:
log.error("S err: %s" % stderr)
return p.returncode
def sh_target(self, cmd):
if self.target_host:
cmd = [self.ssh_exe] + self.ssh_args + [self.target_host] + cmd
log.debug("T cmd: " + " ".join(cmd))
p = Popen(cmd, stdout = PIPE, stderr = PIPE)
stdout, stderr = p.communicate()
if stdout:
log.debug("T ret: %s" % stdout)
if stderr:
log.error("T err: %s" % stderr)
p.wait()
return p.returncode, stdout
def snapy(self):
if self.sh_target(["test","-d",self.target])[0] != 0:
self.sh_target(["mkdir","-p",self.target])
today = time.strftime("%Y-%m-%d")
today_path = "/".join([self.target, today])
# find last backup
dirs = self.sh_target(["ls",self.target])[1].split("\n")
dirs = [d for d in dirs if re.match(r"\d\d\d\d-\d\d-\d\d", d)]
dirs.sort()
if len(dirs) > 0:
last_dir = dirs[-1]
last_dir_path = "/".join([self.target, last_dir])
if last_dir != today:
self.sh_target(["cp", "-al", last_dir_path, today_path])
if self.sh_target(["test", "-d", today_path ]) [0] == 0:
self.sh_target(["chmod", "-R", "u+rw", today_path])
if self.target_host:
today_path = "%s:%s" % (self.target_host, today_path)
if self.ssh_exe and self.target_host:
ssh = ["-e",self.ssh_exe + " "+ " ".join(self.ssh_args)]
else:
ssh = []
self.sh([self.rsync_exe] + ssh + self.rsync_args + self.sources + [today_path])
def prune(self):
# find last backup
dirs = self.sh_target(["ls",self.target])[1].split("\n")
dirs = [d for d in dirs if re.match(r"\d\d\d\d-\d\d-\d\d", d)]
dirs.sort()
dirs.reverse()
keep_days = {}
keep_months = {}
keep_years = {}
if len(dirs) > 0:
for d in dirs:
year = d[:4]
month = d[5:7]
day = d[8:10]
if len(keep_days) < self.days and year+month+day not in keep_days:
keep_days[year+month+day] = d
if len(keep_months) < self.months and year+month not in keep_months:
keep_months[year+month] = d
if len(keep_years) < self.years and year not in keep_years:
keep_years[year] = d
keep = keep_days.values() + keep_months.values() + keep_years.values()
for d in dirs:
if d not in keep:
toremove = "/".join([self.target, d])
log.info("prune %s" % toremove)
self.sh_target(["chmod","-R","u+w",toremove])
self.sh_target(["rm","-r","-f",toremove])
if __name__ == '__main__':
log = logging.getLogger('')
log.setLevel(logging.DEBUG)
log_out = logging.StreamHandler()
log_out.setFormatter(logging.Formatter("%(asctime)s %(levelname)s:%(message)s","%Y-%m-%d %H:%M"))
log.addHandler(log_out)
log_out.setLevel(logging.INFO)
if len(sys.argv) == 2:
filename = sys.argv[1]
else:
filename = "snapy.conf"
config = RawConfigParser({
"target-host":"",
"prune":"False",
"days":"7",
"months":"12",
"years":"5",
"rsync-exe":"rsync",
"ssh-exe":"ssh",
"pre-snapy":"",
"post-snapy":"",
"pre-prune":"",
"post-prune":"",
"sources":"",
"target":"",
"cron":None,
"log-file": "snapy.log",
"log-file-keep":10,
"log-config":None,
"debug":False
})
config.read(filename)
log_config = config.get("DEFAULT","log-config")
if log_config:
logging.config.fileConfig(log_config)
if config.get("DEFAULT","debug"):
log_out.setLevel(logging.DEBUG)
log.debug(file(filename).read())
def args(ch):
""" return list """
if not ch:
return []
if type(ch) in StringTypes:
return ch.split("\n")
else:
return ch
Snapy(config)