#!/usr/bin/perl -w # # faiupdate -- connect to host, start the fai update and keep stdout # for parsing in FAI::Updater # use FindBin qw($Bin); use lib "$Bin/../lib"; use strict; use warnings; use Scriptalicious; use GreatExpectations ":all"; use Perl6::Junction qw(any); my $timeout = 900; my $st = 45; my $user; my %options = qw( StrictHostKeyChecking no ForwardAgent no ForwardX11 no ); my @extra_ssh_options; my $yes; my $new_class; getopt("timeout|t=i" => \$timeout, "user|u=s" => \$user, "ssh-option|o=s@" => \@extra_ssh_options, "yes|y" => \$yes, "newclass|N" => \$new_class, ); barf "ENOTIMPLEMENTEDYET" if @extra_ssh_options; my $host = shift or abort "no hostname given"; @ARGV and abort "junk at end of command: @ARGV"; system("ping -c 1 $host >/dev/null") == 0 or barf "can't ping $host"; my $password; if ( -t STDIN ) { my $x = ($user ? "$user\@" : ""); $password = prompt_passwd("login/sudo password for $x$host: "); print "\n"; # HACK :) } else { alarm 1; defined($password = ) or abort "no password fed on STDIN"; alarm 0; } # start an interactive shell :) my @ssh_cmd = ("ssh", (map {( "-o", "$_ $options{$_}" )} (keys %options)), "-q", $host); mutter "Logging in."; setup_expect; $exp->spawn(@ssh_cmd); wait_prompt: my $sent_passwd = 0; $exp->expect(60, [ qr/password: /i => sub { whisper "sending password"; $exp->send_slow( (1/9600), $password."\r" ); $sent_passwd = 1; }, ], [ qr/\$ \Z/i => sub { whisper "seen prompt"; } ], ); goto wait_prompt if $sent_passwd; # ok, so, first, we should check the production level of the remote # end. $exp->send('. /etc/catalyst-machineinfo; echo "#$ROLE#"'."\r"); my $role; $exp->expect($st, [ qr/#(\w*)#/ => sub { $role = ($exp->matchlist)[0]; } ]) or vomit "no role response seen"; mutter "production level of $host is ".($role||"not set"); # let's find out what we're syncing. (my $fai_dir = $Bin) =~ s{/[^/]+/?$}{}; open GIT_STATUS, "cd $fai_dir && git-status |" or die $!; (my $fai_branch = ) =~ s{^# On branch (\S+)(?s:.*)}{$1} or barf "failed to parse git-status output"; my $dirty; while () { m{^nothing to commit} && next; $dirty++; } close GIT_STATUS; # ok, so what's the commit ID my $commit_id = `cd $fai_dir && git-rev-parse $fai_branch` or barf "failed to get commit ID for $fai_branch"; chomp($commit_id); $fai_branch =~ s{refs/heads/}{}; if ( $role eq any(qw(internal production staging)) ) { # abort if we're not sending a tagged release barf "refusing to push softupdate of dirty directory to $role host" if $dirty; } # lots of sanity checks that we don't deploy the wrong branch mutter "checking remote profile version"; $exp->send_slow(1/9600, "echo %\$CATALYST_FAI_REV,\$CATALYST_FAI_BRANCH%\r"); my ($r_commit_id, $r_fai_branch); $exp->expect ($st, [ qr/%([0-9a-f]*),([^%]*)%/ => sub { ($r_commit_id, $r_fai_branch) = ($exp->matchlist) } ]) or vomit "no revision seen"; # first question, is the remote revision in the history of the one # we're deploying? if ( length($r_commit_id) ) { unless ( $r_commit_id eq $commit_id or grep m{\s$r_commit_id}, `cd $fai_dir && git-rev-list --parents ${r_commit_id}..${commit_id}` ) { moan("The profile revision on the target server is not an ancestor of the one you are deploying. Usually this means you are deploying the wrong profile branch to a server."); if ( $yes ) { moan("--yes used on command-line, going ahead anyway!"); } else { say("Remote: $r_commit_id, us: $commit_id"); if ( -t STDIN ) { exit(1) unless prompt_yN("Are you SURE you want to do this? "); } else { barf "refusing to deploy bad revision to $host"; } } } if ( $r_fai_branch ne $fai_branch ) { moan("The FAI branch is changing from $r_fai_branch to $fai_branch"); if ( $yes ) { moan("--yes used on command-line, going ahead anyway!"); } else { if ( -t STDIN ) { exit(1) unless prompt_yN("Are you SURE you want to do this? "); } else { barf "refusing to deploy different branch to $host"; } } } } else { moan("$host has not been successfully FAI softupdated yet."); if ( $yes ) { moan("--yes used on command-line, going ahead anyway!"); } else { if ( -t STDIN ) { exit(1) unless prompt_yN("Are you SURE you want to softupdate this host? "); } else { barf "Refusing to softupdate for the first time to $host"; } } } say "sending $fai_dir ($fai_branch:".substr($commit_id,0,6) .($dirty?"+":"").") to $host"; my $exp_2 = Expect->new; $exp_2->raw_pty(1); $exp_2->log_stdout(0) unless $VERBOSE > 1; $exp_2->spawn("rsync", "-ruvza", "--partial", "--delete", "--exclude", ".git*", "$fai_dir/.", "$host:fai"); my $in_rsync = 1; while ( $in_rsync ) { $exp_2->expect ($timeout, [ qr/password: /i => sub { whisper "sending password"; $exp_2->send_slow( (1/9600), $password."\r" ); $sent_passwd = 1; }, ], [ qr/total size is .* speedup is/ => sub { $in_rsync = 0; } ], ); } $exp_2->soft_close; mutter "Now running softupdate"; $exp->send("echo ready\r"); $exp->send_slow ((1/9600), "sudo fai/libexec/softupdate-remote " .($new_class ? "--new-class " : "") ."$fai_branch $commit_id $dirty; exit\r"); my $in_softupdate = 0; $sent_passwd = 0; my $pre_state = "starting"; while ( $in_softupdate or $pre_state ) { $exp->expect ( ($pre_state ? $st : $timeout), ($pre_state eq "starting" ? ( [ qr/password:/i => sub { whisper "sending password"; $exp->send_slow( (1/9600), $password."\r" ); $pre_state = "sent_passwd"; }, ] ) : () ), ($pre_state eq any("starting", "sent_passwd") ? ( [ qr/softupdate-remote: starting/i => sub { start_timer; mutter "softupdate now in progress"; $exp->set_accum($exp->after); $exp->log_stdout(1); $pre_state = ""; $in_softupdate = 1; }, ], [ qr/is not in the sudoers file|is not allowed to run sudo/ => sub { barf "we can't sudo on this host"; } ], [ qr/Sorry, try again./ => sub { barf "sudo password failed!"; } ], ) : () ), [ eof => sub { if ( $pre_state ) { vomit("saw EOF in state $pre_state"); } else { say "softupdate finished in ".show_elapsed; } $in_softupdate = 0; $pre_state = ""; } ], [ timeout => sub { vomit("softupdate timed out in state ${\($pre_state||'softupdate')}; hope aborting is OK"); } ], ); }