logo chat

Contact/Email

Valid XHTML 1.0!

william dodé

Code source

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)