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