pve-manager/PVE/API2/Ceph/MON.pm
Max Carrara 50ae86b758 api: ceph monitor: rework some code style to modern
Signed-off-by: Max Carrara <m.carrara@proxmox.com>
 [ TL: also improve if-expression wrapping ]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
2024-03-21 18:12:39 +01:00

601 lines
16 KiB
Perl

package PVE::API2::Ceph::MON;
use strict;
use warnings;
use Net::IP;
use File::Path;
use PVE::Ceph::Tools;
use PVE::Ceph::Services;
use PVE::Cluster qw(cfs_read_file cfs_write_file);
use PVE::JSONSchema qw(get_standard_option);
use PVE::Network;
use PVE::RADOS;
use PVE::RESTHandler;
use PVE::RPCEnvironment;
use PVE::Tools qw(run_command file_set_contents);
use PVE::CephConfig;
use PVE::API2::Ceph::MGR;
use base qw(PVE::RESTHandler);
my $find_mon_ips = sub {
my ($cfg, $rados, $node, $mon_address) = @_;
my $overwrite_ips = [ PVE::Tools::split_list($mon_address) ];
$overwrite_ips = PVE::Network::unique_ips($overwrite_ips);
my $pubnet;
if ($rados) {
$pubnet = $rados->mon_command({ prefix => "config get" , who => "mon.",
key => "public_network", format => 'plain' });
# if not defined in the db, the result is empty, it is also always
# followed by a newline
($pubnet) = $pubnet =~ m/^(\S+)$/;
}
$pubnet //= $cfg->{global}->{public_network};
if (!$pubnet) {
if (scalar(@{$overwrite_ips})) {
return $overwrite_ips;
} else {
# don't refactor into '[ PVE::Cluster::remote... ]' as it uses wantarray
my $ip = PVE::Cluster::remote_node_ip($node);
return [ $ip ];
}
}
my $public_nets = [ PVE::Tools::split_list($pubnet) ];
if (scalar(@{$public_nets}) > 1) {
warn "Multiple Ceph public networks detected on $node: $pubnet\n";
warn "Networks must be capable of routing to each other.\n";
}
my $res = [];
if (!scalar(@{$overwrite_ips})) { # auto-select one address for each public network
for my $net (@{$public_nets}) {
my $allowed_ips = PVE::Network::get_local_ip_from_cidr($net);
$allowed_ips = PVE::Network::unique_ips($allowed_ips);
die "No active IP found for the requested ceph public network '$net' on node '$node'\n"
if scalar(@$allowed_ips) < 1;
if (scalar(@$allowed_ips) == 1) {
push @{$res}, $allowed_ips->[0];
} else {
die "Multiple IPs for ceph public network '$net' detected on $node:\n".
join("\n", @$allowed_ips) ."\nuse 'mon-address' to specify one of them.\n";
}
}
} else { # check if overwrite IPs are active and in any of the public networks
my $allowed_list = [];
for my $net (@{$public_nets}) {
push @{$allowed_list}, @{PVE::Network::get_local_ip_from_cidr($net)};
}
my $allowed_ips = PVE::Network::unique_ips($allowed_list);
for my $overwrite_ip (@{$overwrite_ips}) {
die "Specified monitor IP '$overwrite_ip' not configured or up on $node!\n"
if !grep { $_ eq $overwrite_ip } @{$allowed_ips};
push @{$res}, $overwrite_ip;
}
}
return $res;
};
my $ips_from_mon_host = sub {
my ($mon_host) = @_;
my $ips = [];
my @hosts = PVE::Tools::split_list($mon_host);
for my $host (@hosts) {
$host =~ s|^\[?v\d+\:||; # remove beginning of vector
$host =~ s|/\d+\]?||; # remove end of vector
($host) = PVE::Tools::parse_host_and_port($host);
next if !defined($host);
# filter out hostnames
my $ip = PVE::JSONSchema::pve_verify_ip($host, 1);
next if !defined($ip);
push @{$ips}, $ip;
}
return $ips;
};
my $assert_mon_prerequisites = sub {
my ($cfg, $monhash, $monid, $monips) = @_;
my $used_ips = {};
my $mon_host_ips = $ips_from_mon_host->($cfg->{global}->{mon_host});
for my $mon_host_ip (@{$mon_host_ips}) {
my $ip = PVE::Network::canonical_ip($mon_host_ip);
$used_ips->{$ip} = 1;
}
for my $mon (values %{$monhash}) {
next if !defined($mon->{addr});
for my $ip ($ips_from_mon_host->($mon->{addr})->@*) {
$ip = PVE::Network::canonical_ip($ip);
$used_ips->{$ip} = 1;
}
}
for my $monip (@{$monips}) {
$monip = PVE::Network::canonical_ip($monip);
die "monitor address '$monip' already in use\n" if $used_ips->{$monip};
}
if (defined($monhash->{$monid})) {
die "monitor '$monid' already exists\n";
}
};
my $assert_mon_can_remove = sub {
my ($monhash, $monlist, $monid, $mondir) = @_;
if (
!defined($monhash->{$monid} ||
grep { defined($_->{name}) && $_->{name} eq $monid } $monlist->@*)
) {
die "no such monitor id '$monid'\n"
}
die "monitor filesystem '$mondir' does not exist on this node\n" if ! -d $mondir;
die "can't remove last monitor\n" if scalar($monlist->@*) <= 1;
};
my $remove_addr_from_mon_host = sub {
my ($monhost, $addr) = @_;
$addr = "[$addr]" if PVE::JSONSchema::pve_verify_ipv6($addr, 1);
# various replaces to remove the ip
# we always match the beginning or a separator (also at the end)
# so we do not accidentally remove a wrong ip
# e.g. removing 10.0.0.1 should not remove 10.0.0.101 or 110.0.0.1
# remove vector containing this ip
# format is [vX:ip:port/nonce,vY:ip:port/nonce]
my $vectorpart_re = "v\\d+:\Q$addr\E:\\d+\\/\\d+";
$monhost =~ s/(^|[ ,;]*)\[$vectorpart_re(?:,$vectorpart_re)*\](?:[ ,;]+|$)/$1/;
# ip (+ port)
$monhost =~ s/(^|[ ,;]+)\Q$addr\E(?::\d+)?(?:[ ,;]+|$)/$1/;
# ipv6 only without brackets
if ($addr =~ m/^\[?(.*?:.*?)\]?$/) {
$addr = $1;
$monhost =~ s/(^|[ ,;]+)\Q$addr\E(?:[ ,;]+|$)/$1/;
}
# remove trailing separators
$monhost =~ s/[ ,;]+$//;
return $monhost;
};
__PACKAGE__->register_method ({
name => 'listmon',
path => '',
method => 'GET',
description => "Get Ceph monitor list.",
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Audit', 'Datastore.Audit' ], any => 1],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
},
},
returns => {
type => 'array',
items => {
type => "object",
properties => {
addr => { type => 'string', optional => 1 },
ceph_version => { type => 'string', optional => 1 },
ceph_version_short => { type => 'string', optional => 1 },
direxists => { type => 'string', optional => 1 },
host => { type => 'boolean', optional => 1 },
name => { type => 'string' },
quorum => { type => 'boolean', optional => 1 },
rank => { type => 'integer', optional => 1 },
service => { type => 'integer', optional => 1 },
state => { type => 'string', optional => 1 },
},
},
links => [ { rel => 'child', href => "{name}" } ],
},
code => sub {
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_inited();
my $res = [];
my $cfg = cfs_read_file('ceph.conf');
my $rados = eval { PVE::RADOS->new() };
warn $@ if $@;
my $monhash = PVE::Ceph::Services::get_services_info("mon", $cfg, $rados);
if ($rados) {
my $monstat = $rados->mon_command({ prefix => 'quorum_status' });
my $mons = $monstat->{monmap}->{mons};
foreach my $d (@$mons) {
next if !defined($d->{name});
my $name = $d->{name};
$monhash->{$name}->{rank} = $d->{rank};
$monhash->{$name}->{addr} = $d->{addr};
if (grep { $_ eq $d->{rank} } @{$monstat->{quorum}}) {
$monhash->{$name}->{quorum} = 1;
$monhash->{$name}->{state} = 'running';
}
}
} else {
# we cannot check the status if we do not have a RADOS
# object, so set the state to unknown
foreach my $monid (sort keys %$monhash) {
$monhash->{$monid}->{state} = 'unknown';
}
}
return PVE::RESTHandler::hash_to_array($monhash, 'name');
}});
__PACKAGE__->register_method ({
name => 'createmon',
path => '{monid}',
method => 'POST',
description => "Create Ceph Monitor and Manager",
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
monid => {
type => 'string',
optional => 1,
pattern => PVE::Ceph::Services::SERVICE_REGEX,
maxLength => 200,
description => "The ID for the monitor, when omitted the same as the nodename",
},
'mon-address' => {
description => 'Overwrites autodetected monitor IP address(es). ' .
'Must be in the public network(s) of Ceph.',
type => 'string', format => 'ip-list',
optional => 1,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
PVE::Ceph::Tools::check_ceph_installed('ceph_mon');
PVE::Ceph::Tools::check_ceph_inited();
PVE::Ceph::Tools::setup_pve_symlinks();
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
my $cfg = cfs_read_file('ceph.conf');
my $rados = eval { PVE::RADOS->new() }; # try a rados connection, fails for first monitor
my $monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
my $is_first_monitor = !(scalar(keys %$monhash) || $cfg->{global}->{mon_host});
if (!defined($rados) && !$is_first_monitor) {
die "Could not connect to ceph cluster despite configured monitors\n";
}
my $monid = $param->{monid} // $param->{node};
my $monsection = "mon.$monid";
my $ips = $find_mon_ips->($cfg, $rados, $param->{node}, $param->{'mon-address'});
$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
my $worker = sub {
my $upid = shift;
PVE::Cluster::cfs_lock_file('ceph.conf', undef, sub {
# update cfg content and reassert prereqs inside the lock
$cfg = cfs_read_file('ceph.conf');
# reopen with longer timeout
if (defined($rados)) {
$rados = PVE::RADOS->new(timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
}
$monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
$assert_mon_prerequisites->($cfg, $monhash, $monid, $ips);
my $client_keyring = PVE::Ceph::Tools::get_or_create_admin_keyring();
my $mon_keyring = PVE::Ceph::Tools::get_config('pve_mon_key_path');
if (! -f $mon_keyring) {
print "creating new monitor keyring\n";
run_command([
'ceph-authtool',
'--create-keyring',
$mon_keyring,
'--gen-key',
'-n',
'mon.',
'--cap',
'mon',
'allow *',
]);
run_command([
'ceph-authtool',
$mon_keyring,
'--import-keyring',
$client_keyring,
]);
}
my $ccname = PVE::Ceph::Tools::get_config('ccname');
my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
-d $mondir && die "monitor filesystem '$mondir' already exist\n";
my $monmap = "/tmp/monmap";
eval {
mkdir $mondir;
run_command(['chown', 'ceph:ceph', $mondir]);
my $is_first_address = !defined($rados);
my $monaddrs = [];
for my $ip (@{$ips}) {
if (Net::IP::ip_is_ipv6($ip)) {
$cfg->{global}->{ms_bind_ipv6} = 'true';
$cfg->{global}->{ms_bind_ipv4} = 'false' if $is_first_address;
} else {
$cfg->{global}->{ms_bind_ipv4} = 'true';
$cfg->{global}->{ms_bind_ipv6} = 'false' if $is_first_address;
}
my $monaddr = Net::IP::ip_is_ipv6($ip) ? "[$ip]" : $ip;
push @{$monaddrs}, "v2:$monaddr:3300";
push @{$monaddrs}, "v1:$monaddr:6789";
$is_first_address = 0;
}
my $monmaptool_cmd = [
'monmaptool',
'--clobber',
'--addv',
$monid,
"[" . join(',', @{$monaddrs}) . "]",
'--print',
$monmap,
];
if (defined($rados)) { # we can only have a RADOS object if we have a monitor
my $mapdata = $rados->mon_command({ prefix => 'mon getmap', format => 'plain' });
file_set_contents($monmap, $mapdata);
run_command($monmaptool_cmd);
} else { # we need to create a monmap for the first monitor
push @{$monmaptool_cmd}, '--create';
run_command($monmaptool_cmd);
}
run_command([
'ceph-mon',
'--mkfs',
'-i',
$monid,
'--monmap',
$monmap,
'--keyring',
$mon_keyring,
]);
run_command(['chown', 'ceph:ceph', '-R', $mondir]);
};
my $err = $@;
unlink $monmap;
if ($err) {
File::Path::remove_tree($mondir);
die $err;
}
# update ceph.conf
my $monhost = $cfg->{global}->{mon_host} // "";
# add all known monitor ips to mon_host if it does not exist
if (!defined($cfg->{global}->{mon_host})) {
for my $mon (sort keys %$monhash) {
$monhost .= " " . $monhash->{$mon}->{addr};
}
}
$monhost .= " " . join(' ', @{$ips});
$cfg->{global}->{mon_host} = $monhost;
# The IP is needed in the ceph.conf for the first boot
$cfg->{$monsection}->{public_addr} = $ips->[0];
cfs_write_file('ceph.conf', $cfg);
PVE::Ceph::Services::ceph_service_cmd('start', $monsection);
if ($is_first_monitor) {
print "created the first monitor, assume it's safe to disable insecure global"
." ID reclaim for new setup\n";
eval {
run_command(
['ceph', 'config', 'set', 'mon', 'auth_allow_insecure_global_id_reclaim', 'false'],
errfunc => sub { print STDERR "$_[0]\n" },
)
};
warn "$@" if $@;
}
eval { PVE::Ceph::Services::ceph_service_cmd('enable', $monsection) };
warn "Enable ceph-mon\@${monid}.service failed, do manually: $@\n" if $@;
PVE::Ceph::Services::broadcast_ceph_services();
});
die $@ if $@;
# automatically create manager after the first monitor is created
if ($is_first_monitor) {
PVE::API2::Ceph::MGR->createmgr({
node => $param->{node},
id => $param->{node}
})
}
};
return $rpcenv->fork_worker('cephcreatemon', $monsection, $authuser, $worker);
}});
__PACKAGE__->register_method ({
name => 'destroymon',
path => '{monid}',
method => 'DELETE',
description => "Destroy Ceph Monitor and Manager.",
proxyto => 'node',
protected => 1,
permissions => {
check => ['perm', '/', [ 'Sys.Modify' ]],
},
parameters => {
additionalProperties => 0,
properties => {
node => get_standard_option('pve-node'),
monid => {
description => 'Monitor ID',
type => 'string',
pattern => PVE::Ceph::Services::SERVICE_REGEX,
},
},
},
returns => { type => 'string' },
code => sub {
my ($param) = @_;
my $rpcenv = PVE::RPCEnvironment::get();
my $authuser = $rpcenv->get_user();
PVE::Ceph::Tools::check_ceph_inited();
my $cfg = cfs_read_file('ceph.conf');
my $monid = $param->{monid};
my $monsection = "mon.$monid";
my $rados = PVE::RADOS->new();
my $monstat = $rados->mon_command({ prefix => 'quorum_status' });
my $monlist = $monstat->{monmap}->{mons};
my $monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
my $ccname = PVE::Ceph::Tools::get_config('ccname');
my $mondir = "/var/lib/ceph/mon/$ccname-$monid";
$assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
my $worker = sub {
my $upid = shift;
PVE::Cluster::cfs_lock_file('ceph.conf', undef, sub {
# reload info and recheck
$cfg = cfs_read_file('ceph.conf');
# reopen with longer timeout
$rados = PVE::RADOS->new(timeout => PVE::Ceph::Tools::get_config('long_rados_timeout'));
$monhash = PVE::Ceph::Services::get_services_info('mon', $cfg, $rados);
$monstat = $rados->mon_command({ prefix => 'quorum_status' });
$monlist = $monstat->{monmap}->{mons};
my $addrs = [];
my $add_addr = sub {
my ($addr) = @_;
# extract the ip without port and nonce (if present)
($addr) = $addr =~ m|^(.*):\d+(/\d+)?$|;
($addr) = $addr =~ m|^\[?(.*?)\]?$|; # remove brackets
push @{$addrs}, $addr;
};
for my $mon (@$monlist) {
if ($mon->{name} eq $monid) {
if ($mon->{public_addrs} && $mon->{public_addrs}->{addrvec}) {
my $addrvec = $mon->{public_addrs}->{addrvec};
for my $addr (@{$addrvec}) {
$add_addr->($addr->{addr});
}
} else {
$add_addr->($mon->{public_addr} // $mon->{addr});
}
last;
}
}
$assert_mon_can_remove->($monhash, $monlist, $monid, $mondir);
# this also stops the service
$rados->mon_command({ prefix => "mon remove", name => $monid, format => 'plain' });
# delete section
delete $cfg->{$monsection};
# delete from mon_host
if (my $monhost = $cfg->{global}->{mon_host}) {
my $mon_host_ips = $ips_from_mon_host->($cfg->{global}->{mon_host});
for my $addr (@{$addrs}) {
$monhost = $remove_addr_from_mon_host->($monhost, $addr);
# also remove matching IPs that differ syntactically
if (PVE::JSONSchema::pve_verify_ip($addr, 1)) {
$addr = PVE::Network::canonical_ip($addr);
for my $mon_host_ip (@{$mon_host_ips}) {
# match canonical addresses, but remove as present in mon_host
if (PVE::Network::canonical_ip($mon_host_ip) eq $addr) {
$monhost = $remove_addr_from_mon_host->($monhost, $mon_host_ip);
}
}
}
}
$cfg->{global}->{mon_host} = $monhost;
}
cfs_write_file('ceph.conf', $cfg);
File::Path::remove_tree($mondir);
eval { PVE::Ceph::Services::ceph_service_cmd('disable', $monsection) };
warn $@ if $@;
PVE::Ceph::Services::broadcast_ceph_services();
});
die $@ if $@;
};
return $rpcenv->fork_worker('cephdestroymon', $monsection, $authuser, $worker);
}});
1;