]> git.proxmox.com Git - pve-cluster.git/blob - data/PVE/CLI/pvecm.pm
41c2f9cc69d645b4634b49a9990815c20321d1d2
[pve-cluster.git] / data / PVE / CLI / pvecm.pm
1 package PVE::CLI::pvecm;
2
3 use strict;
4 use warnings;
5 use Getopt::Long;
6 use Socket;
7 use IO::File;
8 use IO::Socket::IP;
9 use POSIX;
10 use Net::IP;
11 use File::Path;
12 use File::Basename;
13 use PVE::Tools;
14 use PVE::Cluster;
15 use PVE::INotify;
16 use PVE::JSONSchema;
17 use PVE::CLIHandler;
18 use PVE::Corosync;
19
20 use base qw(PVE::CLIHandler);
21
22 $ENV{HOME} = '/root'; # for ssh-copy-id
23
24 my $basedir = "/etc/pve";
25 my $clusterconf = "$basedir/corosync.conf";
26 my $libdir = "/var/lib/pve-cluster";
27 my $backupdir = "/var/lib/pve-cluster/backup";
28 my $dbfile = "$libdir/config.db";
29 my $authfile = "/etc/corosync/authkey";
30
31 sub backup_database {
32
33 print "backup old database\n";
34
35 mkdir $backupdir;
36
37 my $ctime = time();
38 my $cmd = [
39 ['echo', '.dump'],
40 ['sqlite3', $dbfile],
41 ['gzip', '-', \ ">${backupdir}/config-${ctime}.sql.gz"],
42 ];
43
44 PVE::Tools::run_command($cmd, 'errmsg' => "cannot backup old database\n");
45
46 # purge older backup
47 my $maxfiles = 10;
48
49 my @bklist = ();
50 foreach my $fn (<$backupdir/config-*.sql.gz>) {
51 if ($fn =~ m!/config-(\d+)\.sql.gz$!) {
52 push @bklist, [$fn, $1];
53 }
54 }
55
56 @bklist = sort { $b->[1] <=> $a->[1] } @bklist;
57
58 while (scalar (@bklist) >= $maxfiles) {
59 my $d = pop @bklist;
60 print "delete old backup '$d->[0]'\n";
61 unlink $d->[0];
62 }
63 }
64
65 __PACKAGE__->register_method ({
66 name => 'keygen',
67 path => 'keygen',
68 method => 'PUT',
69 description => "Generate new cryptographic key for corosync.",
70 parameters => {
71 additionalProperties => 0,
72 properties => {
73 filename => {
74 type => 'string',
75 description => "Output file name"
76 }
77 },
78 },
79 returns => { type => 'null' },
80
81 code => sub {
82 my ($param) = @_;
83
84 my $filename = $param->{filename};
85
86 # test EUID
87 $> == 0 || die "Error: Authorization key must be generated as root user.\n";
88 my $dirname = dirname($filename);
89 my $basename = basename($filename);
90
91 die "key file '$filename' already exists\n" if -e $filename;
92
93 File::Path::make_path($dirname) if $dirname;
94
95 my $cmd = ['corosync-keygen', '-l', '-k', $filename];
96 PVE::Tools::run_command($cmd);
97
98 return undef;
99 }});
100
101 __PACKAGE__->register_method ({
102 name => 'create',
103 path => 'create',
104 method => 'PUT',
105 description => "Generate new cluster configuration.",
106 parameters => {
107 additionalProperties => 0,
108 properties => {
109 clustername => {
110 description => "The name of the cluster.",
111 type => 'string', format => 'pve-node',
112 maxLength => 15,
113 },
114 nodeid => {
115 type => 'integer',
116 description => "Node id for this node.",
117 minimum => 1,
118 optional => 1,
119 },
120 votes => {
121 type => 'integer',
122 description => "Number of votes for this node.",
123 minimum => 1,
124 optional => 1,
125 },
126 bindnet0_addr => {
127 type => 'string', format => 'ip',
128 description => "This specifies the network address the corosync ring 0".
129 " executive should bind to and defaults to the local IP address of the node.",
130 optional => 1,
131 },
132 ring0_addr => {
133 type => 'string', format => 'address',
134 description => "Hostname (or IP) of the corosync ring0 address of this node.".
135 " Defaults to the hostname of the node.",
136 optional => 1,
137 },
138 bindnet1_addr => {
139 type => 'string', format => 'ip',
140 description => "This specifies the network address the corosync ring 1".
141 " executive should bind to and is optional.",
142 optional => 1,
143 },
144 ring1_addr => {
145 type => 'string', format => 'address',
146 description => "Hostname (or IP) of the corosync ring1 address, this".
147 " needs an valid bindnet1_addr.",
148 optional => 1,
149 },
150 },
151 },
152 returns => { type => 'null' },
153
154 code => sub {
155 my ($param) = @_;
156
157 -f $clusterconf && die "cluster config '$clusterconf' already exists\n";
158
159 PVE::Cluster::setup_sshd_config(1);
160 PVE::Cluster::setup_rootsshconfig();
161 PVE::Cluster::setup_ssh_keys();
162
163 -f $authfile || __PACKAGE__->keygen({filename => $authfile});
164
165 -f $authfile || die "no authentication key available\n";
166
167 my $clustername = $param->{clustername};
168
169 $param->{nodeid} = 1 if !$param->{nodeid};
170
171 $param->{votes} = 1 if !defined($param->{votes});
172
173 my $nodename = PVE::INotify::nodename();
174
175 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
176
177 $param->{bindnet0_addr} = $local_ip_address
178 if !defined($param->{bindnet0_addr});
179
180 $param->{ring0_addr} = $nodename if !defined($param->{ring0_addr});
181
182 die "Param bindnet1_addr and ring1_addr are dependend, use both or none!\n"
183 if (defined($param->{bindnet1_addr}) != defined($param->{ring1_addr}));
184
185 my $bind_is_ipv6 = Net::IP::ip_is_ipv6($param->{bindnet0_addr});
186
187 # use string as here-doc format distracts more
188 my $interfaces = "interface {\n ringnumber: 0\n" .
189 " bindnetaddr: $param->{bindnet0_addr}\n }";
190
191 my $ring_addresses = "ring0_addr: $param->{ring0_addr}" ;
192
193 # allow use of multiple rings (rrp) at cluster creation time
194 if ($param->{bindnet1_addr}) {
195 die "IPv6 and IPv4 cannot be mixed, use one or the other!\n"
196 if Net::IP::ip_is_ipv6($param->{bindnet1_addr}) != $bind_is_ipv6;
197
198 $interfaces .= "\n interface {\n ringnumber: 1\n" .
199 " bindnetaddr: $param->{bindnet1_addr}\n }\n";
200
201 $interfaces .= "rrp_mode: passive\n"; # only passive is stable and tested
202
203 $ring_addresses .= "\n ring1_addr: $param->{ring1_addr}";
204
205 } elsif($param->{rrp_mode} && $param->{rrp_mode} ne 'none') {
206
207 warn "rrp_mode '$param->{rrp_mode}' useless when using only one".
208 " ring, using 'none' instead";
209 # corosync defaults to none if only one interface is configured
210 $param->{rrp_mode} = undef;
211
212 }
213
214 # No, corosync cannot deduce this on its own
215 my $ipversion = $bind_is_ipv6 ? 'ipv6' : 'ipv4';
216
217 my $config = <<_EOD;
218 totem {
219 version: 2
220 secauth: on
221 cluster_name: $clustername
222 config_version: 1
223 ip_version: $ipversion
224 $interfaces
225 }
226
227 nodelist {
228 node {
229 $ring_addresses
230 name: $nodename
231 nodeid: $param->{nodeid}
232 quorum_votes: $param->{votes}
233 }
234 }
235
236 quorum {
237 provider: corosync_votequorum
238 }
239
240 logging {
241 to_syslog: yes
242 debug: off
243 }
244 _EOD
245 ;
246 PVE::Tools::file_set_contents($clusterconf, $config);
247
248 PVE::Cluster::ssh_merge_keys();
249
250 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
251
252 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
253
254 PVE::Tools::run_command('systemctl restart pve-cluster'); # restart
255
256 PVE::Tools::run_command('systemctl restart corosync'); # restart
257
258 return undef;
259 }});
260
261 __PACKAGE__->register_method ({
262 name => 'addnode',
263 path => 'addnode',
264 method => 'PUT',
265 description => "Adds a node to the cluster configuration.",
266 parameters => {
267 additionalProperties => 0,
268 properties => {
269 node => PVE::JSONSchema::get_standard_option('pve-node'),
270 nodeid => {
271 type => 'integer',
272 description => "Node id for this node.",
273 minimum => 1,
274 optional => 1,
275 },
276 votes => {
277 type => 'integer',
278 description => "Number of votes for this node",
279 minimum => 0,
280 optional => 1,
281 },
282 force => {
283 type => 'boolean',
284 description => "Do not throw error if node already exists.",
285 optional => 1,
286 },
287 ring0_addr => {
288 type => 'string', format => 'address',
289 description => "Hostname (or IP) of the corosync ring0 address of this node.".
290 " Defaults to nodes hostname.",
291 optional => 1,
292 },
293 ring1_addr => {
294 type => 'string', format => 'address',
295 description => "Hostname (or IP) of the corosync ring1 address, this".
296 " needs an valid bindnet1_addr.",
297 optional => 1,
298 },
299 },
300 },
301 returns => { type => 'null' },
302
303 code => sub {
304 my ($param) = @_;
305
306 if (!$param->{force} && (-t STDIN || -t STDOUT)) {
307 die "error: `addnode` should not get called interactively!\nUse ".
308 "`pvecm add <cluster-node>` to add a node to a cluster!\n";
309 }
310
311 PVE::Cluster::check_cfs_quorum();
312
313 my $code = sub {
314 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
315 my $nodelist = PVE::Corosync::nodelist($conf);
316 my $totem_cfg = PVE::Corosync::totem_config($conf);
317
318 my $name = $param->{node};
319
320 # ensure we do not reuse an address, that can crash the whole cluster!
321 my $check_duplicate_addr = sub {
322 my $addr = shift;
323 return if !defined($addr);
324
325 while (my ($k, $v) = each %$nodelist) {
326 next if $k eq $name; # allows re-adding a node if force is set
327 if ($v->{ring0_addr} eq $addr || ($v->{ring1_addr} && $v->{ring1_addr} eq $addr)) {
328 die "corosync: address '$addr' already defined by node '$k'\n";
329 }
330 }
331 };
332
333 &$check_duplicate_addr($param->{ring0_addr});
334 &$check_duplicate_addr($param->{ring1_addr});
335
336 $param->{ring0_addr} = $name if !$param->{ring0_addr};
337
338 die "corosync: using 'ring1_addr' parameter needs a configured ring 1 interface!\n"
339 if $param->{ring1_addr} && !defined($totem_cfg->{interface}->{1});
340
341 die "corosync: ring 1 interface configured but 'ring1_addr' parameter not defined!\n"
342 if defined($totem_cfg->{interface}->{1}) && !defined($param->{ring1_addr});
343
344 if (defined(my $res = $nodelist->{$name})) {
345 $param->{nodeid} = $res->{nodeid} if !$param->{nodeid};
346 $param->{votes} = $res->{quorum_votes} if !defined($param->{votes});
347
348 if ($res->{quorum_votes} == $param->{votes} &&
349 $res->{nodeid} == $param->{nodeid}) {
350 print "node $name already defined\n";
351 if ($param->{force}) {
352 exit (0);
353 } else {
354 exit (-1);
355 }
356 } else {
357 die "can't add existing node\n";
358 }
359 } elsif (!$param->{nodeid}) {
360 my $nodeid = 1;
361
362 while(1) {
363 my $found = 0;
364 foreach my $v (values %$nodelist) {
365 if ($v->{nodeid} eq $nodeid) {
366 $found = 1;
367 $nodeid++;
368 last;
369 }
370 }
371 last if !$found;
372 };
373
374 $param->{nodeid} = $nodeid;
375 }
376
377 $param->{votes} = 1 if !defined($param->{votes});
378
379 PVE::Cluster::gen_local_dirs($name);
380
381 eval { PVE::Cluster::ssh_merge_keys(); };
382 warn $@ if $@;
383
384 $nodelist->{$name} = {
385 ring0_addr => $param->{ring0_addr},
386 nodeid => $param->{nodeid},
387 name => $name,
388 };
389 $nodelist->{$name}->{ring1_addr} = $param->{ring1_addr} if $param->{ring1_addr};
390 $nodelist->{$name}->{quorum_votes} = $param->{votes} if $param->{votes};
391
392 PVE::Corosync::update_nodelist($conf, $nodelist);
393 };
394
395 PVE::Cluster::cfs_lock_file('corosync.conf', 10, &$code);
396 die $@ if $@;
397
398 exit (0);
399 }});
400
401
402 __PACKAGE__->register_method ({
403 name => 'delnode',
404 path => 'delnode',
405 method => 'PUT',
406 description => "Removes a node to the cluster configuration.",
407 parameters => {
408 additionalProperties => 0,
409 properties => {
410 node => {
411 type => 'string',
412 description => "Hostname or IP of the corosync ring0 address of this node.",
413 },
414 },
415 },
416 returns => { type => 'null' },
417
418 code => sub {
419 my ($param) = @_;
420
421 PVE::Cluster::check_cfs_quorum();
422
423 my $code = sub {
424 my $conf = PVE::Cluster::cfs_read_file("corosync.conf");
425 my $nodelist = PVE::Corosync::nodelist($conf);
426
427 my $node;
428 my $nodeid;
429
430 foreach my $tmp_node (keys %$nodelist) {
431 my $d = $nodelist->{$tmp_node};
432 my $ring0_addr = $d->{ring0_addr};
433 my $ring1_addr = $d->{ring1_addr};
434 if (($tmp_node eq $param->{node}) ||
435 (defined($ring0_addr) && ($ring0_addr eq $param->{node})) ||
436 (defined($ring1_addr) && ($ring1_addr eq $param->{node}))) {
437 $node = $tmp_node;
438 $nodeid = $d->{nodeid};
439 last;
440 }
441 }
442
443 die "Node/IP: $param->{node} is not a known host of the cluster.\n"
444 if !defined($node);
445
446 my $our_nodename = PVE::INotify::nodename();
447 die "Cannot delete myself from cluster!\n" if $node eq $our_nodename;
448
449 delete $nodelist->{$node};
450
451 PVE::Corosync::update_nodelist($conf, $nodelist);
452
453 PVE::Tools::run_command(['corosync-cfgtool','-k', $nodeid])
454 if defined($nodeid);
455 };
456
457 PVE::Cluster::cfs_lock_file('corosync.conf', 10, $code);
458 die $@ if $@;
459
460 return undef;
461 }});
462
463 __PACKAGE__->register_method ({
464 name => 'add',
465 path => 'add',
466 method => 'PUT',
467 description => "Adds the current node to an existing cluster.",
468 parameters => {
469 additionalProperties => 0,
470 properties => {
471 hostname => {
472 type => 'string',
473 description => "Hostname (or IP) of an existing cluster member."
474 },
475 nodeid => {
476 type => 'integer',
477 description => "Node id for this node.",
478 minimum => 1,
479 optional => 1,
480 },
481 votes => {
482 type => 'integer',
483 description => "Number of votes for this node",
484 minimum => 0,
485 optional => 1,
486 },
487 force => {
488 type => 'boolean',
489 description => "Do not throw error if node already exists.",
490 optional => 1,
491 },
492 ring0_addr => {
493 type => 'string', format => 'address',
494 description => "Hostname (or IP) of the corosync ring0 address of this node.".
495 " Defaults to nodes hostname.",
496 optional => 1,
497 },
498 ring1_addr => {
499 type => 'string', format => 'address',
500 description => "Hostname (or IP) of the corosync ring1 address, this".
501 " needs an valid configured ring 1 interface in the cluster.",
502 optional => 1,
503 },
504 },
505 },
506 returns => { type => 'null' },
507
508 code => sub {
509 my ($param) = @_;
510
511 my $nodename = PVE::INotify::nodename();
512
513 PVE::Cluster::setup_sshd_config(1);
514 PVE::Cluster::setup_rootsshconfig();
515 PVE::Cluster::setup_ssh_keys();
516
517 my $host = $param->{hostname};
518
519 my ($errors, $warnings) = ('', '');
520
521 my $error = sub {
522 my ($msg, $suppress) = @_;
523
524 if ($suppress) {
525 $warnings .= "* $msg\n";
526 } else {
527 $errors .= "* $msg\n";
528 }
529 };
530
531 if (!$param->{force}) {
532
533 if (-f $authfile) {
534 &$error("authentication key '$authfile' already exists", $param->{force});
535 }
536
537 if (-f $clusterconf) {
538 &$error("cluster config '$clusterconf' already exists", $param->{force});
539 }
540
541 my $vmlist = PVE::Cluster::get_vmlist();
542 if ($vmlist && $vmlist->{ids} && scalar(keys %{$vmlist->{ids}})) {
543 &$error("this host already contains virtual guests", $param->{force});
544 }
545
546 if (system("corosync-quorumtool -l >/dev/null 2>&1") == 0) {
547 &$error("corosync is already running, is this node already in a cluster?!", $param->{force});
548 }
549 }
550
551 # check if corosync ring IPs are configured on the current nodes interfaces
552 my $check_ip = sub {
553 my $ip = shift;
554 if (defined($ip)) {
555 if (!PVE::JSONSchema::pve_verify_ip($ip, 1)) {
556 my $host = $ip;
557 eval { $ip = PVE::Network::get_ip_from_hostname($host); };
558 if ($@) {
559 &$error("cannot use '$host': $@\n") ;
560 return;
561 }
562 }
563
564 my $cidr = (Net::IP::ip_is_ipv6($ip)) ? "$ip/128" : "$ip/32";
565 my $configured_ips = PVE::Network::get_local_ip_from_cidr($cidr);
566
567 &$error("cannot use IP '$ip', it must be configured exactly once on local node!\n")
568 if (scalar(@$configured_ips) != 1);
569 }
570 };
571
572 &$check_ip($param->{ring0_addr});
573 &$check_ip($param->{ring1_addr});
574
575 warn "warning, ignore the following errors:\n$warnings" if $warnings;
576 die "detected the following error(s):\n$errors" if $errors;
577
578 # make sure known_hosts is on local filesystem
579 PVE::Cluster::ssh_unmerge_known_hosts();
580
581 my $cmd = ['ssh-copy-id', '-i', '/root/.ssh/id_rsa', "root\@$host"];
582 PVE::Tools::run_command($cmd, 'outfunc' => sub {}, 'errfunc' => sub {},
583 'errmsg' => "unable to copy ssh ID");
584
585 $cmd = ['ssh', $host, '-o', 'BatchMode=yes',
586 'pvecm', 'addnode', $nodename, '--force', 1];
587
588 push @$cmd, '--nodeid', $param->{nodeid} if $param->{nodeid};
589
590 push @$cmd, '--votes', $param->{votes} if defined($param->{votes});
591
592 push @$cmd, '--ring0_addr', $param->{ring0_addr} if defined($param->{ring0_addr});
593
594 push @$cmd, '--ring1_addr', $param->{ring1_addr} if defined($param->{ring1_addr});
595
596 if (system (@$cmd) != 0) {
597 my $cmdtxt = join (' ', @$cmd);
598 die "unable to add node: command failed ($cmdtxt)\n";
599 }
600
601 my $tmpdir = "$libdir/.pvecm_add.tmp.$$";
602 mkdir $tmpdir;
603
604 eval {
605 print "copy corosync auth key\n";
606 $cmd = ['rsync', '--rsh=ssh -l root -o BatchMode=yes', '-lpgoq',
607 "[$host]:$authfile $clusterconf", $tmpdir];
608
609 system(@$cmd) == 0 || die "can't rsync data from host '$host'\n";
610
611 mkdir "/etc/corosync";
612 my $confbase = basename($clusterconf);
613
614 $cmd = "cp '$tmpdir/$confbase' '/etc/corosync/$confbase'";
615 system($cmd) == 0 || die "can't copy cluster configuration\n";
616
617 my $keybase = basename($authfile);
618 system ("cp '$tmpdir/$keybase' '$authfile'") == 0 ||
619 die "can't copy '$tmpdir/$keybase' to '$authfile'\n";
620
621 print "stopping pve-cluster service\n";
622
623 system("umount $basedir -f >/dev/null 2>&1");
624 system("systemctl stop pve-cluster") == 0 ||
625 die "can't stop pve-cluster service\n";
626
627 backup_database();
628
629 unlink $dbfile;
630
631 system("systemctl start pve-cluster") == 0 ||
632 die "starting pve-cluster failed\n";
633
634 system("systemctl start corosync");
635
636 # wait for quorum
637 my $printqmsg = 1;
638 while (!PVE::Cluster::check_cfs_quorum(1)) {
639 if ($printqmsg) {
640 print "waiting for quorum...";
641 STDOUT->flush();
642 $printqmsg = 0;
643 }
644 sleep(1);
645 }
646 print "OK\n" if !$printqmsg;
647
648 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
649
650 print "generating node certificates\n";
651 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address);
652
653 print "merge known_hosts file\n";
654 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address, 1);
655
656 print "restart services\n";
657 # restart pvedaemon (changed certs)
658 system("systemctl restart pvedaemon");
659 # restart pveproxy (changed certs)
660 system("systemctl restart pveproxy");
661
662 print "successfully added node '$nodename' to cluster.\n";
663 };
664 my $err = $@;
665
666 rmtree $tmpdir;
667
668 die $err if $err;
669
670 return undef;
671 }});
672
673 __PACKAGE__->register_method ({
674 name => 'status',
675 path => 'status',
676 method => 'GET',
677 description => "Displays the local view of the cluster status.",
678 parameters => {
679 additionalProperties => 0,
680 properties => {},
681 },
682 returns => { type => 'null' },
683
684 code => sub {
685 my ($param) = @_;
686
687 PVE::Corosync::check_conf_exists();
688
689 my $cmd = ['corosync-quorumtool', '-siH'];
690
691 exec (@$cmd);
692
693 exit (-1); # should not be reached
694 }});
695
696 __PACKAGE__->register_method ({
697 name => 'nodes',
698 path => 'nodes',
699 method => 'GET',
700 description => "Displays the local view of the cluster nodes.",
701 parameters => {
702 additionalProperties => 0,
703 properties => {},
704 },
705 returns => { type => 'null' },
706
707 code => sub {
708 my ($param) = @_;
709
710 PVE::Corosync::check_conf_exists();
711
712 my $cmd = ['corosync-quorumtool', '-l'];
713
714 exec (@$cmd);
715
716 exit (-1); # should not be reached
717 }});
718
719 __PACKAGE__->register_method ({
720 name => 'expected',
721 path => 'expected',
722 method => 'PUT',
723 description => "Tells corosync a new value of expected votes.",
724 parameters => {
725 additionalProperties => 0,
726 properties => {
727 expected => {
728 type => 'integer',
729 description => "Expected votes",
730 minimum => 1,
731 },
732 },
733 },
734 returns => { type => 'null' },
735
736 code => sub {
737 my ($param) = @_;
738
739 PVE::Corosync::check_conf_exists();
740
741 my $cmd = ['corosync-quorumtool', '-e', $param->{expected}];
742
743 exec (@$cmd);
744
745 exit (-1); # should not be reached
746
747 }});
748
749 __PACKAGE__->register_method ({
750 name => 'updatecerts',
751 path => 'updatecerts',
752 method => 'PUT',
753 description => "Update node certificates (and generate all needed files/directories).",
754 parameters => {
755 additionalProperties => 0,
756 properties => {
757 force => {
758 description => "Force generation of new SSL certifate.",
759 type => 'boolean',
760 optional => 1,
761 },
762 silent => {
763 description => "Ignore errors (i.e. when cluster has no quorum).",
764 type => 'boolean',
765 optional => 1,
766 },
767 },
768 },
769 returns => { type => 'null' },
770 code => sub {
771 my ($param) = @_;
772
773 PVE::Cluster::setup_sshd_config(0);
774 PVE::Cluster::setup_rootsshconfig();
775
776 PVE::Cluster::gen_pve_vzdump_symlink();
777
778 if (!PVE::Cluster::check_cfs_quorum(1)) {
779 return undef if $param->{silent};
780 die "no quorum - unable to update files\n";
781 }
782
783 PVE::Cluster::setup_ssh_keys();
784
785 my $nodename = PVE::INotify::nodename();
786
787 my $local_ip_address = PVE::Cluster::remote_node_ip($nodename);
788
789 PVE::Cluster::gen_pve_node_files($nodename, $local_ip_address, $param->{force});
790 PVE::Cluster::ssh_merge_keys();
791 PVE::Cluster::ssh_merge_known_hosts($nodename, $local_ip_address);
792 PVE::Cluster::gen_pve_vzdump_files();
793
794 return undef;
795 }});
796
797 __PACKAGE__->register_method ({
798 name => 'mtunnel',
799 path => 'mtunnel',
800 method => 'POST',
801 description => "Used by VM/CT migration - do not use manually.",
802 parameters => {
803 additionalProperties => 0,
804 properties => {
805 get_migration_ip => {
806 type => 'boolean',
807 default => 0,
808 description => 'return the migration IP, if configured',
809 optional => 1,
810 },
811 migration_network => {
812 type => 'string',
813 format => 'CIDR',
814 description => 'the migration network used to detect the local migration IP',
815 optional => 1,
816 },
817 'run-command' => {
818 type => 'boolean',
819 description => 'Run a command with a tcp socket as standard input.'
820 .' The IP address and port are printed via this'
821 ." command's stdandard output first, each on a separate line.",
822 optional => 1,
823 },
824 'extra-args' => PVE::JSONSchema::get_standard_option('extra-args'),
825 },
826 },
827 returns => { type => 'null'},
828 code => sub {
829 my ($param) = @_;
830
831 if (!PVE::Cluster::check_cfs_quorum(1)) {
832 print "no quorum\n";
833 return undef;
834 }
835
836 my $network = $param->{migration_network};
837 if ($param->{get_migration_ip}) {
838 die "cannot use --run-command with --get_migration_ip\n"
839 if $param->{'run-command'};
840 if (my $ip = PVE::Cluster::get_local_migration_ip($network)) {
841 print "ip: '$ip'\n";
842 } else {
843 print "no ip\n";
844 }
845 # do not keep tunnel open when asked for migration ip
846 return undef;
847 }
848
849 if ($param->{'run-command'}) {
850 my $cmd = $param->{'extra-args'};
851 die "missing command\n"
852 if !$cmd || !scalar(@$cmd);
853
854 # Get an ip address to listen on, and find a free migration port
855 my ($ip, $family);
856 if (defined($network)) {
857 $ip = PVE::Cluster::get_local_migration_ip($network)
858 or die "failed to get migration IP address to listen on\n";
859 $family = Net::IP::ip_is_ipv6($ip) ? AF_INET6 : AF_INET;
860 } else {
861 my $nodename = PVE::INotify::nodename();
862 ($ip, $family) = PVE::Network::get_ip_from_hostname($nodename, 0);
863 }
864 my $port = PVE::Tools::next_migrate_port($family, $ip);
865
866 PVE::Tools::pipe_socket_to_command($cmd, $ip, $port);
867 return undef;
868 }
869
870 print "tunnel online\n";
871 *STDOUT->flush();
872
873 while (my $line = <>) {
874 chomp $line;
875 last if $line =~ m/^quit$/;
876 }
877
878 return undef;
879 }});
880
881
882 our $cmddef = {
883 keygen => [ __PACKAGE__, 'keygen', ['filename']],
884 create => [ __PACKAGE__, 'create', ['clustername']],
885 add => [ __PACKAGE__, 'add', ['hostname']],
886 addnode => [ __PACKAGE__, 'addnode', ['node']],
887 delnode => [ __PACKAGE__, 'delnode', ['node']],
888 status => [ __PACKAGE__, 'status' ],
889 nodes => [ __PACKAGE__, 'nodes' ],
890 expected => [ __PACKAGE__, 'expected', ['expected']],
891 updatecerts => [ __PACKAGE__, 'updatecerts', []],
892 mtunnel => [ __PACKAGE__, 'mtunnel', ['extra-args']],
893 };
894
895 1;