1 package PVE
::API2
::Ceph
::MON
;
10 use PVE
::Ceph
::Services
;
11 use PVE
::Cluster
qw(cfs_read_file cfs_write_file);
12 use PVE
::JSONSchema
qw(get_standard_option);
16 use PVE
::RPCEnvironment
;
17 use PVE
::Tools
qw(run_command file_set_contents);
19 use PVE
::API2
::Ceph
::MGR
;
21 use base
qw(PVE::RESTHandler);
23 my $find_mon_ips = sub {
24 my ($cfg, $rados, $node, $mon_address) = @_;
26 my $overwrite_ips = [ PVE
::Tools
::split_list
($mon_address) ];
27 $overwrite_ips = PVE
::Network
::unique_ips
($overwrite_ips);
31 $pubnet = $rados->mon_command({ prefix
=> "config get" , who
=> "mon.",
32 key
=> "public_network", format
=> 'plain' });
33 # if not defined in the db, the result is empty, it is also always
34 # followed by a newline
35 ($pubnet) = $pubnet =~ m/^(\S+)$/;
37 $pubnet //= $cfg->{global
}->{public_network
};
40 if (scalar(@{$overwrite_ips})) {
41 return $overwrite_ips;
43 # don't refactor into '[ PVE::Cluster::remote... ]' as it uses wantarray
44 my $ip = PVE
::Cluster
::remote_node_ip
($node);
49 my $public_nets = [ PVE
::Tools
::split_list
($pubnet) ];
50 if (scalar(@{$public_nets}) > 1) {
51 warn "Multiple Ceph public networks detected on $node: $pubnet\n";
52 warn "Networks must be capable of routing to each other.\n";
57 if (!scalar(@{$overwrite_ips})) { # auto-select one address for each public network
58 for my $net (@{$public_nets}) {
59 $net = PVE
::JSONSchema
::pve_verify_cidr
($net);
61 my $allowed_ips = PVE
::Network
::get_local_ip_from_cidr
($net);
62 $allowed_ips = PVE
::Network
::unique_ips
($allowed_ips);
64 die "No active IP found for the requested ceph public network '$net' on node '$node'\n"
65 if scalar(@$allowed_ips) < 1;
67 if (scalar(@$allowed_ips) == 1) {
68 push @{$res}, $allowed_ips->[0];
70 die "Multiple IPs for ceph public network '$net' detected on $node:\n".
71 join("\n", @$allowed_ips) ."\nuse 'mon-address' to specify one of them.\n";
74 } else { # check if overwrite IPs are active and in any of the public networks
75 my $allowed_list = [];
77 for my $net (@{$public_nets}) {
78 $net = PVE
::JSONSchema
::pve_verify_cidr
($net);
80 push @{$allowed_list}, @{PVE
::Network
::get_local_ip_from_cidr
($net)};
83 my $allowed_ips = PVE
::Network
::unique_ips
($allowed_list);
85 for my $overwrite_ip (@{$overwrite_ips}) {
86 die "Specified monitor IP '$overwrite_ip' not configured or up on $node!\n"
87 if !grep { $_ eq $overwrite_ip } @{$allowed_ips};
89 push @{$res}, $overwrite_ip;
96 my $ips_from_mon_host = sub {
101 my @hosts = PVE
::Tools
::split_list
($mon_host);
103 for my $host (@hosts) {
104 $host =~ s
|^\
[?v\d
+\
:||; # remove beginning of vector
105 $host =~ s
|/\d
+\
]?
||; # remove end of vector
107 ($host) = PVE
::Tools
::parse_host_and_port
($host);
108 next if !defined($host);
110 # filter out hostnames
111 my $ip = PVE
::JSONSchema
::pve_verify_ip
($host, 1);
112 next if !defined($ip);
120 my $assert_mon_prerequisites = sub {
121 my ($cfg, $monhash, $monid, $monips) = @_;
125 my $mon_host_ips = $ips_from_mon_host->($cfg->{global
}->{mon_host
});
127 for my $mon_host_ip (@{$mon_host_ips}) {
128 my $ip = PVE
::Network
::canonical_ip
($mon_host_ip);
129 $used_ips->{$ip} = 1;
132 for my $mon (values %{$monhash}) {
133 next if !defined($mon->{addr
});
135 for my $ip ($ips_from_mon_host->($mon->{addr
})->@*) {
136 $ip = PVE
::Network
::canonical_ip
($ip);
137 $used_ips->{$ip} = 1;
141 for my $monip (@{$monips}) {
142 $monip = PVE
::Network
::canonical_ip
($monip);
143 die "monitor address '$monip' already in use\n" if $used_ips->{$monip};
146 if (defined($monhash->{$monid})) {
147 die "monitor '$monid' already exists\n";
151 my $assert_mon_can_remove = sub {
152 my ($monhash, $monlist, $monid, $mondir) = @_;
154 if (!(defined($monhash->{"mon.$monid"}) ||
155 grep { $_->{name
} && $_->{name
} eq $monid } @$monlist))
157 die "no such monitor id '$monid'\n"
160 die "monitor filesystem '$mondir' does not exist on this node\n" if ! -d
$mondir;
161 die "can't remove last monitor\n" if scalar(@$monlist) <= 1;
164 my $remove_addr_from_mon_host = sub {
165 my ($monhost, $addr) = @_;
167 $addr = "[$addr]" if PVE
::JSONSchema
::pve_verify_ipv6
($addr, 1);
169 # various replaces to remove the ip
170 # we always match the beginning or a separator (also at the end)
171 # so we do not accidentally remove a wrong ip
172 # e.g. removing 10.0.0.1 should not remove 10.0.0.101 or 110.0.0.1
174 # remove vector containing this ip
175 # format is [vX:ip:port/nonce,vY:ip:port/nonce]
176 my $vectorpart_re = "v\\d+:\Q$addr\E:\\d+\\/\\d+";
177 $monhost =~ s/(^|[ ,;]*)\[$vectorpart_re(?:,$vectorpart_re)*\](?:[ ,;]+|$)/$1/;
180 $monhost =~ s/(^|[ ,;]+)\Q$addr\E(?::\d+)?(?:[ ,;]+|$)/$1/;
182 # ipv6 only without brackets
183 if ($addr =~ m/^\[?(.*?:.*?)\]?$/) {
185 $monhost =~ s/(^|[ ,;]+)\Q$addr\E(?:[ ,;]+|$)/$1/;
188 # remove trailing separators
189 $monhost =~ s/[ ,;]+$//;
194 __PACKAGE__-
>register_method ({
198 description
=> "Get Ceph monitor list.",
202 check
=> ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any
=> 1],
205 additionalProperties
=> 0,
207 node
=> get_standard_option
('pve-node'),
215 name
=> { type
=> 'string' },
216 addr
=> { type
=> 'string', optional
=> 1 },
217 host
=> { type
=> 'string', optional
=> 1 },
220 links
=> [ { rel
=> 'child', href
=> "{name}" } ],
225 PVE
::Ceph
::Tools
::check_ceph_inited
();
229 my $cfg = cfs_read_file
('ceph.conf');
231 my $rados = eval { PVE
::RADOS-
>new() };
233 my $monhash = PVE
::Ceph
::Services
::get_services_info
("mon", $cfg, $rados);
236 my $monstat = $rados->mon_command({ prefix
=> 'quorum_status' });
238 my $mons = $monstat->{monmap
}->{mons
};
239 foreach my $d (@$mons) {
240 next if !defined($d->{name
});
241 my $name = $d->{name
};
242 $monhash->{$name}->{rank
} = $d->{rank
};
243 $monhash->{$name}->{addr
} = $d->{addr
};
244 if (grep { $_ eq $d->{rank
} } @{$monstat->{quorum
}}) {
245 $monhash->{$name}->{quorum
} = 1;
246 $monhash->{$name}->{state} = 'running';
251 # we cannot check the status if we do not have a RADOS
252 # object, so set the state to unknown
253 foreach my $monid (sort keys %$monhash) {
254 $monhash->{$monid}->{state} = 'unknown';
258 return PVE
::RESTHandler
::hash_to_array
($monhash, 'name');
261 __PACKAGE__-
>register_method ({
265 description
=> "Create Ceph Monitor and Manager",
269 check
=> ['perm', '/', [ 'Sys.Modify' ]],
272 additionalProperties
=> 0,
274 node
=> get_standard_option
('pve-node'),
278 pattern
=> PVE
::Ceph
::Services
::SERVICE_REGEX
,
280 description
=> "The ID for the monitor, when omitted the same as the nodename",
283 description
=> 'Overwrites autodetected monitor IP address(es). ' .
284 'Must be in the public network(s) of Ceph.',
285 type
=> 'string', format
=> 'ip-list',
290 returns
=> { type
=> 'string' },
294 PVE
::Ceph
::Tools
::check_ceph_installed
('ceph_mon');
295 PVE
::Ceph
::Tools
::check_ceph_inited
();
296 PVE
::Ceph
::Tools
::setup_pve_symlinks
();
298 my $rpcenv = PVE
::RPCEnvironment
::get
();
299 my $authuser = $rpcenv->get_user();
301 my $cfg = cfs_read_file
('ceph.conf');
302 my $rados = eval { PVE
::RADOS-
>new() }; # try a rados connection, fails for first monitor
303 my $monhash = PVE
::Ceph
::Services
::get_services_info
('mon', $cfg, $rados);
305 my $is_first_monitor = !(scalar(keys %$monhash) || $cfg->{global
}->{mon_host
});
307 if (!defined($rados) && !$is_first_monitor) {
308 die "Could not connect to ceph cluster despite configured monitors\n";
311 my $monid = $param->{monid
} // $param->{node
};
312 my $monsection = "mon.$monid";
313 my $ips = $find_mon_ips->($cfg, $rados, $param->{node
}, $param->{'mon-address'});
315 $assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
320 PVE
::Cluster
::cfs_lock_file
('ceph.conf', undef, sub {
321 # update cfg content and reassert prereqs inside the lock
322 $cfg = cfs_read_file
('ceph.conf');
323 # reopen with longer timeout
324 if (defined($rados)) {
325 $rados = PVE
::RADOS-
>new(timeout
=> PVE
::Ceph
::Tools
::get_config
('long_rados_timeout'));
327 $monhash = PVE
::Ceph
::Services
::get_services_info
('mon', $cfg, $rados);
328 $assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
330 my $client_keyring = PVE
::Ceph
::Tools
::get_or_create_admin_keyring
();
331 my $mon_keyring = PVE
::Ceph
::Tools
::get_config
('pve_mon_key_path');
333 if (! -f
$mon_keyring) {
334 print "creating new monitor keyring\n";
354 my $ccname = PVE
::Ceph
::Tools
::get_config
('ccname');
355 my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
356 -d
$mondir && die "monitor filesystem '$mondir' already exist\n";
358 my $monmap = "/tmp/monmap";
363 run_command
(['chown', 'ceph:ceph', $mondir]);
365 my $is_first_address = !defined($rados);
369 for my $ip (@{$ips}) {
370 if (Net
::IP
::ip_is_ipv6
($ip)) {
371 $cfg->{global
}->{ms_bind_ipv6
} = 'true';
372 $cfg->{global
}->{ms_bind_ipv4
} = 'false' if $is_first_address;
374 $cfg->{global
}->{ms_bind_ipv4
} = 'true';
375 $cfg->{global
}->{ms_bind_ipv6
} = 'false' if $is_first_address;
378 my $monaddr = Net
::IP
::ip_is_ipv6
($ip) ?
"[$ip]" : $ip;
379 push @{$monaddrs}, "v2:$monaddr:3300";
380 push @{$monaddrs}, "v1:$monaddr:6789";
382 $is_first_address = 0;
385 my $monmaptool_cmd = [
390 "[" . join(',', @{$monaddrs}) . "]",
395 if (defined($rados)) { # we can only have a RADOS object if we have a monitor
396 my $mapdata = $rados->mon_command({ prefix
=> 'mon getmap', format
=> 'plain' });
397 file_set_contents
($monmap, $mapdata);
398 run_command
($monmaptool_cmd);
399 } else { # we need to create a monmap for the first monitor
400 push @{$monmaptool_cmd}, '--create';
401 run_command
($monmaptool_cmd);
414 run_command
(['chown', 'ceph:ceph', '-R', $mondir]);
419 File
::Path
::remove_tree
($mondir);
424 my $monhost = $cfg->{global
}->{mon_host
} // "";
425 # add all known monitor ips to mon_host if it does not exist
426 if (!defined($cfg->{global
}->{mon_host
})) {
427 for my $mon (sort keys %$monhash) {
428 $monhost .= " " . $monhash->{$mon}->{addr
};
431 $monhost .= " " . join(' ', @{$ips});
432 $cfg->{global
}->{mon_host
} = $monhost;
433 # The IP is needed in the ceph.conf for the first boot
434 $cfg->{$monsection}->{public_addr
} = $ips->[0];
436 cfs_write_file
('ceph.conf', $cfg);
438 PVE
::Ceph
::Services
::ceph_service_cmd
('start', $monsection);
440 if ($is_first_monitor) {
441 print "created the first monitor, assume it's safe to disable insecure global"
442 ." ID reclaim for new setup\n";
445 ['ceph', 'config', 'set', 'mon', 'auth_allow_insecure_global_id_reclaim', 'false'],
446 errfunc
=> sub { print STDERR
"$_[0]\n" },
452 eval { PVE
::Ceph
::Services
::ceph_service_cmd
('enable', $monsection) };
453 warn "Enable ceph-mon\@${monid}.service failed, do manually: $@\n" if $@;
455 PVE
::Ceph
::Services
::broadcast_ceph_services
();
458 # automatically create manager after the first monitor is created
459 if ($is_first_monitor) {
460 PVE
::API2
::Ceph
::MGR-
>createmgr({
461 node
=> $param->{node
},
467 return $rpcenv->fork_worker('cephcreatemon', $monsection, $authuser, $worker);
470 __PACKAGE__-
>register_method ({
471 name
=> 'destroymon',
474 description
=> "Destroy Ceph Monitor and Manager.",
478 check
=> ['perm', '/', [ 'Sys.Modify' ]],
481 additionalProperties
=> 0,
483 node
=> get_standard_option
('pve-node'),
485 description
=> 'Monitor ID',
487 pattern
=> PVE
::Ceph
::Services
::SERVICE_REGEX
,
491 returns
=> { type
=> 'string' },
495 my $rpcenv = PVE
::RPCEnvironment
::get
();
497 my $authuser = $rpcenv->get_user();
499 PVE
::Ceph
::Tools
::check_ceph_inited
();
501 my $cfg = cfs_read_file
('ceph.conf');
503 my $monid = $param->{monid
};
504 my $monsection = "mon.$monid";
506 my $rados = PVE
::RADOS-
>new();
507 my $monstat = $rados->mon_command({ prefix
=> 'quorum_status' });
508 my $monlist = $monstat->{monmap
}->{mons
};
509 my $monhash = PVE
::Ceph
::Services
::get_services_info
('mon', $cfg, $rados);
511 my $ccname = PVE
::Ceph
::Tools
::get_config
('ccname');
512 my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
514 $assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
518 PVE
::Cluster
::cfs_lock_file
('ceph.conf', undef, sub {
519 # reload info and recheck
520 $cfg = cfs_read_file
('ceph.conf');
522 # reopen with longer timeout
523 $rados = PVE
::RADOS-
>new(timeout
=> PVE
::Ceph
::Tools
::get_config
('long_rados_timeout'));
524 $monhash = PVE
::Ceph
::Services
::get_services_info
('mon', $cfg, $rados);
525 $monstat = $rados->mon_command({ prefix
=> 'quorum_status' });
526 $monlist = $monstat->{monmap
}->{mons
};
533 # extract the ip without port and nonce (if present)
534 ($addr) = $addr =~ m
|^(.*):\d
+(/\d
+)?
$|;
535 ($addr) = $addr =~ m
|^\
[?
(.*?
)\
]?
$|; # remove brackets
536 push @{$addrs}, $addr;
539 for my $mon (@$monlist) {
540 if ($mon->{name
} eq $monid) {
541 if ($mon->{public_addrs
} && $mon->{public_addrs
}->{addrvec
}) {
542 my $addrvec = $mon->{public_addrs
}->{addrvec
};
543 for my $addr (@{$addrvec}) {
544 $add_addr->($addr->{addr
});
547 $add_addr->($mon->{public_addr
} // $mon->{addr
});
553 $assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
555 # this also stops the service
556 $rados->mon_command({ prefix
=> "mon remove", name
=> $monid, format
=> 'plain' });
559 delete $cfg->{$monsection};
561 # delete from mon_host
562 if (my $monhost = $cfg->{global
}->{mon_host
}) {
563 my $mon_host_ips = $ips_from_mon_host->($cfg->{global
}->{mon_host
});
565 for my $addr (@{$addrs}) {
566 $monhost = $remove_addr_from_mon_host->($monhost, $addr);
568 # also remove matching IPs that differ syntactically
569 if (PVE
::JSONSchema
::pve_verify_ip
($addr, 1)) {
570 $addr = PVE
::Network
::canonical_ip
($addr);
572 for my $mon_host_ip (@{$mon_host_ips}) {
573 # match canonical addresses, but remove as present in mon_host
574 if (PVE
::Network
::canonical_ip
($mon_host_ip) eq $addr) {
575 $monhost = $remove_addr_from_mon_host->($monhost, $mon_host_ip);
580 $cfg->{global
}->{mon_host
} = $monhost;
583 cfs_write_file
('ceph.conf', $cfg);
584 File
::Path
::remove_tree
($mondir);
585 eval { PVE
::Ceph
::Services
::ceph_service_cmd
('disable', $monsection) };
587 PVE
::Ceph
::Services
::broadcast_ceph_services
();
593 return $rpcenv->fork_worker('cephdestroymon', $monsection, $authuser, $worker);