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