mirror of
https://github.com/xcat2/xcat-core.git
synced 2025-05-22 11:42:05 +00:00
1211 lines
40 KiB
Perl
Executable File
1211 lines
40 KiB
Perl
Executable File
# 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';
|
|
}
|
|
use lib "$::XCATROOT/lib/perl";
|
|
|
|
use strict;
|
|
use POSIX qw(WNOHANG nice);
|
|
use POSIX qw(WNOHANG setsid :errno_h);
|
|
use Errno;
|
|
use IO::Select;
|
|
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 Getopt::Long;
|
|
Getopt::Long::Configure("bundling");
|
|
use HTTP::Headers;
|
|
use HTTP::Request;
|
|
use HTTP::Response;
|
|
use xCAT::Utils;
|
|
use xCAT::MsgUtils;
|
|
use Getopt::Long;
|
|
use File::Basename;
|
|
use Cwd;
|
|
use xCAT::Usage;
|
|
use JSON;
|
|
#use Data::Dumper;
|
|
|
|
my $verbose;
|
|
my $global_callback;
|
|
|
|
my $select = IO::Select->new();
|
|
|
|
#-------------------------------------------------------
|
|
=head3 The hash variable to store node related SSL connection and state information
|
|
|
|
The structure is like this
|
|
%node_hash_variable = (
|
|
$SSL_connection => {
|
|
node => $node,
|
|
state => $current_state,
|
|
state_machine_engine => $state_machine_for_the_node,
|
|
total_len => $total_len,
|
|
get_len => $get_len,
|
|
data_buf => $data,
|
|
},
|
|
);
|
|
|
|
=cut
|
|
#-------------------------------------------------------
|
|
|
|
my %node_hash_variable = ();
|
|
|
|
#-------------------------------------------------------
|
|
=head3 The hash variable to store node parameters to create docker container
|
|
|
|
The structure is like this
|
|
%node_create_variable = (
|
|
$node => {
|
|
image=>$nodetype.provmethod,
|
|
cmd=>$nodetype.provmethod,
|
|
ip=>$host.ip,
|
|
mac=>$mac.mac,
|
|
cpu=>$vm.cpus
|
|
memory=>$vm.memory
|
|
flag=>$vm.othersettings
|
|
},
|
|
);
|
|
|
|
=cut
|
|
#-------------------------------------------------------
|
|
|
|
my %node_create_variable = ();
|
|
|
|
# The counter to record how many request have been send and responses are expected.
|
|
my $pending_res = 0;
|
|
|
|
# The counter to record concurrent openting SSL connection numbers
|
|
my $concurrent_ssl_sessions = 0;
|
|
|
|
# The function point used for mkdocker to generate http request, for other cmd it will point to &genreq;
|
|
my $genreq_ptr = \&genreq;
|
|
|
|
# The vairables below are used to update attributes
|
|
my $vmtab; # vm.othersettings
|
|
my $nodelisttab; # nodelist.status
|
|
my $nodetypetab; #nodetype.provmethod
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=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',
|
|
} );
|
|
}
|
|
|
|
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=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 = (
|
|
rpower => {
|
|
start => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/start",
|
|
init_state => "INIT_TO_WAIT_FOR_START_DONE",
|
|
},
|
|
stop => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/stop",
|
|
init_state => "INIT_TO_WAIT_FOR_STOP_DONE",
|
|
},
|
|
restart => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/restart",
|
|
},
|
|
pause => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/pause",
|
|
},
|
|
unpause => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/#NODE#/unpause",
|
|
},
|
|
state => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "GET",
|
|
init_url => "/containers/#NODE#/json",
|
|
init_state => "INIT_TO_WAIT_FOR_QUERY_STATE_DONE",
|
|
},
|
|
},
|
|
mkdocker => {
|
|
all => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "POST",
|
|
init_url => "/containers/create?name=#NODE#",
|
|
init_state => "INIT_TO_WAIT_FOR_CREATE_DONE"
|
|
},
|
|
},
|
|
rmdocker => {
|
|
force => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "DELETE",
|
|
init_url => "/containers/#NODE#?force=1",
|
|
},
|
|
all => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "DELETE",
|
|
init_url => "/containers/#NODE#",
|
|
},
|
|
},
|
|
lsdocker => {
|
|
all => {
|
|
state_machine_engine => \&single_state_engine,
|
|
init_method => "GET",
|
|
init_url => "/containers/#NODE#/json?",
|
|
init_state => "INIT_TO_WAIT_FOR_QUERY_DOCKER_DONE",
|
|
},
|
|
log => {
|
|
state_machine_engine => \&single_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 stoped"
|
|
=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 stoped"];
|
|
}
|
|
}
|
|
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 single_state_engine
|
|
|
|
The state_machine_engine to deal with http response
|
|
Input:
|
|
$sockfd: The SSL connection from which the http response is returned
|
|
$data: The http response
|
|
Return:
|
|
If there are any errors or msg, they will be outputed directly.
|
|
Else, nothing returned.
|
|
Usage example:
|
|
single_state_engine($sockfd, HTTP Response data);
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub single_state_engine {
|
|
my $sockfd = shift;
|
|
my $data = shift;
|
|
if (!defined $node_hash_variable{$sockfd}) {
|
|
return;
|
|
}
|
|
my $info_flag = 'data';
|
|
my $get_another_pkg = 0;
|
|
my $node = $node_hash_variable{$sockfd}->{node};
|
|
my $curr_state = $node_hash_variable{$sockfd}->{state};
|
|
my $data_buf = $node_hash_variable{$sockfd}->{data_buf};
|
|
my $data_total_len = $node_hash_variable{$sockfd}->{total_len};
|
|
my $data_get_len = $node_hash_variable{$sockfd}->{get_len};
|
|
my $data_chunked = $node_hash_variable{$sockfd}->{chunked};
|
|
my @chunked_array = ();
|
|
# The code logic to deal with http response and state machine
|
|
#Need to Dumper to log file later
|
|
my $res = HTTP::Response->parse($data);
|
|
#print Dumper($res);
|
|
my $content = undef;
|
|
# Deal with the scenario that a http response is splited into multiple pkgs
|
|
unless ($res->code and $res->code =~ /\d{3}/) {
|
|
my $len = length($data);
|
|
if (defined($data_chunked)) {
|
|
$content = $data;
|
|
$res = HTTP::Response->parse($data_buf);
|
|
}
|
|
elsif (!defined($data_buf) or !defined($data_total_len) or !defined($data_get_len) or ($data_get_len + $len > $data_total_len)) {
|
|
$global_callback->({node=>[{name=>[$node],error=>["Incorrect data received"],errorcode=>[1]}]});
|
|
$concurrent_ssl_sessions--;
|
|
$select->remove($sockfd);
|
|
close($sockfd);
|
|
delete($node_hash_variable{$sockfd});
|
|
return;
|
|
}
|
|
else {
|
|
my $len = length($data);
|
|
if ($data_get_len + $len < $data_total_len) {
|
|
$node_hash_variable{$sockfd}->{get_len} += $len;
|
|
$node_hash_variable{$sockfd}->{data_buf} .= $data;
|
|
$pending_res++;
|
|
return;
|
|
}
|
|
else { # Exactly all the data are received
|
|
$res = HTTP::Response->parse($data_buf.$data);
|
|
delete $node_hash_variable{$sockfd}->{data_buf};
|
|
delete $node_hash_variable{$sockfd}->{total_len};
|
|
delete $node_hash_variable{$sockfd}->{get_len};
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!defined($content) and $res->content()) {
|
|
$content = $res->content();
|
|
}
|
|
my $get_content_len = length($content);
|
|
my $content_length = $res->header('content-length');
|
|
if (defined($content_length) and $get_content_len < $content_length) {
|
|
$node_hash_variable{$sockfd}->{data_buf} = $data;
|
|
$node_hash_variable{$sockfd}->{total_len} = $content_length;
|
|
$node_hash_variable{$sockfd}->{get_len} = $get_content_len;
|
|
$pending_res++;
|
|
return;
|
|
}
|
|
|
|
my $encoding_flag = $res->header('transfer-encoding');
|
|
if (defined($encoding_flag) and $encoding_flag eq 'chunked') {
|
|
$node_hash_variable{$sockfd}->{chunked} = 1;
|
|
$data_chunked = 1;
|
|
if ($get_content_len < 3) {
|
|
$node_hash_variable{$sockfd}->{data_buf} = $data;
|
|
$pending_res++;
|
|
return;
|
|
}
|
|
}
|
|
if (defined($data_chunked)) {
|
|
while (length($content)) {
|
|
my $split_pos = index($content, "\r\n");
|
|
my $length_string = substr($content, 0, $split_pos);
|
|
my $data_length = hex($length_string);
|
|
if ($data_length lt 2) {
|
|
if ($data_length eq 0) {
|
|
push @chunked_array, '0';
|
|
}
|
|
last;
|
|
}
|
|
push @chunked_array, $length_string;
|
|
push @chunked_array, substr($content, $split_pos + 2, $data_length);
|
|
$content = substr($content, $split_pos + 4 + $data_length);
|
|
}
|
|
}
|
|
my @msg = ();
|
|
$msg[0] = &http_state_code_info($res->code, $curr_state);
|
|
unless ($res->is_success) {
|
|
if ($content ne '') {
|
|
$msg[0]->[1] = "$content";
|
|
}
|
|
}
|
|
if ($curr_state eq "INIT_TO_WAIT_FOR_QUERY_STATE_DONE") {
|
|
if ($res->is_success) {
|
|
my $node_state = undef;
|
|
if ($data_chunked) {
|
|
my $length = shift @chunked_array;
|
|
while ($length) {
|
|
my $content_hash = decode_json (shift @chunked_array);
|
|
if (defined($content_hash->{'State'}->{'Status'})) {
|
|
$node_state = $content_hash->{'State'}->{'Status'};
|
|
last;
|
|
}
|
|
$length = shift @chunked_array;
|
|
}
|
|
if (!defined($node_state) and $length) {
|
|
$get_another_pkg = 1;
|
|
}
|
|
}
|
|
else {
|
|
my $content_hash = decode_json $content;
|
|
$node_state = $content_hash->{'State'}->{'Status'};
|
|
}
|
|
if (defined($node_state)) {
|
|
if ($nodelisttab) {
|
|
$nodelisttab->setNodeAttribs($node, {status=>$node_state});
|
|
}
|
|
$msg[0] = [0, $node_state];
|
|
}
|
|
elsif (!$get_another_pkg) {
|
|
$msg[0] = [1, "Can not get status"];
|
|
}
|
|
}
|
|
elsif ($res->code eq '404') {
|
|
if ($nodelisttab) {
|
|
$nodelisttab->setNodeAttribs($node, {status=>''});
|
|
}
|
|
}
|
|
}
|
|
elsif ($curr_state eq "INIT_TO_WAIT_FOR_QUERY_LOG_DONE") {
|
|
if (!$msg[0]->[0]) {
|
|
$info_flag = "base64_data";
|
|
@msg = ();
|
|
if (defined($data_chunked)) {
|
|
my @data_array = ();
|
|
my $tmp_len = shift(@chunked_array);
|
|
while ($tmp_len and scalar(@chunked_array)) {
|
|
push @data_array, shift(@chunked_array);
|
|
$tmp_len = shift(@chunked_array);
|
|
}
|
|
if ($tmp_len ne 0) {
|
|
$get_another_pkg = 1;
|
|
}
|
|
if (scalar(@data_array)) {
|
|
my $string = join('', @data_array);
|
|
$msg[0] = [0, encode_base64($string)];
|
|
}
|
|
else {
|
|
$msg[0] = [0, encode_base64("No logs")];
|
|
}
|
|
}
|
|
else {
|
|
$msg[0] = [0, encode_base64($content)];
|
|
}
|
|
}
|
|
}
|
|
elsif ($curr_state eq "INIT_TO_WAIT_FOR_QUERY_DOCKER_DONE") {
|
|
if ($res->is_success) {
|
|
@msg = ();
|
|
if (!defined($content_length) or ($content_length > 3)) {
|
|
if (defined($data_chunked)) {
|
|
my $tmp_entry = shift @chunked_array;
|
|
while ($tmp_entry and scalar(@chunked_array)) {
|
|
my $content_hash = decode_json (shift @chunked_array);
|
|
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)];
|
|
}
|
|
$tmp_entry = shift @chunked_array;
|
|
}
|
|
if ($tmp_entry ne '0') {
|
|
$get_another_pkg = 1;
|
|
}
|
|
}
|
|
else {
|
|
my $content_hash = decode_json $content;
|
|
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)];
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
@msg = [0, "No running docker"];
|
|
}
|
|
}
|
|
}
|
|
elsif ($curr_state eq 'INIT_TO_WAIT_FOR_CREATE_DONE') {
|
|
if ($nodetypetab) {
|
|
$nodetypetab->setNodeAttribs($node,{provmethod=>"$node_create_variable{$node}->{image}:$node_create_variable{$node}->{cmd}"});
|
|
}
|
|
if ($vmtab) {
|
|
$vmtab->setNodeAttribs($node,{othersettings=>$node_create_variable{$node}->{flag}});
|
|
}
|
|
}
|
|
|
|
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]"]}]});
|
|
}
|
|
}
|
|
if ($get_another_pkg) {
|
|
$pending_res++;
|
|
return;
|
|
}
|
|
$concurrent_ssl_sessions--;
|
|
$select->remove($sockfd);
|
|
close($sockfd);
|
|
delete($node_hash_variable{$sockfd});
|
|
return;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=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\t$image\t$command\t$created\t$status\t$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 = substr($docker_info_hash->{'Image'}, 0, 20);
|
|
$command = $docker_info_hash->{'Command'};
|
|
$created = $docker_info_hash->{'Created'};
|
|
$status = $docker_info_hash->{'Status'};
|
|
|
|
$command = substr($command,0, 20);
|
|
$names = join(',',@{$docker_info_hash->{'Names'}});
|
|
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'};
|
|
$command = join(',', @{$docker_info_hash->{Config}->{'Cmd'}});
|
|
$names = $docker_info_hash->{'Name'};
|
|
$created = $docker_info_hash->{'Created'};
|
|
$status = $docker_info_hash->{'State'}->{'Status'};
|
|
$created =~ s/\..*$//;
|
|
}
|
|
return("$id\t$image\t$command\t$created\t$status\t$names");
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=head3 deal_with_rsp
|
|
|
|
The function to deal with SELECT
|
|
Input:
|
|
%args: a hash which currently only key 'timeout' is using
|
|
Return:
|
|
The expected number of response which havn't been received
|
|
Usage example:
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub deal_with_rsp
|
|
{
|
|
my %args = @_;
|
|
my $timeout = 0;
|
|
if (defined($args{timeout})) {
|
|
$timeout = $args{timeout};
|
|
}
|
|
my @data = ();
|
|
if ($select->can_read($timeout)) {
|
|
my @ready_fds = $select->can_read(0);
|
|
foreach my $sockfd (@ready_fds) {
|
|
my $res = "";
|
|
my $node_hash = $node_hash_variable{$sockfd};
|
|
if (defined($node_hash)) {
|
|
while (1) {
|
|
my $readbytes = undef;
|
|
$readbytes = sysread($sockfd, $res, 65535, length($res));
|
|
if (!defined($readbytes)) {
|
|
if ($!{EAGAIN} or $!{EWOULDBLOCK}) {
|
|
$pending_res--;
|
|
last;
|
|
}
|
|
elsif ($!{EINTR} or $!{ENOTTY}) {
|
|
next;
|
|
}
|
|
else {
|
|
die "read failed: $!";
|
|
}
|
|
}
|
|
elsif ($readbytes == 0) {
|
|
$pending_res--;
|
|
last;
|
|
}
|
|
}
|
|
# readbytes UNDEF means a reading error, so print out a msg and parse the next SSL connection
|
|
push @data, [$node_hash->{state_machine_engine}, $sockfd, $res];
|
|
}
|
|
}
|
|
}
|
|
foreach (@data) {
|
|
$_->[0]->($_->[1], $_->[2]);
|
|
}
|
|
return $pending_res;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=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 )) {
|
|
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 {
|
|
@ARGV = ();
|
|
}
|
|
}
|
|
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->{arg}->[0] = "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->{arg}->[0] = "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;
|
|
|
|
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 $init_method = undef;
|
|
my $init_url = undef;
|
|
my $init_state = undef;
|
|
my $state_machine_engine = undef;
|
|
my @nodeargs = ();
|
|
my @errornodes = ();
|
|
my $mapping_hash = undef;
|
|
my $max_concur_ssl_session_allow = 10; # A variable can be set by caculated in the future
|
|
$mapping_hash = $command_states{$command}{$args->[0]};
|
|
unless($mapping_hash) {
|
|
$mapping_hash = $command_states{$command}{all};
|
|
}
|
|
unless ($mapping_hash) {
|
|
my $option = '';
|
|
if (defined($args->[0])) {
|
|
$option = $args->[0];
|
|
}
|
|
$callback->({error=>["Not support $command $option"], errorcode=>1});
|
|
return;
|
|
}
|
|
$init_method = $mapping_hash->{init_method};
|
|
$init_url = $mapping_hash->{init_url};
|
|
$state_machine_engine = $mapping_hash->{state_machine_engine};
|
|
$init_state = $mapping_hash->{init_state};
|
|
if (!defined($init_state)) {
|
|
$init_state = "INIT_TO_WAIT_FOR_RSP";
|
|
}
|
|
if ($command eq 'rpower' and defined($args->[0]) and ($args->[0] eq 'state')) {
|
|
$nodelisttab = xCAT::Table->new('nodelist');
|
|
}
|
|
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} eq 'ipmi') {
|
|
|
|
if (defined($args) and $args->[0] ne '') {
|
|
$callback->({error=>[" -l|--log is not support for $node"], errorcode=>1});
|
|
return;
|
|
}
|
|
my $node_init_url = $init_url;
|
|
$node_init_url =~ s/#NODE#\///;
|
|
push @nodeargs, [$node, {name=>$node,port=>'2375'}, $init_method, $node_init_url, $init_state, $state_machine_engine];
|
|
}
|
|
else {
|
|
push @new_noderange, $node;
|
|
}
|
|
}
|
|
}
|
|
$noderange = \@new_noderange;
|
|
}
|
|
|
|
# The dockerhost is mapped to vm.host, so open vm table here
|
|
$vmtab = xCAT::Table->new('vm');
|
|
if ($vmtab) {
|
|
my $vmhashs = $vmtab->getNodesAttribs($noderange, ['host']);
|
|
if ($vmhashs) {
|
|
foreach my $node (@$noderange) {
|
|
my $vmhash = $vmhashs->{$node}->[0];
|
|
if (!defined($vmhash) or !defined($vmhash->{host})) {
|
|
push @errornodes, $node;
|
|
next;
|
|
}
|
|
my ($host, $port) = split /:/,$vmhash->{host};
|
|
if (!defined($host)) {
|
|
push @errornodes, $node;
|
|
next;
|
|
}
|
|
if (!defined($port)) {
|
|
$port = 2375;
|
|
}
|
|
my $node_init_url = $init_url;
|
|
$node_init_url =~ s/#NODE#/$node/;
|
|
push @nodeargs, [$node, {name=>$host,port=>$port}, $init_method, $node_init_url, $init_state, $state_machine_engine];
|
|
}
|
|
}
|
|
}
|
|
#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;
|
|
}
|
|
}
|
|
$genreq_ptr = \&genreq_for_mkdocker;
|
|
$nodetypetab = xCAT::Table->new('nodetype');
|
|
my $hosttab = xCAT::Table->new('hosts');
|
|
my $mactab = xCAT::Table->new('mac');
|
|
if (!defined($hosttab) or !defined($nodetypetab) or !defined($mactab) or !defined($vmtab)) {
|
|
$callback->({error=>["Open table 'nodetype', 'hosts' or 'mac' failed"], errorcode=>1});
|
|
return;
|
|
}
|
|
my $nodetypehash = $nodetypetab->getNodesAttribs($noderange, ['provmethod']);
|
|
my $hosthash = $hosttab->getNodesAttribs($noderange, ['ip']);
|
|
my $machash = $mactab->getNodesAttribs($noderange, ['mac']);
|
|
my $vmhash = $vmtab->getNodesAttribs($noderange, ['cpus', 'memory', 'othersettings']);
|
|
my @errornodes = ();
|
|
foreach my $node (@$noderange) {
|
|
if ($imagearg) {
|
|
$node_create_variable{$node}->{image} = $imagearg;
|
|
if ($cmdarg) {
|
|
$node_create_variable{$node}->{cmd} = $cmdarg;
|
|
}
|
|
}
|
|
else {
|
|
if (!defined($nodetypehash->{$node}->[0]->{provmethod})) {
|
|
push @errornodes, $node;
|
|
next;
|
|
}
|
|
else {
|
|
my ($tmp_img,$tmp_cmd) = split /:/, $nodetypehash->{$node}->[0]->{provmethod};
|
|
if (!defined($tmp_img)) {
|
|
push @errornodes, $node;
|
|
next;
|
|
}
|
|
$node_create_variable{$node}->{image} = $tmp_img;
|
|
$node_create_variable{$node}->{cmd} = $tmp_cmd;
|
|
}
|
|
}
|
|
if ($flagarg) {
|
|
$node_create_variable{$node}->{flag} = $flagarg;
|
|
}
|
|
if (defined($hosthash->{$node}->[0]->{ip})) {
|
|
$node_create_variable{$node}->{ip} = $hosthash->{$node}->[0]->{ip};
|
|
}
|
|
if (defined($machash->{$node}->[0]->{mac})) {
|
|
$node_create_variable{$node}->{mac} = $machash->{$node}->[0]->{mac};
|
|
}
|
|
my $vmnodehash = $vmhash->{$node}->[0];
|
|
if (defined($vmnodehash)) {
|
|
if (defined($vmnodehash->{cpus})) {
|
|
$node_create_variable{$node}->{cpus} = $vmnodehash->{cpus};
|
|
}
|
|
if (defined($vmnodehash->{memory})) {
|
|
$node_create_variable{$node}->{memory} = $vmnodehash->{memory};
|
|
}
|
|
if (!defined($flagarg) and defined($vmnodehash->{othersettings})) {
|
|
$node_create_variable{$node}->{flag} = $vmnodehash->{othersettings};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (scalar(@errornodes)) {
|
|
$callback->({error=>["Docker host not set correct for @errornodes"], errorcode=>1});
|
|
return;
|
|
}
|
|
my $timeout = 0;
|
|
my $pre_pending_res = undef;
|
|
my $no_res_times = 0;
|
|
while (1) {
|
|
my $pending_nodes = scalar(@nodeargs);
|
|
if ($pending_nodes eq 0) {
|
|
if ($pending_res eq 0) { # No more nodes needed to be process, no more response is expected, end the loop
|
|
last;
|
|
}
|
|
# The steps below is used to judge whether there is no response
|
|
# In the 1st round, just record the pending response num
|
|
# Then, check whether the pending num have changed.
|
|
# If NO changes, increase NO-change times counter and waiting time
|
|
# If changed, clear counter, waiting time
|
|
elsif (!defined($pre_pending_res)) {
|
|
$pre_pending_res = $pending_res;
|
|
}
|
|
elsif ($pre_pending_res eq $pending_res) {
|
|
$no_res_times++;
|
|
$timeout += $pending_res;
|
|
}
|
|
else {
|
|
$pre_pending_res = undef;
|
|
$no_res_times = 0;
|
|
$timeout = 0;
|
|
}
|
|
# Wait for 10 * num_of_sessions
|
|
if ($no_res_times > 5) {
|
|
last;
|
|
}
|
|
}
|
|
|
|
|
|
if (($pending_nodes eq 0) and ($pending_res eq 0)) { # No more nodes needed to be process, no more response is expected, end the loop
|
|
last;
|
|
}
|
|
if (($pending_nodes) and ($concurrent_ssl_sessions lt $max_concur_ssl_session_allow)) {
|
|
my $node = shift @nodeargs;
|
|
my $ssl_connect = init_ssl_connection($node->[1]);
|
|
if (!defined($ssl_connect)) {
|
|
$callback->({error=>["Create SSL connection failed for docker $node->[0] on host $node->[1]->{host}"], errorcode=>1});
|
|
}
|
|
elsif (not ref($ssl_connect)) {
|
|
$callback->({error=>["$ssl_connect"], errorcode=>1});
|
|
}
|
|
else {
|
|
my $res = sendreq($ssl_connect, @$node);
|
|
if (defined($res)) {
|
|
$callback->({node=>[{name=>[$node->[0]], error=>[$res], errorcode=>[1]}]});
|
|
close($ssl_connect);
|
|
$concurrent_ssl_sessions--;
|
|
}
|
|
}
|
|
}
|
|
deal_with_rsp(timeout=>$timeout);
|
|
}
|
|
my @failed_handler_array = $select->handles;
|
|
if (scalar(@failed_handler_array)) {
|
|
my @err_msg = ();
|
|
foreach my $sockfd (@failed_handler_array) {
|
|
if (defined($node_hash_variable{$sockfd})) {
|
|
push @err_msg, {name=>[$node_hash_variable{$sockfd}->{node}], error=>["Timeout to wait for response"], errorcode=>[1]};
|
|
}
|
|
}
|
|
$callback->({node=>\@err_msg});
|
|
}
|
|
if ($nodelisttab) { $nodelisttab->commit;}
|
|
if ($nodetypetab) { $nodetypetab->commit;}
|
|
if ($vmtab) {$vmtab->commit;}
|
|
return;
|
|
}
|
|
|
|
#-------------------------------------------------------
|
|
|
|
=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: 1-No image defined;
|
|
2-http response error;
|
|
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_create_variable{$node};
|
|
if (!defined($dockerinfo) or !defined($dockerinfo->{image})) {
|
|
return "No image defined";
|
|
}
|
|
my %info_hash = ();
|
|
#$info_hash{name} = '/'.$node;
|
|
#$info_hash{Hostname} = '';
|
|
#$info_hash{Domainname} = '';
|
|
$info_hash{Image} = "$dockerinfo->{image}";
|
|
$info_hash{Cmd} = "$dockerinfo->{cmd}";
|
|
$info_hash{Memory} = $dockerinfo->{mem};
|
|
$info_hash{MacAddress} = $dockerinfo->{mac};
|
|
$info_hash{CpusetCpus} = $dockerinfo->{cpus};
|
|
if (defined($dockerinfo->{flag})) {
|
|
my $flag_hash = decode_json($dockerinfo->{flag});
|
|
%info_hash = (%info_hash, %$flag_hash);
|
|
}
|
|
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: $ssl_connection: the SSL connection for this request
|
|
$node: the docker container name
|
|
$dockerhost: hash, keys: name, port, user, pw, user, pw
|
|
$method: the http method to generate a http request
|
|
$url: the http url to generate a http request
|
|
$state: the state for the action
|
|
$state_machine_engine: the function to deal with the http response for the request generate by $method and $url
|
|
|
|
return: 0-undefine If no error
|
|
1-return generate http request failed;
|
|
2-return http request error message;
|
|
Usage example:
|
|
my $res = send_req($ssl_connetion, $node, \%dockerhost, 'GET', '/containers/$node/json', "INIT_TO_WAIT_FOR_RSP", \&single_state_engine);
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub sendreq {
|
|
my ($ssl_connection, $node, $dockerhost, $init_method, $init_url, $init_state, $state_machine_engine) = @_;
|
|
my $http_req = $genreq_ptr->($node, $dockerhost, $init_method, $init_url);
|
|
# Need to Dumper to log file later
|
|
#print Dumper($http_req);
|
|
if (!defined($http_req)) {
|
|
return "Generate http request failed";
|
|
}
|
|
elsif (not ref($http_req)) {
|
|
return $http_req;
|
|
}
|
|
$select->add($ssl_connection);
|
|
print $ssl_connection $http_req->as_string();
|
|
$node_hash_variable{$ssl_connection}->{node} = $node;
|
|
$node_hash_variable{$ssl_connection}->{state} = $init_state;
|
|
$node_hash_variable{$ssl_connection}->{state_machine_engine} = $state_machine_engine;
|
|
$pending_res++;
|
|
return undef;
|
|
}
|
|
#-------------------------------------------------------
|
|
|
|
=head3 init_ssl_connection
|
|
|
|
This function is used to create a SSL connection to the docker host
|
|
|
|
Input: $dockerhost: hash, keys: name, port, user, pw, user, pw
|
|
|
|
return: A SSL connection handler if success.
|
|
An error msg if failed.
|
|
Usage example:
|
|
my $ssl_connect = init_ssl_connection(\%dockerhost);
|
|
|
|
=cut
|
|
|
|
#-------------------------------------------------------
|
|
|
|
sub init_ssl_connection {
|
|
my $dockerhost = shift;
|
|
my $hostname = $dockerhost->{name};
|
|
my $port = $dockerhost->{port};
|
|
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";
|
|
my $rc = 0;
|
|
my $response;
|
|
my $connect;
|
|
my $socket = IO::Socket::INET->new( PeerHost => $hostname,
|
|
PeerPort => $port,
|
|
Timeout => 2);
|
|
if ($socket) {
|
|
$connect = IO::Socket::SSL->start_SSL( $socket,
|
|
SSL_verify_mode => "SSL_VERIFY_PEER",
|
|
SSL_ca_file => $ssl_ca_file,
|
|
SSL_cert_file =>$ssl_cert_file,
|
|
SSL_key_file => $key_file,
|
|
Timeout => 2
|
|
);
|
|
if ($connect) {
|
|
my $flags=fcntl($connect,F_GETFL,0);
|
|
$flags |= O_NONBLOCK;
|
|
fcntl($connect,F_SETFL,$flags);
|
|
} else {
|
|
$rc = 1;
|
|
$response = "Could not make ssl connection to $hostname:$port.";
|
|
}
|
|
} else {
|
|
$rc = 1;
|
|
$response = "Could not create socket to $hostname:$port.";
|
|
}
|
|
|
|
if ($rc) {
|
|
return $response;
|
|
} else {
|
|
$concurrent_ssl_sessions++;
|
|
return $connect;
|
|
}
|
|
}
|
|
|
|
1;
|
|
|