#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace bp = boost::process; namespace asio = boost::asio; namespace po = boost::program_options; namespace fs = boost::filesystem; class zfsbackupcleaner { public: zfsbackupcleaner(std::string backupsystem): _backupsystem(backupsystem), _now(boost::posix_time::second_clock::local_time()), _dry_run(false) { std::cout << "Gehe aus von aktueller Zeit: " << _now << std::endl << std::endl; } void set_dry_run(bool val = true) { _dry_run = val; } void read_backups() { asio::io_context ctx; asio::readable_pipe out(ctx); bp::process proc(ctx, bp::environment::find_executable("zfs"), {"list", "-t", "snapshot", "/backup/"+_backupsystem}, bp::process_stdio{{/* in to default*/}, out, {/* err to default */}}); std::string output; boost::system::error_code ec; asio::read(out, asio::dynamic_buffer(output), ec); assert(!ec || (ec==asio::error::eof)); proc.wait(); std::stringstream lines(output); for(std::string line; std::getline(lines, line);) { std::string::size_type pos = line.find(' '); line = line.substr(0,pos); //std::cout << "X: " << line << std::endl; if(line == "NAME") continue; pos = line.find('@'); _backupbase = line.substr(0,pos); std::string timestring = line.substr(pos+1); timestring[10]=' '; timestring[13]=':'; timestring[16]=':'; boost::posix_time::ptime t(boost::posix_time::time_from_string(timestring)); _backuptimes.insert(t); } } void clean_backups () { if(_backuptimes.empty()) { std::cout << "Keine Backups für >" << _backupsystem << "< gefunden." << std::endl << "Keine weitere Behandlung." << std::endl; return; } if(_backuptimes.size() < 7) { std::cout << "Weniger als 7 Backups für >" << _backupsystem << "< gefunden." << std::endl << "Keine weitere Behandlung." << std::endl; return; } // Hinweis: Die Backups werden so von "zfs list" übermittelt, dass das älteste am Anfang steht und das jüngste am Ende der Liste _iter_current=_backuptimes.begin(); _iter_last_to_handle = _backuptimes.end(); std::advance(_iter_last_to_handle, -7); // Die letzten sieben Backups werden besonders behandelt - sie bleiben in jedem Fall erhalten! // Nun von hinten her die Backups durchgehen // A) Das allererste Backup behalten wir in jedem Fall std::cout << "Behalte allererstes Backup " << *_iter_current << " in diesem Backup-Set." << std::endl << std::endl; _iter_current++; // B) after 365 days, we keep one backup every 30 days handle_backups<30,365>(); // C) after 30 days, we keep one backup every 7 days handle_backups<7,30>(); // D) after one day, we keep one backup per day handle_backups<1,1>(); // E) Behalte die letzten bis zu sieben Backups for(; _iter_current != _backuptimes.end(); _iter_current++) { std::cout << "Behalte Backup " << *_iter_current << ", weil es zu den letzten 7 gehört." << std::endl; } } private: template void handle_backups() { if(_iter_current == _iter_last_to_handle) return; boost::gregorian::date_duration dur; boost::posix_time::ptime last_kept_time; while( _iter_current != _iter_last_to_handle && (dur = _now.date() - _iter_current->date()) > boost::gregorian::days(after_y_day)) { last_kept_time = *_iter_current; std::cout << "Behalte Backup " << last_kept_time << " [Älter als " << after_y_day << ", Backup alle " << keep_every_x_days << " Tage behalten]." << std::endl << std::endl; _iter_current++; while( _iter_current != _iter_last_to_handle && (dur = _iter_current->date() - last_kept_time.date()) < boost::gregorian::days(keep_every_x_days)) { std::cout << "Entferne Backup " << *_iter_current << " weil wir bereits eines im aktuellen Zyklus haben [Älter als " << after_y_day << ", Backup alle " << keep_every_x_days << " Tage behalten]." << std::endl; if(!_dry_run) { std::string timestring = boost::posix_time::to_iso_extended_string(*_iter_current); timestring[10]='_'; timestring[13]='-'; timestring[16]='-'; timestring=timestring.substr(0, 19); asio::io_context ctx; asio::readable_pipe out(ctx); std::vector args; args.push_back("destroy"); args.push_back("-v"); if(_dry_run) args.push_back("-n"); args.push_back(_backupbase + "@" + timestring); bp::process proc(ctx, bp::environment::find_executable("zfs"), args, bp::process_stdio{{/* in to default*/}, out, {/* err to default */}}); std::string output; boost::system::error_code ec; asio::read(out, asio::dynamic_buffer(output), ec); assert(!ec || (ec==asio::error::eof)); proc.wait(); std::cout << output << std::endl; } _iter_current++; } } } std::string _backupsystem; boost::posix_time::ptime _now; bool _dry_run; std::string _backupbase; std::set _backuptimes; std::set::iterator _iter_current; std::set::iterator _iter_last_to_handle; }; int main(int argc, char** argv){ std::cout << "Hello, from zfsbackupcleaner!\n"; po::options_description options("Argumente"); options.add_options() ("help", "Hilfe anzeigen") ("dry-run,n", "Test-Modus; keine Backups entfernen") ("backupsystem", po::value(), "Computer, dessen Backups bereinigt werden sollen") ; po::positional_options_description p; p.add("backupsystem", 1); po::variables_map vm; try { po::store(po::command_line_parser(argc,argv).options(options).positional(p).run(), vm); po::notify(vm); } catch (const boost::program_options::invalid_command_line_syntax & error) { std::cout << "Error parsing commandline-arguments:" << std::endl; std::cout << error.what() << std::endl; return -1; } if(vm.count("help") || !vm.count("backupsystem")) { std::cout << "Benutzung: zfsbackupcleaner [Optionen] " << std::endl << std::endl; std::cout << options << std::endl; return 0; } std::string backupsystem = vm["backupsystem"].as(); if(backupsystem != "workstation" && ! fs::exists("/backup/"+backupsystem)) { std::cout << "Für das System >" << backupsystem << "< gibt es keine Backups." << std::endl; return -1; } zfsbackupcleaner zbc(backupsystem); if(vm.count("dry-run")) zbc.set_dry_run(true); zbc.read_backups(); zbc.clean_backups(); return 0; }