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