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 $@;

		    print "Configuring keyring for ceph-crash.service\n";
		    eval {
			PVE::Ceph::Tools::create_or_update_crash_keyring_file();
			$cfg->{'client.crash'}->{keyring} = '/etc/pve/ceph/$cluster.$name.keyring';
			cfs_write_file('ceph.conf', $cfg);
		    };
		    warn "Unable to configure keyring for ceph-crash.service: $@" 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;
