use strict;
use warnings;
use Data::Dumper;
+use Term::ReadLine;
+use POSIX qw(strftime);
+use JSON;
use PVE::SafeSyslog;
use PVE::Tools qw(extract_param);
use PVE::INotify;
use PVE::CLIHandler;
+use PMG::Utils;
+use PMG::Ticket;
+use PMG::RESTEnvironment;
use PMG::DBTools;
+use PMG::RuleDB;
+use PMG::RuleCache;
use PMG::Cluster;
use PMG::ClusterConfig;
use PMG::API2::Cluster;
use base qw(PVE::CLIHandler);
+sub setup_environment {
+ PMG::RESTEnvironment->setup_default_cli_env();
+
+ my $rpcenv = PMG::RESTEnvironment->get();
+ # API /config/cluster/nodes need a ticket to connect to other nodes
+ my $ticket = PMG::Ticket::assemble_ticket('root@pam');
+ $rpcenv->set_ticket($ticket);
+}
+
+my $upid_exit = sub {
+ my $upid = shift;
+ my $status = PVE::Tools::upid_read_status($upid);
+ exit($status eq 'OK' ? 0 : -1);
+};
+
my $format_nodelist = sub {
my $res = shift;
print "NAME(CID)--------------IPADDRESS----ROLE-STATE---------UPTIME---LOAD----MEM---DISK\n";
foreach my $ni (@$res) {
- my $state = '?';
+ my $state = 'A';
+ $state = 'S' if !$ni->{insync};
+
+ if (my $err = $ni->{conn_error}) {
+ $err =~ s/\n/ /g;
+ $state = "ERROR: $err";
+ }
+
+ my $uptime = $ni->{uptime} ? PMG::Utils::format_uptime($ni->{uptime}) : '-';
+
+ my $loadavg1 = '-';
+ if (my $d = $ni->{loadavg}) {
+ $loadavg1 = $d->[0];
+ }
+
+ my $mem = '-';
+ if (my $d = $ni->{memory}) {
+ $mem = int(0.5 + ($d->{used}*100/$d->{total}));
+ }
+ my $disk = '-';
+ if (my $d = $ni->{rootfs}) {
+ $disk = int(0.5 + ($d->{used}*100/$d->{total}));
+ }
printf "%-20s %-15s %-6s %1s %15s %6s %5s%% %5s%%\n",
"$ni->{name}($ni->{cid})", $ni->{ip}, $ni->{type},
- $state, '-', '-', '-', '-';
+ $state, $uptime, $loadavg1, $mem, $disk;
}
};
name => 'join_cmd',
path => 'join_cmd',
method => 'GET',
- description => "Print information about cluster master node.",
+ description => "Prints the command for joining an new node to the cluster. You need to execute the command on the new node.",
parameters => {
additionalProperties => 0,
properties => {},
code => sub {
my ($param) = @_;
- my $cfg = PVE::INotify::read_file('cluster.conf');
+ my $cinfo = PMG::ClusterConfig->new();
- if (scalar(keys %{$cfg->{ids}})) {
+ if (scalar(keys %{$cinfo->{ids}})) {
- my $master = $cfg->{master} ||
+ my $master = $cinfo->{master} ||
die "no master found\n";
- print "pmgcm join --master_ip $master->{ip} --fingerprint $master->{fingerprint}\n";
+ print "pmgcm join $master->{ip} --fingerprint $master->{fingerprint}\n";
} else {
die "no cluster defined\n";
return undef;
}});
+__PACKAGE__->register_method({
+ name => 'delete',
+ path => 'delete',
+ method => 'GET',
+ description => "Remove a node from the cluster.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ cid => {
+ description => "Cluster Node ID.",
+ type => 'integer',
+ minimum => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $code = sub {
+ my $cinfo = PMG::ClusterConfig->new();
+
+ die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}});
+
+ my $master = $cinfo->{master} || die "unable to lookup master node\n";
+
+ die "operation not permitted (not master)\n"
+ if $cinfo->{local}->{cid} != $master->{cid};
+
+ my $cid = $param->{cid};
+
+ die "unable to delete master node\n"
+ if $cinfo->{local}->{cid} == $cid;
+
+ die "no such node (cid == $cid does not exists)\n" if !$cinfo->{ids}->{$cid};
+
+ delete $cinfo->{ids}->{$cid};
+
+ $cinfo->write();
+ };
+
+ PMG::ClusterConfig::lock_config($code, "delete cluster node failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'join',
+ path => 'join',
+ method => 'GET',
+ description => "Join a new node to an existing cluster.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ master_ip => {
+ description => "IP address.",
+ type => 'string', format => 'ip',
+ },
+ fingerprint => {
+ description => "SSL certificate fingerprint.",
+ type => 'string',
+ pattern => '^(:?[A-Z0-9][A-Z0-9]:){31}[A-Z0-9][A-Z0-9]$',
+ optional => 1,
+ },
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $code = sub {
+ my $cinfo = PMG::ClusterConfig->new();
+
+ die "cluster already defined\n" if scalar(keys %{$cinfo->{ids}});
+
+ my $term = new Term::ReadLine ('pmgcm');
+ my $attribs = $term->Attribs;
+ $attribs->{redisplay_function} = $attribs->{shadow_redisplay};
+ my $password = $term->readline('Enter password: ');
+
+ my $setup = {
+ username => 'root@pam',
+ password => $password,
+ cookie_name => 'PMGAuthCookie',
+ host => $param->{master_ip},
+ };
+ if ($param->{fingerprint}) {
+ $setup->{cached_fingerprints} = {
+ $param->{fingerprint} => 1,
+ };
+ } else {
+ # allow manual fingerprint verification
+ $setup->{manual_verification} = 1;
+ }
+
+ PMG::API2::Cluster::cluster_join($cinfo, $setup);
+ };
+
+ PMG::ClusterConfig::lock_config($code, "cluster join failed");
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'sync',
+ path => 'sync',
+ method => 'GET',
+ description => "Synchronize cluster configuration.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ master_ip => {
+ description => 'Optional IP address for master node.',
+ type => 'string', format => 'ip',
+ optional => 1,
+ }
+ },
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $cinfo = PMG::ClusterConfig->new();
+
+ my $master_name = undef;
+ my $master_ip = $param->{master_ip};
+
+ if (!$master_ip && $cinfo->{master}) {
+ $master_ip = $cinfo->{master}->{ip};
+ $master_name = $cinfo->{master}->{name};
+ }
+
+ die "no master IP specified (use option --master_ip)\n" if !$master_ip;
+
+ if ($cinfo->{local}->{ip} eq $master_ip) {
+ print STDERR "local node is master - nothing to do\n";
+ return undef;
+ }
+
+ print STDERR "syncing master configuration from '${master_ip}'\n";
+
+ PMG::Cluster::sync_config_from_master($master_name, $master_ip);
+
+ my $cfg = PMG::Config->new();
+
+ $cfg->rewrite_config(undef, 1);
+
+ return undef;
+ }});
+
+__PACKAGE__->register_method({
+ name => 'promote',
+ path => 'promote',
+ method => 'POST',
+ description => "Promote current node to become the new master.",
+ parameters => {
+ additionalProperties => 0,
+ properties => {},
+ },
+ returns => { type => 'null' },
+ code => sub {
+ my ($param) = @_;
+
+ my $code = sub {
+ my $cinfo = PMG::ClusterConfig->new();
+
+ die "no cluster defined\n" if !scalar(keys %{$cinfo->{ids}});
+
+ my $master = $cinfo->{master} || die "unable to lookup master node\n";
+
+ die "this node is already master\n"
+ if $cinfo->{local}->{cid} == $master->{cid};
+
+ my $maxcid = $master->{maxcid};
+ $master->{type} = 'node';
+
+ my $newmaster = $cinfo->{local};
+
+ $newmaster->{maxcid} = $maxcid;
+ $newmaster->{type} = 'master';
+
+ $cinfo->{master} = $newmaster;
+
+ $cinfo->write();
+ };
+
+ PMG::ClusterConfig::lock_config($code, "promote new master failed");
+
+ return undef;
+ }});
+
our $cmddef = {
- nodes => [ 'PMG::API2::Cluster', 'nodes', [], {}, $format_nodelist],
- create => [ 'PMG::API2::Cluster', 'create', []],
- join => [ 'PMG::API2::Cluster', 'join', ['master_ip', 'fingerprint']],
+ status => [ 'PMG::API2::Cluster', 'status', [], {}, $format_nodelist],
+ create => [ 'PMG::API2::Cluster', 'create', [], {}, $upid_exit],
+ delete => [ __PACKAGE__, 'delete', ['cid']],
+ join => [ __PACKAGE__, 'join', ['master_ip']],
join_cmd => [ __PACKAGE__, 'join_cmd', []],
+ sync => [ __PACKAGE__, 'sync', []],
+ promote => [ __PACKAGE__, 'promote', []],
};
1;