use v5.14; use warnings; package Pantry::App::Command::sync; # ABSTRACT: Implements pantry sync subcommand our $VERSION = '0.012'; # VERSION use Pantry::App -command; use autodie; use JSON 2; use Storable qw/dclone/; use Net::OpenSSH; use Path::Class; use Pantry::Model::Util qw/merge_hash dot_to_hash/; use File::Temp 0.22 qw/tempfile/; Net::OpenSSH->VERSION("0.56_01"); use namespace::clean; sub abstract { return 'Run chef-solo on remote node'; } sub command_type { return 'TARGET'; } sub options { my ($self) = @_; return ( $self->sync_options ); } sub valid_types { return qw/node/ } my $rsync_opts = { verbose => 0, # XXX should trigger off a global option compress => 1, recursive => 1, 'delete' => 1, links => 1, times => 1, }; sub _remote_pantry_dir { return "/var/pantry"; }; sub _sync_node { my ($self, $opt, $name) = @_; my $obj = $self->_check_name('node', $name); $name = $obj->name; # canonical name say "Synchronizing $name"; # open SSH connection my $host = $obj->pantry_host // $name; my $port = $obj->pantry_port // 22; my $user = $obj->pantry_user // 'root'; my ($sudo, $sudo_i) = do { $user eq 'root' ? ('','') : ('sudo -- ', 'sudo -i -- '); }; my $ssh = Net::OpenSSH->new($host, port => $port, user => $user); # $Net::OpenSSH::debug = 255; die "Couldn't establish an SSH connection: " . $ssh->error . "\n" if $ssh->error; # ensure destination directory and ownership my $dest_dir = $self->_remote_pantry_dir; $ssh->system($sudo . "mkdir -p $dest_dir") or die "Could not create $dest_dir\n"; if ( $sudo ) { $ssh->system($sudo . "chown $user $dest_dir") or die "Could not chown $dest_dir to $user\n"; } # generate local solo.rb and rsync it to /etc/chef/solo.rb my ($fh, $solo_rb) = tempfile( "pantry-solo.rb-XXXXXX", TMPDIR => 1 ); print {$fh} $self->_solo_rb_guts; close $fh; $ssh->rsync_put($rsync_opts, $solo_rb, "$dest_dir/solo.rb") or die "Could not rsync solo.rb\n"; # prepare node JSON for upload # XXX should really check to be sure it exists my ($node_fh, $node_json) = tempfile( "node.json-XXXXXX", TMPDIR => 1 ); $obj->save_as($node_json); # XXX Chef Solo doesn't yet support environment data files, so we merge them # in manually, though it doesn't quite match Chef's natural attribute # precedence table. # http://wiki.opscode.com/display/chef/Attributes#Attributes-Precedence # # When Chef Solo supports env files (in repo but not released) this won't be # necessary, but we'll need to upload environment files and configure them in # solo.rb my $env = $self->pantry->environment($obj->env); if ( -e $env->path ) { # if we have a data file, then merge it my $base = decode_json( file($node_json)->slurp ); my $env_default = dclone $env->default_attributes; my $env_override = dclone $env->override_attributes; for my $hash ( $env_default, $env_override ) { for my $k ( keys %$hash ) { dot_to_hash($hash, $k, $hash->{$k}); delete $hash->{$k}; } } my $merged = merge_hash( merge_hash( $env_default, $base ), $env_override ); file($node_json)->spew( to_json( $merged, { utf8 => 1, pretty => 1 } ) ); } # rsync node JSON to remote /etc/chef/node.json $ssh->rsync_put($rsync_opts, $node_json, "$dest_dir/node.json") or die "Could not rsync node.json\n"; # rsync cookbooks to remote /var/chef-solo/cookbooks $ssh->rsync_put($rsync_opts, "cookbooks", $dest_dir) or die "Could not rsync cookbooks\n"; # rsync roles to remote /var/chef-solo/roles $ssh->rsync_put($rsync_opts, "roles", $dest_dir) or die "Could not rsync roles\n"; # rsync databags to remote /var/chef-solo/data_bags $ssh->rsync_put($rsync_opts, "data_bags", $dest_dir) or die "Could not rsync data_bags\n"; # ssh execute chef-solo my $command = $sudo_i . "chef-solo -c $dest_dir/solo.rb"; $command .= " -l debug" if $ENV{PANTRY_CHEF_DEBUG}; $ssh->system({tty => $sudo ? 1 : 0}, $command) # XXX eventually capture output or die "Error running chef-solo\n"; # cleanup report permissions if running under sudo so we can find/download it $ssh->system($sudo . "chown -R $user $dest_dir/reports") if $sudo; # scp get run report my $report = $ssh->capture("ls -t $dest_dir/reports | head -1"); chomp $report; # XXX should check that the report timestamp makes sense -- xdg, 2012-05-03 dir("reports")->mkpath; $ssh->rsync_get($rsync_opts, "$dest_dir/reports/$report", "reports/$name"); if ( $opt->{reboot} ) { $ssh->system($sudo . "shutdown -r now"); } } sub _solo_rb_guts { my ($self) = @_; my $dest_dir = $self->_remote_pantry_dir; return << "HERE"; file_cache_path "$dest_dir" cookbook_path "$dest_dir/cookbooks" role_path "$dest_dir/roles" data_bag_path "$dest_dir/data_bags" json_attribs "$dest_dir/node.json" require 'chef/handler/json_file' report_handlers << Chef::Handler::JsonFile.new(:path => "$dest_dir/reports") exception_handlers << Chef::Handler::JsonFile.new(:path => "$dest_dir/reports") HERE } 1; # vim: ts=2 sts=2 sw=2 et: __END__ =pod =head1 NAME Pantry::App::Command::sync - Implements pantry sync subcommand =head1 VERSION version 0.012 =head1 SYNOPSIS $ pantry sync node foo.example.com =head1 DESCRIPTION This class implements the C command, which is used to rsync recipes and node data to a server and then run C on the server to finish configuration. =for Pod::Coverage options validate =head1 AUTHOR David Golden =head1 COPYRIGHT AND LICENSE This software is Copyright (c) 2011 by David Golden. This is free software, licensed under: The Apache License, Version 2.0, January 2004 =cut