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