#!/usr/bin/perl # # **** License **** # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # 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. # # A copy of the GNU General Public License is available as # `/usr/share/common-licenses/GPL' in the Debian GNU/Linux distribution # or on the World Wide Web at `http://www.gnu.org/copyleft/gpl.html'. # You can also obtain it by writing to the Free Software Foundation, # Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, # MA 02110-1301, USA. # # This code was originally developed by Vyatta, Inc. # Portions created by Vyatta are Copyright (C) 2010 Vyatta, Inc. # All Rights Reserved. # # Author: Stig Thormodsrud # Date: October 2010 # Description: Script for config management features. # # **** End License **** # use strict; use warnings; use lib '/opt/vyatta/share/perl5/'; use Vyatta::Config; use Vyatta::ConfigMgmt; use Getopt::Long; use File::Basename; use File::Copy; use URI; use IO::Prompt; use Sys::Syslog qw(:standard :macros); my $commit_uri_script = '/opt/vyatta/sbin/vyatta-commit-push.pl'; my $commit_revs_script = '/opt/vyatta/sbin/vyatta-commit-revs.pl'; my $commit_hook_dir = cm_get_commit_hook_dir(); my $archive_dir = cm_get_archive_dir(); my $config_file = "$archive_dir/config.boot"; my $lr_conf_file = cm_get_lr_conf_file(); my $job_name = "commit-confirm"; my $debug = 0; sub get_link { my ($path) = @_; my $script = basename($path); if ($script =~ /revs/) { $script = "01" . $script; } elsif ($script =~ /push/) { $script = "02" . $script; } my $link = $commit_hook_dir . $script; return $link; } sub check_integer { my ($num) = @_; if ($num !~ /^\d+$/) { print "Invalid number [$num]\n"; exit 1; } return 1; } sub check_valid_rev { my ($rev) = @_; check_integer($rev); my $num_revs = cm_get_num_revs(); return 1 if $rev <= $num_revs; print "Invalid revision [$rev]\n"; exit 1; } sub filter_file_lines { my ($diff) = @_; my @lines = split("\n", $diff); my $line1 = shift @lines; my $line2 = shift @lines; unshift(@lines, $line2) if $line2 !~ /^\+\+\+ /; unshift(@lines, $line1) if $line1 !~ /^\-\-\- /; return join("\n", @lines); } sub filter_version_string { my ($diff) = @_; # find last diff hunk, skip it if it's the version string my @lines = split("\n", $diff); my @last_hunk = (); my $found = 0; while (my $line = pop(@lines)) { unshift(@last_hunk, $line); $found = 1 if $line =~ /vyatta-config-version/; last if $line =~ /^@@/; } return join("\n", @lines) if $found; push(@lines, @last_hunk); return join("\n", @lines); } sub format_commands { my ($diff) = @_; my @difflines = split('\n', $diff); my @cmds = (); foreach my $line (@difflines){ my @words = split(' ', $line); my $elements = scalar(@words); my @non_leaf = @words[0 .. ($elements - 2)] ; my $path = join(' ', @non_leaf); $path =~ s/'//g; my $cmd = "$path " . @words[($elements - 1)]; push(@cmds, $cmd); } my $results = join("\n", @cmds); return "$results\n"; } # # main # my ($action, $uri, $revs, $revnum, $minutes, $filename); Getopt::Long::Configure('pass_through'); GetOptions("action=s" => \$action, "uri=s" => \$uri, "revs=s" => \$revs, "revnum=s" => \$revnum, "minutes=s" => \$minutes, "file=s" => \$filename, ); die "Error: no action" if ! defined $action; my ($cmd, $rc) = ('', 1); if ($action eq 'update-uri') { print "update-uri\n" if $debug; my $config = new Vyatta::Config; $config->setLevel('system config-management commit-archive'); my @uris = $config->returnValues('location'); my $link = get_link($commit_uri_script); if (scalar(@uris) > 0 and ! -e $link) { print "add link [$link]\n" if $debug; $rc = system("ln -s $commit_uri_script $link"); exit $rc; } elsif (scalar(@uris) < 1 and -e $link) { print "remove link [$link]\n" if $debug; $rc = system("rm -f $link"); exit $rc; } exit 0; } if ($action eq 'valid-uri') { die "Error: no uri" if ! defined $uri; print "valid-uri [$uri]\n" if $debug; my $u = URI->new($uri); exit 1 if ! defined $u; my $scheme = $u->scheme(); my $auth = $u->authority(); my $path = $u->path(); exit 1 if ! defined $scheme or ! defined $path; if ($scheme eq 'tftp') { } elsif ($scheme eq 'ftp') { } elsif ($scheme eq 'scp') { } elsif ($scheme eq 'sftp') { } else { print "Unsupported URI scheme.\n"; exit 1; } exit 0; } if ($action eq 'update-revs') { die "Error: no revs" if ! defined $revs; print "update-revs [$revs]\n" if $debug; check_integer($revs); my $link = get_link($commit_revs_script); if ($revs == 0) { print "remove link [$link]\n" if $debug; $rc = system("rm -f $link"); } else { if (! -e $link) { print "add link [$link]\n" if $debug; $rc = system("ln -s $commit_revs_script $link"); } if (! -e $archive_dir) { system("sudo mkdir $archive_dir"); system("sudo chgrp vyattacfg $archive_dir"); system("sudo chmod 775 $archive_dir"); } my $lr_conf = "$config_file {\n"; $lr_conf .= "\t su root vyattacfg\n"; $lr_conf .= "\t rotate $revs\n"; $lr_conf .= "\t start 0\n"; $lr_conf .= "\t compress \n"; $lr_conf .= "\t copy \n"; $lr_conf .= "}\n"; cm_write_file($lr_conf_file, $lr_conf); # logrotate doesn't like when its config is writeable # for anyone but root system("sudo chmod 644 $lr_conf_file"); my $num_revs = cm_get_num_revs(); if (! -e "$archive_dir/commits" or $num_revs == 0) { # store a baseline config system("sudo touch $archive_dir/commits"); system("sudo chgrp vyattacfg $archive_dir/commits"); system("sudo chmod 664 $archive_dir/commits"); my $cmd = "$commit_revs_script baseline config.boot"; system("sudo sg vyattacfg \"export COMMIT_VIA=init; $cmd\""); } exit 0; } exit 0; } if ($action eq 'show-commit-log') { print "show-commit-log\n" if $debug; my $max_revs = cm_get_max_revs(); if (!defined $max_revs or $max_revs <= 0) { print "commit-revisions is not configured.\n\n"; } my @log = cm_commit_get_log(); foreach my $line (@log) { print $line; } exit 0; } if ($action eq 'show-commit-log-brief') { print "show-commit-log-brief\n" if $debug; my $max_revs = cm_get_max_revs(); my @log = cm_commit_get_log(1); foreach my $line (@log) { $line =~ s/\s/_/g; print $line, ' '; } exit 0; } if ($action eq 'show-commit-file') { die "Error: no revnum" if ! defined $revnum; print "show-commit-file [$revnum]\n" if $debug; check_valid_rev($revnum); my $file = cm_commit_get_file($revnum); print $file; exit 0; } if ($action eq 'diff') { print "diff\n" if $debug; my $args = $#ARGV; my $show_args = '--show-show-defaults --show-context-diff'; # flag for command style output my $is_command_format = 0; if ($args >= 0 and $ARGV[$args] eq 'commands') { $is_command_format = 1; $show_args = '$show_args --show-commands'; delete($ARGV[$args]); $args--; } if ($args < 0) { my $rc = system("cli-shell-api sessionChanged"); if (defined $rc and $rc > 0) { print "No changes between working and active configurations.\n"; exit 0; } # default behavior for showConfig is @ACTIVE vs. @WORKING, so no # need to write to a file first my $diff = `cli-shell-api showConfig $show_args`; if (defined $diff and length($diff) > 0) { if ($is_command_format) { $diff = format_commands($diff); } print "$diff"; } else { print "No changes between working and active configurations.\n"; exit 0; } } elsif ($args eq 0) { my $rev1 = $ARGV[0]; check_valid_rev($rev1); my $filename1 = cm_commit_get_file_name($rev1); my $outfile = $filename1; $outfile =~ s/(.*)\.gz/$1/g; system("zcat $filename1 > $outfile"); my $diff = `cli-shell-api showConfig --show-cfg1 $outfile --show-cfg2 \@WORKING $show_args`; if (defined $diff and length($diff) > 0) { if ($is_command_format) { $diff = format_commands($diff); } print "$diff"; } else { print "No changes between working and " . "revision $rev1 configurations.\n"; } system("rm $outfile"); } elsif ($args eq 1) { my $rev1 = $ARGV[0]; my $rev2 = $ARGV[1]; check_valid_rev($rev1); check_valid_rev($rev2); my $filename = cm_commit_get_file_name($rev1); my $filename2 = cm_commit_get_file_name($rev2); my $outfile = $filename; $outfile =~ s/(.*)\.gz/$1/g; system("zcat $filename > $outfile"); my $outfile2 = $filename2; $outfile2 =~ s/(.*)\.gz/$1/g; system("zcat $filename2 > $outfile2"); my $diff = `cli-shell-api showConfig --show-cfg1 $outfile2 --show-cfg2 $outfile $show_args`; if (defined $diff and length($diff) > 0) { if ($is_command_format) { $diff = format_commands($diff); } print "$diff"; } else { print "No changes between revision $rev1 and " . "revision $rev2 configurations.\n"; } system("rm $outfile2"); system("rm $outfile"); } elsif ($args eq 2) { my $rev1 = $ARGV[0]; my $rev2 = $ARGV[1]; check_valid_rev($rev1); check_valid_rev($rev2); my $filename = cm_commit_get_file_name($rev1); my $filename2 = cm_commit_get_file_name($rev2); my $outfile = $filename; $outfile =~ s/(.*)\.gz/$1/g; system("zcat $filename > $outfile"); my $outfile2 = $filename2; $outfile2 =~ s/(.*)\.gz/$1/g; system("zcat $filename2 > $outfile2"); my $diff = `cli-shell-api showConfig --show-cfg1 $outfile2 --show-cfg2 $outfile --show-commands --show-show-defaults --show-context-diff`; if (defined $diff and length($diff) > 0) { my $commands = format_commands($diff); print "$commands" } else { print "No changes between revision $rev1 and " . "revision $rev2 configurations.\n"; } system("rm $outfile2"); system("rm $outfile"); } exit 0; } if ($action eq 'commit-confirm') { die "Error: no minutes defined." if ! defined $minutes; print "commit-confirm [$minutes]\n" if $debug; # Check if there's already a timer. system("systemctl is-active --quiet ${job_name}.timer"); if ($? == 0) { print "Another confirm is pending.\n"; exit 0; } my $max_revs = cm_get_max_revs(); if (!defined $max_revs or $max_revs <= 0) { print "commit-revisions is not configured.\n\n"; exit 1; } check_integer($minutes); print "commit-confirm will automatically reboot in $minutes" . " minutes unless changes are confirmed.\n"; if (prompt("Proceed?", -y1d=>"y")) { } else { print "commit-confirm canceled.\n"; exit 1; } my @lines= cm_read_file(cm_get_boot_config_file()); my $rollback_config = join("\n", @lines); $rollback_config .= "\n"; my $config_rb = cm_get_config_rb(); cm_write_file($config_rb, $rollback_config); my $command = "/opt/vyatta/sbin/vyatta-config-mgmt.pl --action rollback " . "--file $config_rb"; # Schedule a systemd timer to run rollback $minutes later. system("sudo systemd-run --quiet --on-active=${minutes}m --unit=${job_name} " . "sg vyattacfg \"${command}\""); # Check if the timer was created. system("systemctl is-active --quiet ${job_name}.timer"); if ($? != 0) { print "Error: unable to schedule reboot.\n"; exit 1; } print "Reboot scheduled for commit-confirm. " . "Confirm your changes to cancel the reboot.\n"; # Call the script to notify the users of the impending reboot. system("sudo -b /usr/libexec/vyos/commit-confirm-notify.py ${minutes}"); exit 0; } if ($action eq 'confirm') { # Check if timer exists. system("systemctl is-active --quiet ${job_name}.timer"); if ($? != 0) { print "No confirm pending.\n"; } else { # If so, stop it. system("sudo systemctl stop --quiet ${job_name}.timer"); # Kill the notification daemon if it's still running. system("sudo pkill -f commit-confirm-notify.py"); print "Reboot timer stopped.\n"; } exit 0; } if ($action eq 'rollback') { my ($method, $rollback_config) = (undef, undef); if (defined $revnum) { print "rollback [$revnum]\n" if $debug; check_valid_rev($revnum); $method = 'revnum'; if (prompt("Proceed with reboot? [confirm]", -y1d=>"y")) { } else { print "Cancelling rollback\n"; exit 0; } $rollback_config = cm_commit_get_file($revnum); } if (defined $filename) { print "rollback [$filename]\n" if $debug; if (! -e $filename) { die "Error: file [$filename] doesn't exist"; } if (defined $method) { die "Error: can only define revnum or file"; } $method = 'file'; # Should have code to validate config, but for now only # called internally. If we later expose this to cli # we'll need to prompt for confirmation. my @lines = cm_read_file($filename); $rollback_config = join("\n", @lines); $rollback_config .= "\n"; } if (!defined $method) { die "Error: must define either revnum or file"; } my ($user) = getpwuid($<); my $boot_config_file = cm_get_boot_config_file(); my $archive_dir = cm_get_archive_dir(); my $last_commit_file = cm_get_last_commit_file(); system("sudo cp $boot_config_file $archive_dir/config.boot-prerollback"); cm_write_file($boot_config_file, $rollback_config); cm_write_file($last_commit_file, $rollback_config); # white lie my $cmd = "$commit_revs_script --rollback=1 rollback/reboot"; system("sudo sg vyattacfg \"$cmd\""); openlog($0, "", LOG_USER); my $login = getpwuid($<) || "unknown"; syslog("warning", "Rollback reboot requested by $login"); closelog(); exec("sudo /sbin/reboot"); } exit $rc; # end of file