Advertisement
mikelieman

Rsync based backup ported from Perl to Rust

Jun 16th, 2022
1,805
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Rust 12.93 KB | None | 0 0
  1. use itertools::Itertools;
  2. use log::{debug, error, info, trace, warn, log_enabled};
  3. use std::collections::HashMap;
  4. use std::ffi::OsStr;
  5. use std::fs::File;
  6. use std::io::Write;
  7. use std::path::Path;
  8. use std::process::{Command, Output};
  9. use whoami::username;
  10.  
  11.  
  12. ///////////////////////////////////////////////////////////////////////
  13. //
  14. /*
  15.  
  16. my $VERBOSE = q{-v};
  17.  
  18. # mount->cmd needs T_FSTYPE and T_UUID
  19. $cmds->{'mount'}->{'cmd'} = $cmds->{'mount'}->{'bin'} . qq{ $VERBOSE -t $T_FSTYPE -U $T_UUID $FS_MOUNTPOINT } . $cmds->{'mount'}->{'opt'};
  20.  
  21. # rsync-opt needs excludes filename, derived from T_UUID
  22. my $CHKSUM = q{};
  23. if ($RSYNC_CHECKSUMS) {
  24.     INFO(q{Checksums for rsync are enabled});
  25.     $CHKSUM = q{-c}
  26. };
  27.  
  28. $cmds->{'rsync'}->{'opt'} = qq{-a $VERBOSE $CHKSUM --delete --delete-excluded --delete-before }
  29.     . qq{--exclude-from=$backup_vols->{$T_UUID}->{'EXCLUDES'} };
  30.  
  31. $cmds->{'rsync'}->{'cmd'} = qq{$cmds->{'ionice'}->{'bin'} $cmds->{'ionice'}->{'opt'} }
  32.                                     . qq{$cmds->{'rsync'}->{'bin'} $cmds->{'rsync'}->{'opt'} }
  33.                                     . qq{$backup_vols->{$T_UUID}->{'SOURCES'} $FS_MOUNTPOINT};
  34.  
  35. */
  36. //
  37. //
  38. /*
  39.  
  40. $cmds->{'rsync'}->{'cmd'} =           qq{$cmds->{'ionice'}->{'bin'} $cmds->{'ionice'}->{'opt'} }
  41.                                     . qq{$cmds->{'rsync'}->{'bin'}
  42.  
  43.                                     . qq{-a $VERBOSE $CHKSUM --delete --delete-excluded --delete-before }
  44.                                     . qq{--exclude-from=$backup_vols->{$T_UUID}->{'EXCLUDES'} };
  45.                                     . qq{$backup_vols->{$T_UUID}->{'SOURCES'} $FS_MOUNTPOINT};
  46.  
  47. 'SOURCES'  => '/boot /etc /home /opt /root /srv /storage /usr /var'
  48.  
  49. */
  50. ///////////////////////////////////////////////////////////////////////
  51. fn test_rsync() {
  52.  
  53.     info!("test_rsync() - Begin.");
  54.  
  55.     info!("testing rsync with rpm exclusion file...");
  56.  
  57.     let mut command = Command::new("/usr/bin/ionice");
  58.     command.arg("-c"); // scheduling class
  59.     command.arg("3"); // idle
  60.     command.arg("/usr/bin/rsync");
  61.     command.arg("--dry-run");
  62.     command.arg("-a"); // archive mode
  63.     command.arg("-v"); // verbose
  64. //    command.arg(""); // checksum
  65.     command.arg("--delete");
  66.     command.arg("--delete-excluded");
  67.     command.arg("--delete-before");
  68. //    command.arg("--exclude-from=rsync-excludes.txt"); // RPM EXCLUDES
  69.     command.arg("--exclude-from=excludes-1tb.conf"); // VOLUME EXCLUDES
  70.     command.arg("/boot"); // SOURCES
  71.     command.arg("/etc"); // SOURCES
  72.     command.arg("/home"); // SOURCES
  73.     command.arg("/opt"); // SOURCES
  74.     command.arg("/root"); // SOURCES
  75.     command.arg("/srv"); // SOURCES
  76.     command.arg("/storage"); // SOURCES
  77.     command.arg("/usr"); // SOURCES
  78.     command.arg("/var"); // SOURCES
  79.     command.arg("/storage/tmp/rsync-test"); // FS_MOUNTPOINT
  80.  
  81.     let args: Vec<&OsStr> = command.get_args().collect();
  82.     info!("Arguments to ionice: {:?}", args);
  83.  
  84.     match command.output() {
  85.        
  86.         Ok(o) => {
  87.             // stdout
  88.             match String::from_utf8(o.stdout) {
  89.  
  90.                 Ok(stdout) => {
  91.                     for line in stdout.lines() {
  92.                         info!("stdout: {}", line);
  93.                     } // for
  94.                 }, // Ok(stdout)
  95.  
  96.                 Err(e) => {
  97.                     error!("Could not convert from UTF8.  Bailing out! {:?}", e);
  98.                     panic!("Could not convert from UTF8.  Bailing out! {:?}", e);
  99.                 }, // Err
  100.  
  101.             } //match stdout
  102.  
  103.             // stderr
  104.             match String::from_utf8(o.stderr) {
  105.  
  106.                 Ok(stderr) => {
  107.                     for line in stderr.lines() {
  108.                         info!("stderr: {}", line);
  109.                     } // for
  110.                 }, // Ok(stderr)
  111.  
  112.                 Err(e) => {
  113.                     error!("Could not convert from UTF8.  Bailing out! {:?}", e);
  114.                     panic!("Could not convert from UTF8.  Bailing out! {:?}", e);
  115.                 }, // Err
  116.  
  117.             } //match stderr
  118.  
  119.         }, // Ok(o)
  120.  
  121.         Err(e) => {
  122.             error!("Could not run the command.  Bailing out! {:?}", e);
  123.             panic!("Could not run the command.  Bailing out! {:?}", e);
  124.         }, // Err
  125.  
  126.     } // match
  127.     info!("test_rsync() - End.");
  128.  
  129. } // fn test_rsync
  130.  
  131.  
  132. ///////////////////////////////////////////////////////////////////////
  133. //
  134. //
  135. //
  136. ///////////////////////////////////////////////////////////////////////
  137. fn main() {
  138.  
  139.     match setup_logger() {
  140.         Ok(_) => info!("Logging initialized. (log level: {})", log::max_level()),
  141.         Err(e) => {
  142.             println!("Could not initialize logging: {e:?}.  Bailing out! ");
  143.             std::process::exit(1);
  144.             },
  145.     } // match setup_logger
  146.  
  147.     let username = username();
  148.     info!("Running as user '{}'", username);
  149.  
  150.     if username.ne(&String::from("root")) {
  151.         error!("YOU ARE NOT RUNNING AS 'root'!  YOU WILL NOT GET A PROPER BACKUP!");
  152.     }
  153.  
  154.     debug!("create_rsync_exclude_rpm_files()");
  155.     create_rsync_exclude_rpm_files();
  156.  
  157.     info!("testing rsync w/ exclude file (--dry-run)");
  158.     test_rsync();
  159.  
  160. } // main
  161.  
  162.  
  163. ///////////////////////////////////////////////////////////////////////
  164. //
  165. //
  166. //
  167. ///////////////////////////////////////////////////////////////////////
  168. fn setup_logger() -> Result<(), fern::InitError> {
  169.  
  170.     fern::Dispatch::new()
  171.         .format(|out, message, record| {
  172.             out.finish(format_args!(
  173.                 "{} [{}] [{}] {}",
  174.                 chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
  175.                 record.target(),
  176.                 record.level(),
  177.                 message
  178.             ))
  179.         })
  180.         .level(log::LevelFilter::Debug)
  181.         .chain(std::io::stdout())
  182.         .chain(fern::log_file("output.log")?)
  183.         .apply()?;
  184.     Ok(())
  185.  
  186. } //setup_logger
  187.  
  188.  
  189. ///////////////////////////////////////////////////////////////////////
  190. //
  191. //
  192. //
  193. ///////////////////////////////////////////////////////////////////////
  194. fn get_all_rpm_files(f: &mut HashMap<String, i32>) {
  195.  
  196.     info!("Collecting exclusion list of files from all installed rpm packages...");
  197.  
  198.     let mut command = Command::new("/usr/bin/rpm");
  199.     command.arg("-qal");
  200.  
  201.     match command.output() {
  202.         Ok(output) => {
  203.             process_rpm_file(f, output);
  204.         },
  205.         Err(e) => {
  206.             error!("Could not run the command.  Bailing out! {:?}", e);
  207.             panic!("Could not run the command.  Bailing out! {:?}", e);
  208.         },
  209.     } // match command.output()
  210.  
  211.     info!("HashMap has {} elements.", f.len());
  212.  
  213. } // get_all_rpm_files
  214.  
  215.  
  216. ///////////////////////////////////////////////////////////////////////
  217. //
  218. //
  219. //
  220. ///////////////////////////////////////////////////////////////////////
  221. fn process_rpm_file(f: &mut HashMap<String, i32>, o: Output) {
  222.  
  223.     // stdout
  224.     match String::from_utf8(o.stdout) {
  225.  
  226.         Ok(stdout) => {
  227.  
  228.             for line in stdout.lines() {
  229.                 trace!("{}", line);
  230.  
  231.                 if Path::new(line).is_file() {
  232.                     trace!("{} is a regular file", line);
  233.                     // insert file, with some arbitrary value
  234.                     f.insert( line.to_string(), 1 ) ;
  235.                 } // if path exists
  236.                 else {
  237.                     debug!("{} is not a regular file. Skipped.", line);
  238.                 } // else
  239.             } // for
  240.  
  241.         }, // Ok(stdout)
  242.  
  243.         Err(e) => {
  244.             error!("Could not convert from UTF8.  Bailing out! {:?}", e);
  245.             panic!("Could not convert from UTF8.  Bailing out! {:?}", e);
  246.         }, // Err
  247.  
  248.     } //match
  249.  
  250.     // stderr
  251.     match String::from_utf8(o.stderr) {
  252.  
  253.         Ok(stderr) => {
  254.             for line in stderr.lines() {
  255.                 info!("stderr: {}", line);
  256.             } // for
  257.         }, // Ok(stderr)
  258.  
  259.         Err(e) => {
  260.             error!("Could not convert from UTF8.  Bailing out! {:?}", e);
  261.             panic!("Could not convert from UTF8.  Bailing out! {:?}", e);
  262.         }, // Err
  263.  
  264.     } //match stderr
  265.  
  266. } // fn process_rom_file
  267.  
  268. ///////////////////////////////////////////////////////////////////////
  269. //
  270. //
  271. //
  272. ///////////////////////////////////////////////////////////////////////
  273. fn get_rpm_verify_info(f: &mut HashMap<String, i32>) {
  274.  
  275.     info!("Verifying files on exclusion list...");
  276.  
  277.     let mut command = Command::new("/usr/bin/ionice");
  278.     command.arg("-c"); // scheduling class
  279.     command.arg("3"); // idle
  280.     command.arg("/usr/bin/rpm");
  281.     command.arg("-Va"); // Verify all
  282.  
  283.     match command.output() {
  284.         Ok(output) => {
  285.             process_verify_entry(f, output);
  286.         },
  287.         Err(e) => {
  288.             error!("Could not run the command.  Bailing out! {:?}", e);
  289.             panic!("Could not run the command.  Bailing out! {:?}", e);
  290.         },
  291.     } // match command.output()
  292.  
  293.     info!("HashMap has {} elements.", f.len());
  294.  
  295. } // get_rpm_verify_info
  296.  
  297.  
  298. ///////////////////////////////////////////////////////////////////////
  299. //
  300. //
  301. //
  302. ///////////////////////////////////////////////////////////////////////
  303. fn process_verify_entry(f: &mut HashMap<String, i32>, o: Output) {
  304.  
  305.     // stdout
  306.     match String::from_utf8(o.stdout) {
  307.  
  308.         Ok(stdout) => {
  309.  
  310.             let mut file_list: Vec<&str> = Vec::new();
  311.  
  312.             for line in stdout.lines() {
  313.                 trace!("{}", line);
  314.                 if line.contains("(Permission denied)") {
  315.                     error!("{}", line);
  316.                 } // if line contains...
  317.                 else {
  318.                     let mut t: Vec<&str> = line.split_whitespace().collect();
  319.                     t.reverse();
  320.                     let _flags = t.pop().unwrap();
  321.                     t.reverse();
  322.                     let name = t.pop().unwrap();
  323.  
  324.                     if Path::new(name).is_file() {
  325.                         file_list.push(name);
  326.                     }
  327.                     else {
  328.                         debug!("{} is a directory",name);
  329.                     }
  330.  
  331.                 }  // else line contains.
  332.             } // for
  333.  
  334.             file_list.sort();
  335.             file_list.dedup();
  336.  
  337.             info!("Removing changed files from exclusion list...");
  338.  
  339.             let mut removed_count = 0;
  340.  
  341.             for elem in file_list.iter() {
  342.                 f.remove(&elem.to_string());
  343.                 debug!("removed {} from HashMap", elem);
  344.                 removed_count += 1;
  345.                 trace!("HashMap has {} elements.", f.len());
  346.             } // for...
  347.  
  348.             info!("Total removed from HashMap: {}", removed_count);
  349.  
  350.         }, // Ok(stdout)
  351.  
  352.         Err(e) => {
  353.             error!("Could not convert from UTF8.  Bailing out! {:?}", e);
  354.             panic!("Could not convert from UTF8.  Bailing out! {:?}", e);
  355.         }, // Err
  356.  
  357.     } //match
  358.  
  359.     // stderr
  360.     match String::from_utf8(o.stderr) {
  361.  
  362.         Ok(stderr) => {
  363.             for line in stderr.lines() {
  364.                 info!("stderr: {}", line);
  365.             } // for
  366.         }, // Ok(stderr)
  367.  
  368.         Err(e) => {
  369.             error!("Could not convert from UTF8.  Bailing out! {:?}", e);
  370.             panic!("Could not convert from UTF8.  Bailing out! {:?}", e);
  371.         }, // Err
  372.  
  373.     } //match stderr
  374.  
  375. } // fn process_verify_entry
  376.  
  377.  
  378. ///////////////////////////////////////////////////////////////////////
  379. //
  380. // fn create_rsync_exclude_rpm_files()
  381. //
  382. //  Write HashMap keys to rsync-format exclude file
  383. //
  384. ///////////////////////////////////////////////////////////////////////
  385. fn create_rsync_exclude_rpm_files() {
  386.  
  387.     info!("Creating rsync exclude file for unchanged rpm managed files...");
  388.  
  389.     let mut files: HashMap<String, i32> = HashMap::new();
  390.     info!("HashMap has {} elements.", files.len());
  391.  
  392.     // step 1
  393.     get_all_rpm_files(&mut files);
  394.  
  395.     trace!("HashMap has {} elements after get_all_rpm_files.", files.len());
  396.     if log_enabled!(log::Level::Trace) {
  397.         trace!("Raw {:?}", files);
  398.         trace!("Contents of HashMap");
  399.         for (file, status) in &files {
  400.             trace!("[all-files-tag] {}: {}", file, status);
  401.         }
  402.     } // if log enabled
  403.  
  404.     // step 2
  405.     get_rpm_verify_info(&mut files);
  406.  
  407.     trace!("HashMap has {} elements after get_rpm_verify_info.", files.len());
  408.     if log_enabled!(log::Level::Trace) {
  409.         trace!("Raw {:?}", files);
  410.         trace!("Contents of HashMap");
  411.         for (file, status) in &files {
  412.             trace!("[all-rpm-tag] {}: {}", file, status);
  413.         }
  414.     } // if log enabled
  415.  
  416.     // step 3
  417.     let mut exclude_file = File::create("rsync-excludes.txt").expect("Unable to create file");
  418.     for file in files.keys().sorted() {
  419.         let line = format!("{}\n", file);
  420.         trace!("{}", line);
  421.         exclude_file.write_all(line.as_bytes()).expect("Unable to write data");
  422.     } // for...
  423.  
  424. } // fn create_rsync_exclude_rpm_files()
  425.  
  426.  
  427.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement