mirror of
https://github.com/xcat2/xcat-core.git
synced 2025-05-21 19:22:05 +00:00
1143 lines
37 KiB
Perl
1143 lines
37 KiB
Perl
# IBM(c) 2007 EPL license http://www.eclipse.org/legal/epl-v10.html
|
|
#-------------------------------------------------------
|
|
|
|
=head1
|
|
xCAT plugin package to handle docker
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
package xCAT_plugin::docker;
|
|
|
|
BEGIN
|
|
{
|
|
$::XCATROOT = $ENV{'XCATROOT'} ? $ENV{'XCATROOT'} : '/opt/xcat';
|
|
my $async_path = "/usr/local/share/perl5/";
|
|
unless (grep { $_ eq $async_path } @INC) {
|
|
push @INC, $async_path;
|
|
}
|
|
}
|
|
use lib "$::XCATROOT/lib/perl";
|
|
|
|
#use strict;
|
|
use POSIX qw(WNOHANG nice);
|
|
use POSIX qw(WNOHANG setsid :errno_h);
|
|
use Errno;
|
|
use MIME::Base64 qw(encode_base64);
|
|
require IO::Socket::SSL; IO::Socket::SSL->import('inet4');
|
|
use Time::HiRes qw(gettimeofday sleep);
|
|
use Fcntl qw/:DEFAULT :flock/;
|
|
use File::Path;
|
|
use File::Copy;
|
|
use File::Basename;
|
|
use Getopt::Long;
|
|
Getopt::Long::Configure("bundling");
|
|
use HTTP::Headers;
|
|
use HTTP::Request;
|
|
use xCAT::Utils;
|
|
use xCAT::MsgUtils;
|
|
use Cwd;
|
|
use xCAT::Usage;
|
|
use JSON;
|
|
|
|
my $verbose;
|
|
my $global_callback;
|
|
my $subreq;
|
|
|
|
my $async;
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 The hash variable to store node related http request id
|
|
|
|
The structure is like this
|
|
%http_session_variable = (
|
|
$session_id => $node,
|
|
);
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
my %http_session_variable = ();
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 The hash variable to store node parameters to access docker container
|
|
|
|
The structure is like this
|
|
%node_hash_variable = (
|
|
$node => {
|
|
image=>$nodetype.provmethod,
|
|
cmd=>$nodetype.provmethod,
|
|
ip=>$host.ip,
|
|
nics=>$vm.vmnics,
|
|
mac=>$mac.mac,
|
|
cpu=>$vm.cpus
|
|
memory=>$vm.memory
|
|
flag=>$vm.othersettings,
|
|
hostinfo=>{
|
|
name => $host,
|
|
port => $port,
|
|
},
|
|
genreq_ptr => \&genreq;
|
|
http_req_method => $init_method,
|
|
http_req_url => $node_init_url,
|
|
node_app_state => $init_state,
|
|
state_machine_engine => $state_machine_engine,
|
|
},
|
|
);
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
my %node_hash_variable = ();
|
|
|
|
# The num of HTTP requests that is progressing
|
|
my $http_requests_in_progress = 0;
|
|
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 handled_commands
|
|
|
|
Return list of commands handled by this plugin
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
sub handled_commands {
|
|
return ({ docker => "docker",
|
|
rpower => 'nodehm:mgt',
|
|
mkdocker => 'nodehm:mgt',
|
|
rmdocker => 'nodehm:mgt',
|
|
lsdocker => 'nodehm:mgt=docker|ipmi|kvm',
|
|
});
|
|
}
|
|
|
|
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 The hash table to store mapping of commands and its state_machine_engine
|
|
The structure is like this:
|
|
command => {
|
|
option1 => {
|
|
state_machine_engine => \&state_machine_engine,
|
|
init_method => GET/POST/PUT/DELETE,
|
|
init_url => url,
|
|
},
|
|
},
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
my %command_states = (
|
|
|
|
# For rpower start/stop/restart/pause/unpause/state
|
|
# return error_msg if failed or corresponding msg if success
|
|
rpower => {
|
|
start => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/start",
|
|
init_state => "INIT_TO_WAIT_FOR_START_DONE",
|
|
},
|
|
stop => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/stop",
|
|
init_state => "INIT_TO_WAIT_FOR_STOP_DONE",
|
|
},
|
|
restart => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/restart",
|
|
},
|
|
pause => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/pause",
|
|
},
|
|
unpause => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/unpause",
|
|
},
|
|
state => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "GET",
|
|
init_url => "/containers/#NODE#/json",
|
|
init_state => "INIT_TO_WAIT_FOR_QUERY_STATE_DONE",
|
|
},
|
|
},
|
|
|
|
# The state changing graphic for mkdocker
|
|
# error
|
|
# init-----------> INIT_TO_WAIT_FOR_CREATE_DONE -----------------> error_msg
|
|
# ^ / |
|
|
# | 404 and / |
|
|
# 20x| 'No such image'/ |
|
|
# | v | error
|
|
# CREATE_TO_WAIT_FOR_IMAGE_PULL_DONE ------------------------------> error_msg
|
|
# |
|
|
# |
|
|
# 20x|
|
|
# v
|
|
# create done
|
|
#
|
|
mkdocker => {
|
|
default => {
|
|
genreq_ptr => \&genreq_for_mkdocker,
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/create?name=#NODE#",
|
|
init_state => "INIT_TO_WAIT_FOR_CREATE_DONE"
|
|
},
|
|
pullimage => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/images/create?fromImage=#DOCKER_IMAGE#",
|
|
init_state => "CREATE_TO_WAIT_FOR_IMAGE_PULL_DONE",
|
|
},
|
|
},
|
|
|
|
# For rmdocker
|
|
# return error_msg if failed or success if done
|
|
rmdocker => {
|
|
force => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "DELETE",
|
|
init_url => "/containers/#NODE#?force=1",
|
|
},
|
|
default => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "DELETE",
|
|
init_url => "/containers/#NODE#",
|
|
},
|
|
},
|
|
|
|
# For lsdocker [-l|--logs]
|
|
# return error_msg if failed or corresponding msg if success
|
|
lsdocker => {
|
|
default => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "GET",
|
|
init_url => "/containers/#NODE#/json?",
|
|
init_state => "INIT_TO_WAIT_FOR_QUERY_DOCKER_DONE",
|
|
},
|
|
log => {
|
|
state_machine_engine => \&default_state_engine,
|
|
init_method => "GET",
|
|
init_url => "/containers/#NODE#/logs?stderr=1&stdout=1",
|
|
init_state => "INIT_TO_WAIT_FOR_QUERY_LOG_DONE",
|
|
},
|
|
},
|
|
);
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 http_state_code_info
|
|
The function to deal with http response code
|
|
Input:
|
|
$state_code: the http response code
|
|
$curr_status: the current status for the SSL connection that receive the http response
|
|
It is used for rpower start/stop since they use the same state_code 304 to indicate no modification.
|
|
Return:
|
|
A string to explain the http response code
|
|
Usage example:
|
|
http_state_code_info('304', "INIT_TO_WAIT_FOR_START_DONE") -> "Already started"
|
|
http_state_code_info('304', "INIT_TO_WAIT_FOR_STOP_DONE") -> "Already stopped"
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub http_state_code_info {
|
|
my $state_code = shift;
|
|
my $curr_status = shift;
|
|
if ($state_code =~ /20\d/) {
|
|
return [ 0, "success" ];
|
|
}
|
|
elsif ($state_code eq '304') {
|
|
if (defined $curr_status) {
|
|
if ($curr_status eq "INIT_TO_WAIT_FOR_START_DONE") {
|
|
return [ 0, "container already started" ];
|
|
}
|
|
else {
|
|
return [ 0, "container already stopped" ];
|
|
}
|
|
}
|
|
else {
|
|
return [ 1, "unknown http status code $state_code" ];
|
|
}
|
|
}
|
|
elsif ($state_code eq '404') {
|
|
return [ 1, "no such container" ];
|
|
}
|
|
elsif ($state_code eq '406') {
|
|
return [ 1, "impossible to attach (container not running)" ];
|
|
}
|
|
elsif ($state_code eq '500') {
|
|
return [ 1, "server error" ];
|
|
}
|
|
return [ 1, "unknown http status code $state_code" ];
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 modify_node_state_hash
|
|
To change node state to the state specified.
|
|
Input:
|
|
$node: the node to change state
|
|
$to_state_hash: the hash which store the destination state info
|
|
Return:
|
|
Usage example:
|
|
modify_node_state_hash($node, $command_states{$command}{$option});
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub modify_node_state_hash {
|
|
my $node = shift;
|
|
my $to_state_hash = shift;
|
|
my $node_hash = $node_hash_variable{$node};
|
|
$node_hash->{http_req_method} = $to_state_hash->{init_method};
|
|
$node_hash->{http_req_url} = $to_state_hash->{init_url};
|
|
$node_hash->{node_app_state} = $to_state_hash->{init_state};
|
|
$node_hash->{state_machine_engine} = $to_state_hash->{state_machine_engine};
|
|
$node_hash->{genreq_ptr} = $to_state_hash->{genreq_ptr};
|
|
if (!defined($node_hash->{genreq_ptr})) {
|
|
$node_hash->{genreq_ptr} = \&genreq;
|
|
}
|
|
if ($node_hash->{image} =~ /:/) {
|
|
$node_hash->{http_req_url} =~ s/#DOCKER_IMAGE#/$node_hash->{image}/;
|
|
} else {
|
|
$node_hash->{http_req_url} =~ s/#DOCKER_IMAGE#/$node_hash->{image}:latest/;
|
|
}
|
|
$node_hash->{http_req_url} =~ s/#NETNAME#/$node_hash->{nics}/;
|
|
$node_hash->{http_req_url} =~ s/#NODE#/$node/;
|
|
return;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 change_node_state
|
|
To change node state to the state specified, and then send out the HTTP request.
|
|
Input:
|
|
$node: the node to change state
|
|
$to_state_hash: the hash which store the destination state info
|
|
Return:
|
|
Usage example:
|
|
change_node_state($node, $command_states{$command}{$option});
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub change_node_state {
|
|
my ($node, $to_state_hash) = @_;
|
|
modify_node_state_hash(@_);
|
|
sendreq($node, $node_hash_variable{$node});
|
|
return;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 default_state_engine
|
|
|
|
The state_machine_engine to deal with http response
|
|
Input:
|
|
$id: The http session id when adding HTTP request into HTTP::Async object
|
|
$data: The http response
|
|
Return:
|
|
If there are any errors or msg, they will be outputed directly.
|
|
Else, nothing returned.
|
|
Usage example:
|
|
default_state_engine($id, HTTP Response data);
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub default_state_engine {
|
|
my $id = shift;
|
|
my $data = shift;
|
|
my $node = $http_session_variable{$id};
|
|
if (!defined($node)) {
|
|
return;
|
|
}
|
|
my $node_hash = $node_hash_variable{$node};
|
|
my $curr_state = $node_hash->{node_app_state};
|
|
my $info_flag = 'data';
|
|
|
|
if ($data->is_error or (defined($data->header("connection")) and $data->header("connection") =~ /close/)) {
|
|
$http_requests_in_progress--;
|
|
delete($http_session_variable{$id});
|
|
}
|
|
|
|
my $content = $data->decoded_content;
|
|
my @msg = ();
|
|
$msg[0] = &http_state_code_info($data->code, $curr_state);
|
|
if ($data->is_error) {
|
|
if ($content ne '') {
|
|
$msg[0]->[1] = "$content";
|
|
}
|
|
elsif ($data->message ne '') {
|
|
$msg[0]->[1] = $data->message;
|
|
}
|
|
}
|
|
my $content_type = $data->header("content-type");
|
|
my $content_hash = undef;
|
|
if (defined($content_type) and $content_type =~ /json/i) {
|
|
if ($curr_state ne "CREATE_TO_WAIT_FOR_IMAGE_PULL_DONE") {
|
|
$content_hash = decode_json $content;
|
|
}
|
|
else {
|
|
if ($content =~ /Status: Downloaded newer image/) {
|
|
|
|
}
|
|
elsif ($content =~ /\"error\":\"([^\"]*)\"/) {
|
|
@msg = ();
|
|
$msg[0] = [ 1, $1 ];
|
|
}
|
|
}
|
|
}
|
|
elsif (!defined($content_type)) {
|
|
$content_type = "undefined";
|
|
}
|
|
|
|
if ($curr_state eq "INIT_TO_WAIT_FOR_QUERY_STATE_DONE") {
|
|
if ($data->is_success) {
|
|
if ($content_type =~ /json/i) {
|
|
my $node_state = $content_hash->{'State'}->{'Status'};
|
|
if (defined($node_state)) {
|
|
$msg[0] = [ 0, $node_state ];
|
|
}
|
|
else {
|
|
$msg[0] = [ 1, "Can not get status" ];
|
|
}
|
|
}
|
|
else {
|
|
$msg[0] = [ 1, "The content type: $content_type is unable to be parsed." ];
|
|
}
|
|
}
|
|
}
|
|
elsif ($curr_state eq "INIT_TO_WAIT_FOR_QUERY_LOG_DONE") {
|
|
if ($data->is_success) {
|
|
$info_flag = "base64_data";
|
|
@msg = ();
|
|
if ($content_type =~ /text\/plain/i) {
|
|
$msg[0] = [ 0, encode_base64($content) ];
|
|
}
|
|
else {
|
|
$msg[0] = [ 1, "The content type: $content_type is unable to be parsed." ];
|
|
}
|
|
}
|
|
}
|
|
elsif ($curr_state eq "INIT_TO_WAIT_FOR_QUERY_DOCKER_DONE") {
|
|
if ($data->is_success) {
|
|
@msg = ();
|
|
if ($content_type =~ /json/i) {
|
|
if (ref($content_hash) eq 'ARRAY') {
|
|
foreach (@$content_hash) {
|
|
push @msg, [ 0, parse_docker_list_info($_, 1) ];
|
|
}
|
|
}
|
|
else {
|
|
push @msg, [ 0, parse_docker_list_info($content_hash, 0) ];
|
|
}
|
|
}
|
|
if (!scalar(@msg)) {
|
|
@msg = [ 0, "No running docker" ];
|
|
}
|
|
}
|
|
}
|
|
elsif ($curr_state eq 'INIT_TO_WAIT_FOR_CREATE_DONE') {
|
|
if ($data->code eq '404' and $msg[0]->[1] =~ /image:/i) {
|
|
|
|
# To avoid pulling image loop
|
|
if (defined($node_hash->{have_pulled_image})) {
|
|
return;
|
|
}
|
|
$global_callback->({ node => [ { name => [$node], "$info_flag" => ["Pull image $node_hash->{image} start"] } ] });
|
|
change_node_state($node, $command_states{mkdocker}{pullimage});
|
|
return;
|
|
}
|
|
}
|
|
elsif ($curr_state eq 'CREATE_TO_WAIT_FOR_IMAGE_PULL_DONE') {
|
|
if ($data->is_success and !$msg[0]->[0]) {
|
|
$global_callback->({ node => [ { name => [$node], "$info_flag" => ["Pull image $node_hash->{image} done"] } ] });
|
|
$node_hash->{have_pulled_image} = 1;
|
|
change_node_state($node, $command_states{mkdocker}{default});
|
|
return;
|
|
}
|
|
}
|
|
|
|
foreach my $tmp (@msg) {
|
|
if ($tmp->[0]) {
|
|
$global_callback->({ node => [ { name => [$node], error => ["$tmp->[1]"], errorcode => ["$tmp->[0]"] } ] });
|
|
}
|
|
else {
|
|
$global_callback->({ node => [ { name => [$node], "$info_flag" => ["$tmp->[1]"] } ] });
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 deal_with_space_in_array_entry
|
|
|
|
The function to add '' for entries that have spaces
|
|
Input:
|
|
$array: The string array whose entires may have spaces
|
|
Return:
|
|
A string that join the entries in input $array with space,
|
|
for entries have spaces, they will be put in "'"
|
|
Usage example:
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub deal_with_space_in_array_entry {
|
|
my $array = shift;
|
|
my @ret_array = ();
|
|
push @ret_array, shift @$array;
|
|
foreach (@$array) {
|
|
if (/\s/) {
|
|
push @ret_array, "'$_'";
|
|
}
|
|
else {
|
|
push @ret_array, $_;
|
|
}
|
|
}
|
|
return join(' ', @ret_array);
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 parse_docker_list_info
|
|
|
|
The function to parse the content returned by the lsdocker command
|
|
Input:
|
|
$docker_info_hash: The hash variable which include docker infos
|
|
The variable is decoded from JSON string
|
|
$flag: To show the info is get from dockerhost (1) or a speciifed docker (0)
|
|
Return:
|
|
docker_info_string in the format: $id $image $command $created $status $names;
|
|
Usage example:
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub parse_docker_list_info {
|
|
my $docker_info_hash = shift;
|
|
my $flag = shift; # Use the flag to check whether need to cut command
|
|
my ($id, $image, $command, $created, $status, $names);
|
|
$id = substr($docker_info_hash->{'Id'}, 0, 12);
|
|
if ($flag) {
|
|
$image = $docker_info_hash->{'Image'};
|
|
$command = $docker_info_hash->{'Command'};
|
|
$created = $docker_info_hash->{'Created'};
|
|
$status = $docker_info_hash->{'Status'};
|
|
|
|
$names = $docker_info_hash->{'Names'}->[0];
|
|
my ($sec, $min, $hour, $day, $mon, $year) = localtime($created);
|
|
$mon += 1;
|
|
$year += 1900;
|
|
$created = "$year-$mon-$day - $hour:$min:$sec";
|
|
}
|
|
else {
|
|
$image = $docker_info_hash->{Config}->{'Image'};
|
|
my @cmd = ();
|
|
push @cmd, $docker_info_hash->{Path};
|
|
if (defined($docker_info_hash->{Args})) {
|
|
push @cmd, @{ $docker_info_hash->{Args} };
|
|
}
|
|
$command = deal_with_space_in_array_entry(\@cmd);
|
|
$names = $docker_info_hash->{'Name'};
|
|
$created = $docker_info_hash->{'Created'};
|
|
$status = $docker_info_hash->{'State'}->{'Status'};
|
|
$created =~ s/\..*$//;
|
|
}
|
|
my $cmd = sprintf("\"%.20s\"", $command);
|
|
my $string = sprintf("%-12s %-30.30s %-22s %-20s %-10s %s", $id, $image, $cmd, $created, $status, $names);
|
|
return ($string);
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 deal_with_rsp
|
|
|
|
The function to deal with SELECT
|
|
Input:
|
|
%args: a hash which currently only key 'timeout' is using
|
|
Return:
|
|
The number of response have received
|
|
Usage example:
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub deal_with_rsp
|
|
{
|
|
my %args = @_;
|
|
my $timeout = 0;
|
|
if (defined($args{timeout})) {
|
|
$timeout = $args{timeout};
|
|
}
|
|
my $deal_num = 0;
|
|
while (my ($response, $id) = $async->wait_for_next_response($timeout)) {
|
|
my $node = $http_session_variable{$id};
|
|
if (defined($node)) {
|
|
$deal_num++;
|
|
$node_hash_variable{$node}->{state_machine_engine}->($id, $response);
|
|
}
|
|
}
|
|
|
|
return $deal_num;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 parse_args
|
|
|
|
Parse the command line options and operands
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
sub parse_args {
|
|
|
|
my $request = shift;
|
|
my $args = $request->{arg};
|
|
my $cmd = $request->{command}->[0];
|
|
my %opt;
|
|
#############################################
|
|
# Responds with usage statement
|
|
#############################################
|
|
local *usage = sub {
|
|
my $usage_string = xCAT::Usage->getUsage($cmd);
|
|
return ([ $_[0], $usage_string ]);
|
|
};
|
|
#############################################
|
|
# No command-line arguments - use defaults
|
|
#############################################
|
|
if (!defined($args)) {
|
|
if ($cmd eq "rpower") {
|
|
return ([ 1, "No option specified for rpower" ]);
|
|
}
|
|
return (0);
|
|
}
|
|
#############################################
|
|
# Checks case in GetOptions, allows opts
|
|
# to be grouped (e.g. -vx), and terminates
|
|
# at the first unrecognized option.
|
|
#############################################
|
|
@ARGV = @$args;
|
|
$Getopt::Long::ignorecase = 0;
|
|
Getopt::Long::Configure("bundling");
|
|
|
|
#############################################
|
|
# Process command-line flags
|
|
#############################################
|
|
if (!GetOptions(\%opt,
|
|
qw(h|help V|verbose v|version))) {
|
|
return (usage());
|
|
}
|
|
|
|
#############################################
|
|
# Option -V for verbose output
|
|
#############################################
|
|
if (exists($opt{V})) {
|
|
$verbose = 1;
|
|
}
|
|
if ($cmd eq "rpower") {
|
|
if (scalar(@ARGV) > 1) {
|
|
return ([ 1, "Only one option is supportted at the same time" ]);
|
|
}
|
|
elsif (!defined($command_states{$cmd}{ $ARGV[0] })) {
|
|
return ([ 1, "The option $ARGV[0] not support for $cmd" ]);
|
|
}
|
|
else {
|
|
$request->{mapping_option} = $ARGV[0];
|
|
}
|
|
}
|
|
elsif ($cmd eq 'mkdocker') {
|
|
my ($image, $command);
|
|
foreach my $op (@ARGV) {
|
|
my ($key, $value) = split /=/, $op;
|
|
if ($key !~ /image|command|dockerflag/) {
|
|
return ([ 1, "Option $key is not supported for $cmd" ]);
|
|
}
|
|
elsif (!defined($value)) {
|
|
return ([ 1, "Must set value for $key" ]);
|
|
}
|
|
else {
|
|
if ($key eq 'image') {
|
|
$image = $value;
|
|
}
|
|
elsif ($key eq 'command') {
|
|
$command = $value;
|
|
}
|
|
}
|
|
}
|
|
if (!defined($image) and defined($command)) {
|
|
return ([ 1, "Must set 'image' if use 'command'" ]);
|
|
}
|
|
}
|
|
elsif ($cmd eq 'rmdocker') {
|
|
foreach my $op (@ARGV) {
|
|
if ($op ne '-f' and $op ne '--force') {
|
|
return ([ 1, "Option $op is not supported for $cmd" ]);
|
|
}
|
|
}
|
|
$request->{mapping_option} = "force";
|
|
}
|
|
elsif ($cmd eq 'lsdocker') {
|
|
foreach my $op (@ARGV) {
|
|
if ($op ne '-l' and $op ne '--logs') {
|
|
return ([ 1, "Option $op is not supported for $cmd" ]);
|
|
}
|
|
}
|
|
$request->{mapping_option} = "log";
|
|
}
|
|
|
|
|
|
return;
|
|
}
|
|
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 preprocess_request
|
|
|
|
preprocess the command
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
sub preprocess_request {
|
|
my $req = shift;
|
|
if ($req->{_xcatpreprocessed}->[0] == 1) { return [$req]; }
|
|
my $callback = shift;
|
|
my $command = $req->{command}->[0];
|
|
my $extrargs = $req->{arg};
|
|
my @exargs = ($req->{arg});
|
|
if (ref($extrargs)) {
|
|
@exargs = @$extrargs;
|
|
}
|
|
my $usage_string = xCAT::Usage->parseCommand($command, @exargs);
|
|
if ($usage_string) {
|
|
$callback->({ data => [$usage_string] });
|
|
$req = {};
|
|
return;
|
|
}
|
|
####################################
|
|
# Process command-specific options
|
|
####################################
|
|
my $parse_result = parse_args($req);
|
|
####################################
|
|
# Return error
|
|
####################################
|
|
if (ref($parse_result) eq 'ARRAY') {
|
|
$callback->({ error => $parse_result->[1], errorcode => $parse_result->[0] });
|
|
$req = {};
|
|
return;
|
|
}
|
|
|
|
my @result = ();
|
|
my $mncopy = {%$req};
|
|
push @result, $mncopy;
|
|
return \@result;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 process_request
|
|
|
|
Process the command
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
sub process_request {
|
|
my $req = shift;
|
|
my $callback = shift;
|
|
$subreq = shift;
|
|
my $noderange = $req->{node};
|
|
my $command = $req->{command}->[0];
|
|
my $args = $req->{arg};
|
|
$global_callback = $callback;
|
|
|
|
# For docker create, the attributes needed are
|
|
# vm.host,cpus,memory,othersettings
|
|
# nodetype.provmethod -- the image and command the docker will use
|
|
# mac.mac
|
|
# For other command, get docker host is enough to do operation
|
|
|
|
my $mapping_hash = undef;
|
|
if (defined($req->{mapping_option})) {
|
|
$mapping_hash = $command_states{$command}{ $req->{mapping_option} };
|
|
}
|
|
else {
|
|
$mapping_hash = $command_states{$command}{default};
|
|
}
|
|
my $max_concur_session_allow = 20; # A variable can be set by caculated in the future
|
|
if ($command eq 'lsdocker') {
|
|
my @new_noderange = ();
|
|
my $nodehm = xCAT::Table->new('nodehm');
|
|
if ($nodehm) {
|
|
my $nodehmhash = $nodehm->getNodesAttribs($noderange, ['mgt']);
|
|
foreach my $node (@$noderange) {
|
|
if (defined($nodehmhash->{$node}->[0]->{mgt}) and $nodehmhash->{$node}->[0]->{mgt} =~ /ipmi|kvm/) {
|
|
|
|
if (defined($args) and $args->[0] ne '') {
|
|
$callback->({ error => [" $args->[0] is not support for $node"], errorcode => 1 });
|
|
return;
|
|
}
|
|
${ $node_hash_variable{$node} }{hostinfo} = { name => $node, port => '2375' };
|
|
$mapping_hash->{init_url} =~ s/#NODE#\///;
|
|
modify_node_state_hash($node, $mapping_hash);
|
|
}
|
|
else {
|
|
push @new_noderange, $node;
|
|
}
|
|
}
|
|
}
|
|
$noderange = \@new_noderange;
|
|
}
|
|
|
|
# The dockerhost is mapped to vm.host, so open vm table here
|
|
my $vmtab = xCAT::Table->new('vm');
|
|
if ($vmtab) {
|
|
my $vmhashs = $vmtab->getNodesAttribs($noderange, [ 'host', 'nics' ]);
|
|
if ($vmhashs) {
|
|
my @errornodes = ();
|
|
foreach my $node (@$noderange) {
|
|
my $vmhash = $vmhashs->{$node}->[0];
|
|
if (!defined($vmhash) or !defined($vmhash->{host})) {
|
|
delete $node_hash_variable{$node};
|
|
push @errornodes, $node;
|
|
next;
|
|
}
|
|
my ($host, $port) = split /:/, $vmhash->{host};
|
|
if (!defined($host)) {
|
|
delete $node_hash_variable{$node};
|
|
push @errornodes, $node;
|
|
next;
|
|
}
|
|
if (!defined($port)) {
|
|
$port = 2375;
|
|
}
|
|
${ $node_hash_variable{$node} }{hostinfo} = { name => $host, port => $port };
|
|
if (defined($vmhash->{nics})) {
|
|
$node_hash_variable{$node}->{nics} = $vmhash->{nics};
|
|
} else {
|
|
$node_hash_variable{$node}->{nics} = "mynet0";
|
|
}
|
|
if ($command eq 'rmdocker') {
|
|
if (defined($args->[0])) {
|
|
$node_hash_variable{$node}->{opt} = "force";
|
|
}
|
|
else {
|
|
$node_hash_variable{$node}->{opt} = "default";
|
|
}
|
|
}
|
|
modify_node_state_hash($node, $mapping_hash);
|
|
}
|
|
if (scalar(@errornodes)) {
|
|
$callback->({ error => ["Docker host not set correct for @errornodes"], errorcode => 1 });
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
$callback->({ error => ["Open table 'vm' failed"], errorcode => 1 });
|
|
return;
|
|
}
|
|
|
|
#parse parameters for mkdocker
|
|
if ($command eq 'mkdocker') {
|
|
my ($imagearg, $cmdarg, $flagarg);
|
|
foreach (@$args) {
|
|
if (/image=(.*)$/) {
|
|
$imagearg = $1;
|
|
}
|
|
elsif (/command=(.*)$/) {
|
|
$cmdarg = $1;
|
|
}
|
|
elsif (/dockerflag=(.*)$/) {
|
|
$flagarg = $1;
|
|
}
|
|
}
|
|
my $nodetypetab = xCAT::Table->new('nodetype');
|
|
if (!defined($nodetypetab)) {
|
|
$callback->({ error => ["Open table 'nodetype' failed"], errorcode => 1 });
|
|
return;
|
|
}
|
|
my $mactab = xCAT::Table->new('mac');
|
|
if (!defined($mactab)) {
|
|
$callback->({ error => ["Open table 'mac' failed"], errorcode => 1 });
|
|
return;
|
|
}
|
|
my ($ret, $netcfg_hash) = xCAT::NetworkUtils->getNodesNetworkCfg($noderange);
|
|
if ($ret) {
|
|
$callback->({ error => [$netcfg_hash], errorcode => 1 });
|
|
return;
|
|
}
|
|
my $nodetypehash = $nodetypetab->getNodesAttribs($noderange, ['provmethod']);
|
|
my $machash = $mactab->getNodesAttribs($noderange, ['mac']);
|
|
my $vmhash = $vmtab->getNodesAttribs($noderange, [ 'cpus', 'memory', 'othersettings' ]);
|
|
|
|
my %errornodes = ();
|
|
foreach my $node (@$noderange) {
|
|
if ($imagearg) {
|
|
$node_hash_variable{$node}->{image} = $imagearg;
|
|
if ($cmdarg) {
|
|
$node_hash_variable{$node}->{cmd} = $cmdarg;
|
|
$nodetypetab->setNodeAttribs($node, { provmethod => "$imagearg!$cmdarg" });
|
|
}
|
|
else {
|
|
$nodetypetab->setNodeAttribs($node, { provmethod => "$imagearg" });
|
|
}
|
|
}
|
|
else {
|
|
if (!defined($nodetypehash->{$node}->[0]->{provmethod})) {
|
|
delete $node_hash_variable{$node};
|
|
push @{ $errornodes{Image} }, $node;
|
|
next;
|
|
}
|
|
else {
|
|
my ($tmp_img, $tmp_cmd) = split /!/, $nodetypehash->{$node}->[0]->{provmethod};
|
|
if (!defined($tmp_img)) {
|
|
delete $node_hash_variable{$node};
|
|
push @{ $errornodes{Image} }, $node;
|
|
next;
|
|
}
|
|
$node_hash_variable{$node}->{image} = $tmp_img;
|
|
$node_hash_variable{$node}->{cmd} = $tmp_cmd;
|
|
}
|
|
}
|
|
if ($flagarg) {
|
|
$node_hash_variable{$node}->{flag} = $flagarg;
|
|
$vmtab->setNodeAttribs($node, { othersettings => $flagarg });
|
|
}
|
|
if (defined($machash->{$node}->[0]->{mac})) {
|
|
$node_hash_variable{$node}->{mac} = $machash->{$node}->[0]->{mac};
|
|
}
|
|
my $vmnodehash = $vmhash->{$node}->[0];
|
|
if (defined($vmnodehash)) {
|
|
if (defined($vmnodehash->{cpus})) {
|
|
$node_hash_variable{$node}->{cpus} = $vmnodehash->{cpus};
|
|
}
|
|
if (defined($vmnodehash->{memory})) {
|
|
$node_hash_variable{$node}->{memory} = $vmnodehash->{memory};
|
|
}
|
|
if (!defined($flagarg) and defined($vmnodehash->{othersettings})) {
|
|
$node_hash_variable{$node}->{flag} = $vmnodehash->{othersettings};
|
|
}
|
|
}
|
|
my $netcfg_info = $netcfg_hash->{$node};
|
|
if (!defined($netcfg_info) or !defined($netcfg_info->{'ip'})) {
|
|
delete $node_hash_variable{$node};
|
|
push @{ $errornodes{Network} }, $node;
|
|
next;
|
|
}
|
|
else {
|
|
$node_hash_variable{$node}->{ip} = $netcfg_info->{ip};
|
|
}
|
|
}
|
|
$nodetypetab->close;
|
|
$mactab->close;
|
|
foreach (keys %errornodes) {
|
|
$callback->({ error => ["$_ not set correct for @{$errornodes{$_}}"], errorcode => 1 });
|
|
}
|
|
}
|
|
$vmtab->close;
|
|
|
|
if (my $res = init_async(slots => $max_concur_session_allow)) {
|
|
$callback->({ error => [$res], errorcode => 1 });
|
|
return;
|
|
}
|
|
my @nodeargs = keys(%node_hash_variable);
|
|
|
|
while (1) {
|
|
while ((scalar @nodeargs) and $http_requests_in_progress < $max_concur_session_allow) {
|
|
deal_with_rsp();
|
|
my $node = shift @nodeargs;
|
|
sendreq($node, $node_hash_variable{$node});
|
|
}
|
|
if ($async->empty) {
|
|
last;
|
|
}
|
|
deal_with_rsp();
|
|
}
|
|
return;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 init_async
|
|
|
|
Creates a new HTTP::Async object and sets it up.
|
|
Input:
|
|
%args: the hash stores params to create the HTTP::Async object
|
|
slots: maximum number of parallel requests to make
|
|
Usage example:
|
|
init_async(slots=><num>)
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub init_async {
|
|
my %args = @_;
|
|
eval { require HTTP::Async };
|
|
if ($@) {
|
|
return ("Can't find HTTP/Async.pm, please make sure the package have been installed");
|
|
}
|
|
my @user = getpwuid($>);
|
|
my $homedir = $user[7];
|
|
my $ssl_ca_file = $homedir . "/.xcat/ca.pem";
|
|
my $ssl_cert_file = $homedir . "/.xcat/client-cred.pem";
|
|
my $key_file = $homedir . "/.xcat/client-cred.pem";
|
|
$async = HTTP::Async->new(
|
|
slots => $args{slots},
|
|
ssl_options => {
|
|
SSL_verify_mode => SSL_VERIFY_PEER,
|
|
SSL_ca_file => $ssl_ca_file,
|
|
SSL_cert_file => $ssl_cert_file,
|
|
SSL_key_file => $key_file,
|
|
},
|
|
);
|
|
return undef;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 genreq
|
|
|
|
Generate the docker REST API http request
|
|
Input:
|
|
$node: the docker container name
|
|
$dockerhost: hash, keys: name, port, user, pw, user, pw
|
|
$method: GET, PUT, POST, DELETE
|
|
$api: the url of rest api
|
|
$content: an xml section which including the data to perform the rest api
|
|
Return:
|
|
The REST API http request
|
|
Usage example:
|
|
my $api = "/images/json";
|
|
my $method = "GET";
|
|
my %dockerhost = ( name => "bybc0604", port => "2375", );
|
|
my $request = genreq($node, \%dockerhost, $method,$api, "");
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
sub genreq {
|
|
my $node = shift;
|
|
my $dockerhost = shift;
|
|
my $method = shift;
|
|
my $api = shift;
|
|
my $content = shift;
|
|
|
|
if (!defined($content)) { $content = ""; }
|
|
my $header = HTTP::Headers->new('content-type' => 'application/json',
|
|
'Accept' => 'application/json',
|
|
|
|
#'Connection' => 'keep-alive',
|
|
'Host' => $dockerhost->{name} . ":" . $dockerhost->{port});
|
|
$header->authorization_basic($dockerhost->{user} . '@internal', $dockerhost->{pw});
|
|
|
|
my $ctlen = length($content);
|
|
$header->push_header('Content-Length' => $ctlen);
|
|
|
|
my $url = "https://" . $dockerhost->{name} . ":" . $dockerhost->{port} . $api;
|
|
my $request = HTTP::Request->new($method, $url, $header, $content);
|
|
$request->protocol('HTTP/1.1');
|
|
return $request;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 genreq_for_mkdocker
|
|
|
|
Generate HTTP request for mkdocker
|
|
|
|
Input: $node: The docker container name
|
|
$dockerhost: hash, keys: name, port, user, pw, user, pw, user, pw
|
|
$method: the http method to generate the http request
|
|
$api: the url to generate the http request
|
|
|
|
return: The http request;
|
|
|
|
Usage example:
|
|
my $res = genreq_for_mkdocker($node,\%dockerhost,'GET','/containers/$node/json');
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub genreq_for_mkdocker {
|
|
my ($node, $dockerhost, $method, $api) = @_;
|
|
my $dockerinfo = $node_hash_variable{$node};
|
|
my %info_hash = ();
|
|
if (defined($dockerinfo->{flag})) {
|
|
my $flag_hash = decode_json($dockerinfo->{flag});
|
|
%info_hash = %$flag_hash;
|
|
}
|
|
|
|
#$info_hash{name} = '/'.$node;
|
|
#$info_hash{Hostname} = '';
|
|
#$info_hash{Domainname} = '';
|
|
$info_hash{Image} = "$dockerinfo->{image}";
|
|
@{ $info_hash{Cmd} } = split /,/, $dockerinfo->{cmd};
|
|
$info_hash{Memory} = $dockerinfo->{mem};
|
|
$info_hash{MacAddress} = $dockerinfo->{mac};
|
|
$info_hash{CpusetCpus} = $dockerinfo->{cpus};
|
|
$info_hash{HostConfig}->{NetworkMode} = $dockerinfo->{nics};
|
|
$info_hash{NetworkDisabled} = JSON::false;
|
|
$info_hash{NetworkingConfig}->{EndpointsConfig}->{"$dockerinfo->{nics}"}->{IPAMConfig}->{IPv4Address} = $dockerinfo->{ip};
|
|
my $content = encode_json \%info_hash;
|
|
return genreq($node, $dockerhost, $method, $api, $content);
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 sendreq
|
|
|
|
Based on the method, url create a http request and send out on the given SSL connection
|
|
|
|
Input:
|
|
$node: the docker container name
|
|
$node_hash: the hash that store information for the $node
|
|
return: 0-undefine If no error
|
|
1-return generate http request failed;
|
|
2-return http request error message;
|
|
Usage example:
|
|
my $res = sendreq($node, $node_hash);
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub sendreq {
|
|
my ($node, $node_hash) = @_;
|
|
my $http_req = $node_hash->{genreq_ptr}->($node, $node_hash->{hostinfo}, $node_hash->{http_req_method}, $node_hash->{http_req_url});
|
|
|
|
# Need to Dumper to log file later
|
|
# print Dumper($http_req);
|
|
my $http_session_id = $async->add_with_opts($http_req, {});
|
|
$http_session_variable{$http_session_id} = $node;
|
|
$http_requests_in_progress++;
|
|
return undef;
|
|
}
|
|
1;
|